Skip to content

Commit

Permalink
feat(D3 plugin): axis title alignment (#497)
Browse files Browse the repository at this point in the history
* feat(d3 plugin): axis title alignment

* fix type import

* fix title type
  • Loading branch information
kuzmadom authored Jul 2, 2024
1 parent 6aa9931 commit 1bc167f
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 22 deletions.
8 changes: 8 additions & 0 deletions src/plugins/d3/__stories__/line/Line.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {StoryObj} from '@storybook/react';
import {D3Plugin} from '../..';
import {Loader} from '../../../../components/Loader/Loader';
import {settings} from '../../../../libs';
import {AxisTitle} from '../../examples/line/AxisTitle';
import {LineWithLogarithmicAxis} from '../../examples/line/LogarithmicAxis';

const ChartStory = ({Chart}: {Chart: React.FC}) => {
Expand Down Expand Up @@ -38,6 +39,13 @@ export const LogarithmicAxis: StoryObj<typeof ChartStory> = {
},
};

export const AxisTitleStory: StoryObj<typeof ChartStory> = {
name: 'Axis title',
args: {
Chart: AxisTitle,
},
};

export default {
title: 'Plugins/D3/Line',
component: ChartStory,
Expand Down
60 changes: 60 additions & 0 deletions src/plugins/d3/examples/line/AxisTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';

import {Col, Container, Row, Text} from '@gravity-ui/uikit';

import {ChartKit} from '../../../../components/ChartKit';
import type {ChartKitWidgetAxis, ChartKitWidgetData} from '../../../../types';
import {ExampleWrapper} from '../ExampleWrapper';

export const AxisTitle = () => {
const getWidgetData = (title: ChartKitWidgetAxis['title']): ChartKitWidgetData => ({
yAxis: [
{
title,
},
],
xAxis: {
title,
},
series: {
data: [
{
type: 'line',
name: 'Line series',
data: [
{x: 1, y: 10},
{x: 2, y: 100},
],
},
],
},
});

return (
<Container spaceRow={5}>
<Row space={1}>
<Text variant="subheader-3">Axis title alignment</Text>
</Row>
<Row space={3}>
<Col s={4}>
<ExampleWrapper>
<ChartKit type="d3" data={getWidgetData({text: 'Left', align: 'left'})} />
</ExampleWrapper>
</Col>
<Col s={4}>
<ExampleWrapper>
<ChartKit
type="d3"
data={getWidgetData({text: 'Center', align: 'center'})}
/>
</ExampleWrapper>
</Col>
<Col s={4}>
<ExampleWrapper>
<ChartKit type="d3" data={getWidgetData({text: 'Right', align: 'right'})} />
</ExampleWrapper>
</Col>
</Row>
</Container>
);
};
31 changes: 26 additions & 5 deletions src/plugins/d3/renderer/components/AxisX.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ function getLabelFormatter({axis, scale}: {axis: PreparedAxis; scale: ChartScale
};
}

export function getTitlePosition(axis: PreparedAxis, axisSize: number) {
let x;
const y = axis.title.height + axis.title.margin + axis.labels.height + axis.labels.margin;

switch (axis.title.align) {
case 'left': {
x = axis.title.width / 2;
break;
}
case 'right': {
x = axisSize - axis.title.width / 2;
break;
}
case 'center': {
x = axisSize / 2;
break;
}
}

return {x, y};
}

export const AxisX = React.memo(function AxisX(props: Props) {
const {axis, width, height: totalHeight, scale, split} = props;
const ref = React.useRef<SVGGElement | null>(null);
Expand Down Expand Up @@ -88,15 +110,14 @@ export const AxisX = React.memo(function AxisX(props: Props) {

// add an axis header if necessary
if (axis.title.text) {
const y =
axis.title.height + axis.title.margin + axis.labels.height + axis.labels.margin;

svgElement
.append('text')
.attr('class', b('title'))
.attr('text-anchor', 'middle')
.attr('x', width / 2)
.attr('y', y)
.attr('transform', () => {
const {x, y} = getTitlePosition(axis, width);
return `translate(${x}, ${y})`;
})
.attr('font-size', axis.title.style.fontSize)
.text(axis.title.text)
.call(setEllipsisForOverflowText, width);
Expand Down
30 changes: 27 additions & 3 deletions src/plugins/d3/renderer/components/AxisY.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ function getAxisGenerator(args: {
return axisGenerator;
}

function getTitlePosition(axis: PreparedAxis, axisSize: number) {
const x = -(axis.title.margin + axis.labels.margin + axis.labels.width);
let y;

switch (axis.title.align) {
case 'left': {
y = axisSize - axis.title.width / 2;
break;
}
case 'right': {
y = axis.title.width / 2;
break;
}
case 'center': {
y = axisSize / 2;
break;
}
}

return {x, y};
}

export const AxisY = (props: Props) => {
const {axes, width, height: totalHeight, scale, split} = props;
const height = getAxisHeight({split, boundsHeight: totalHeight});
Expand Down Expand Up @@ -205,10 +227,12 @@ export const AxisY = (props: Props) => {
.append('text')
.attr('class', b('title'))
.attr('text-anchor', 'middle')
.attr('dy', (d) => -(d.title.margin + d.labels.margin + d.labels.width))
.attr('dx', (d) => (d.position === 'left' ? -height / 2 : height / 2))
.attr('font-size', (d) => d.title.style.fontSize)
.attr('transform', (d) => (d.position === 'left' ? 'rotate(-90)' : 'rotate(90)'))
.attr('transform', (d) => {
const {x, y} = getTitlePosition(d, height);
const angle = d.position === 'left' ? -90 : 90;
return `translate(${x}, ${y}) rotate(${angle})`;
})
.text((d) => d.title.text)
.each((_d, index, node) => {
return setEllipsisForOverflowText(
Expand Down
19 changes: 14 additions & 5 deletions src/plugins/d3/renderer/constants/defaults/axis.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ChartKitWidgetAxisType} from '../../../../../types';
import type {BaseTextStyle, ChartKitWidgetAxis, ChartKitWidgetAxisType} from '../../../../../types';

export const axisLabelsDefaults = {
margin: 10,
Expand All @@ -7,16 +7,25 @@ export const axisLabelsDefaults = {
maxWidth: 80,
};

const axisTitleDefaults = {
fontSize: '14px',
type AxisTitleDefaults = Required<ChartKitWidgetAxis['title']> & {
style: BaseTextStyle;
};

export const xAxisTitleDefaults = {
const axisTitleDefaults: AxisTitleDefaults = {
text: '',
margin: 0,
style: {
fontSize: '14px',
},
align: 'center',
};

export const xAxisTitleDefaults: AxisTitleDefaults = {
...axisTitleDefaults,
margin: 4,
};

export const yAxisTitleDefaults = {
export const yAxisTitleDefaults: AxisTitleDefaults = {
...axisTitleDefaults,
margin: 8,
};
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/d3/renderer/hooks/useChartOptions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
BaseTextStyle,
ChartKitWidgetAxis,
ChartKitWidgetAxisLabels,
ChartKitWidgetAxisTitleAlignment,
ChartKitWidgetAxisType,
ChartKitWidgetData,
ChartMargin,
Expand Down Expand Up @@ -29,9 +30,11 @@ export type PreparedAxis = Omit<ChartKitWidgetAxis, 'type' | 'labels'> & {
labels: PreparedAxisLabels;
title: {
height: number;
width: number;
text: string;
margin: number;
style: BaseTextStyle;
align: ChartKitWidgetAxisTitleAlignment;
};
min?: number;
grid: {
Expand Down
10 changes: 6 additions & 4 deletions src/plugins/d3/renderer/hooks/useChartOptions/x-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ export const getPreparedXAxis = ({
}): PreparedAxis => {
const titleText = get(xAxis, 'title.text', '');
const titleStyle: BaseTextStyle = {
fontSize: get(xAxis, 'title.style.fontSize', xAxisTitleDefaults.fontSize),
...xAxisTitleDefaults.style,
...get(xAxis, 'title.style'),
};
const titleSize = getLabelsSize({labels: [titleText], style: titleStyle});
const labelsStyle = {
fontSize: get(xAxis, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE),
};
Expand All @@ -125,9 +127,9 @@ export const getPreparedXAxis = ({
text: titleText,
style: titleStyle,
margin: get(xAxis, 'title.margin', xAxisTitleDefaults.margin),
height: titleText
? getHorisontalSvgTextHeight({text: titleText, style: titleStyle})
: 0,
height: titleSize.maxHeight,
width: titleSize.maxWidth,
align: get(xAxis, 'title.align', xAxisTitleDefaults.align),
},
min: getAxisMin(xAxis, series),
maxPadding: get(xAxis, 'maxPadding', 0.01),
Expand Down
12 changes: 7 additions & 5 deletions src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,11 @@ export const getPreparedYAxis = ({
fontSize: get(axisItem, 'labels.style.fontSize', DEFAULT_AXIS_LABEL_FONT_SIZE),
};
const titleText = get(axisItem, 'title.text', '');
const titleStyle: BaseTextStyle = {
fontSize: get(axisItem, 'title.style.fontSize', yAxisTitleDefaults.fontSize),
const titleStyle = {
...yAxisTitleDefaults.style,
...get(axisItem, 'title.style'),
};
const titleSize = getLabelsSize({labels: [titleText], style: titleStyle});
const axisType = get(axisItem, 'type', DEFAULT_AXIS_TYPE);
const preparedAxis: PreparedAxis = {
type: axisType,
Expand Down Expand Up @@ -135,9 +137,9 @@ export const getPreparedYAxis = ({
text: titleText,
margin: get(axisItem, 'title.margin', yAxisTitleDefaults.margin),
style: titleStyle,
height: titleText
? getHorisontalSvgTextHeight({text: titleText, style: titleStyle})
: 0,
width: titleSize.maxWidth,
height: titleSize.maxHeight,
align: get(axisItem, 'title.align', yAxisTitleDefaults.align),
},
min: getAxisMin(axisItem, series),
maxPadding: get(axisItem, 'maxPadding', 0.05),
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/d3/renderer/utils/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ export function getLabelsSize({
style?: BaseTextStyle;
rotation?: number;
}) {
if (!labels.filter(Boolean).length) {
return {maxHeight: 0, maxWidth: 0};
}

const container = select(document.body)
.append('div')
.attr('class', 'chartkit chartkit-theme_common');
Expand Down
5 changes: 5 additions & 0 deletions src/types/widget-data/axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {FormatNumberOptions} from '../../plugins/shared';
import type {BaseTextStyle} from './base';

export type ChartKitWidgetAxisType = 'category' | 'datetime' | 'linear' | 'logarithmic';
export type ChartKitWidgetAxisTitleAlignment = 'left' | 'center' | 'right';

export type ChartKitWidgetAxisLabels = {
/** Enable or disable the axis labels. */
Expand Down Expand Up @@ -41,11 +42,15 @@ export type ChartKitWidgetAxis = {
lineColor?: string;
title?: {
text?: string;
/** CSS styles for the title */
style?: Partial<BaseTextStyle>;
/** The pixel distance between the axis labels or line and the title.
*
* Defaults to 4 for horizontal axes, 8 for vertical.
* */
margin?: number;
/** Alignment of the title. */
align?: ChartKitWidgetAxisTitleAlignment;
};
/** The minimum value of the axis. If undefined the min value is automatically calculate. */
min?: number;
Expand Down

0 comments on commit 1bc167f

Please sign in to comment.