Skip to content

Commit

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

* fix zero value

* return maxRowCount=3 for multiline story
  • Loading branch information
kuzmadom authored Jul 3, 2024
1 parent 1bc167f commit 025701e
Show file tree
Hide file tree
Showing 14 changed files with 317 additions and 102 deletions.
8 changes: 0 additions & 8 deletions src/plugins/d3/__stories__/line/Line.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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 @@ -39,13 +38,6 @@ 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
116 changes: 116 additions & 0 deletions src/plugins/d3/__stories__/other/AxisTitle.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react';

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

import {ChartKit} from '../../../../components/ChartKit';
import {Loader} from '../../../../components/Loader/Loader';
import {settings} from '../../../../libs';
import type {ChartKitWidgetAxis, ChartKitWidgetData} from '../../../../types';
import {ExampleWrapper} from '../../examples/ExampleWrapper';
import {D3Plugin} from '../../index';

const AxisTitle = () => {
const [loading, setLoading] = React.useState(true);

React.useEffect(() => {
settings.set({plugins: [D3Plugin]});
setLoading(false);
}, []);

if (loading) {
return <Loader />;
}

const longText = `One dollar and eighty-seven cents. That was all.
And sixty cents of it was in pennies. Pennies saved one and two at a time by bulldozing
the grocer and the vegetable man and the butcher until one's cheeks burned with the silent
imputation of parsimony that such close dealing implied. Three times Della counted it.
One dollar and eighty - seven cents.`;
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">Text 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>
<Row space={1}>
<Text variant="subheader-3">Long text</Text>
</Row>
<Row space={3}>
<Col s={6}>
<ExampleWrapper>
<ChartKit
type="d3"
data={{
...getWidgetData({text: longText}),
title: {text: 'default behaviour'},
}}
/>
</ExampleWrapper>
</Col>
<Col s={6}>
<ExampleWrapper>
<ChartKit
type="d3"
data={{
...getWidgetData({text: longText, maxRowCount: 3}),
title: {text: 'multiline'},
}}
/>
</ExampleWrapper>
</Col>
</Row>
</Container>
);
};

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

export default {
title: 'Plugins/D3/other',
component: AxisTitle,
};
60 changes: 0 additions & 60 deletions src/plugins/d3/examples/line/AxisTitle.tsx

This file was deleted.

35 changes: 26 additions & 9 deletions src/plugins/d3/renderer/components/AxisX.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {block} from '../../../../utils/cn';
import type {ChartScale, PreparedAxis, PreparedSplit} from '../hooks';
import {
formatAxisTickLabel,
getAxisTitleRows,
getClosestPointsRange,
getMaxTickCount,
getScaleTicks,
getTicksCount,
setEllipsisForOverflowText,
handleOverflowingText,
} from '../utils';
import {axisBottom} from '../utils/axis-generators';

Expand Down Expand Up @@ -42,21 +43,27 @@ function getLabelFormatter({axis, scale}: {axis: PreparedAxis; scale: ChartScale
};
}

export function getTitlePosition(axis: PreparedAxis, axisSize: number) {
export function getTitlePosition(args: {axis: PreparedAxis; width: number; rowCount: number}) {
const {axis, width, rowCount} = args;
if (rowCount < 1) {
return {x: 0, y: 0};
}

let x;
const y = axis.title.height + axis.title.margin + axis.labels.height + axis.labels.margin;
const y =
axis.title.height / rowCount + 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;
x = width - axis.title.width / 2;
break;
}
case 'center': {
x = axisSize / 2;
x = width / 2;
break;
}
}
Expand Down Expand Up @@ -110,17 +117,27 @@ export const AxisX = React.memo(function AxisX(props: Props) {

// add an axis header if necessary
if (axis.title.text) {
const titleRows = getAxisTitleRows({axis, textMaxWidth: width});
svgElement
.append('text')
.attr('class', b('title'))
.attr('text-anchor', 'middle')
.attr('transform', () => {
const {x, y} = getTitlePosition(axis, width);
const {x, y} = getTitlePosition({axis, width, rowCount: titleRows.length});
return `translate(${x}, ${y})`;
})
.attr('font-size', axis.title.style.fontSize)
.text(axis.title.text)
.call(setEllipsisForOverflowText, width);
.attr('text-anchor', 'middle')
.selectAll('tspan')
.data(titleRows)
.join('tspan')
.attr('x', 0)
.attr('y', (d) => d.y)
.text((d) => d.text)
.each((_d, index, nodes) => {
if (index === axis.title.maxRowCount - 1) {
handleOverflowingText(nodes[index] as SVGTSpanElement, width);
}
});
}
}, [axis, width, totalHeight, scale, split]);

Expand Down
47 changes: 35 additions & 12 deletions src/plugins/d3/renderer/components/AxisY.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import {
calculateSin,
formatAxisTickLabel,
getAxisHeight,
getAxisTitleRows,
getClosestPointsRange,
getScaleTicks,
getTicksCount,
handleOverflowingText,
parseTransformStyle,
setEllipsisForOverflowText,
setEllipsisForOverflowTexts,
wrapText,
} from '../utils';

const b = block('d3-axis');
Expand Down Expand Up @@ -93,21 +95,32 @@ function getAxisGenerator(args: {
return axisGenerator;
}

function getTitlePosition(axis: PreparedAxis, axisSize: number) {
const x = -(axis.title.margin + axis.labels.margin + axis.labels.width);
function getTitlePosition(args: {axis: PreparedAxis; axisHeight: number; rowCount: number}) {
const {axis, axisHeight, rowCount} = args;
if (rowCount < 1) {
return {x: 0, y: 0};
}

const x = -(
axis.title.height -
axis.title.height / rowCount +
axis.title.margin +
axis.labels.margin +
axis.labels.width
);
let y;

switch (axis.title.align) {
case 'left': {
y = axisSize - axis.title.width / 2;
y = axisHeight - axis.title.width / 2;
break;
}
case 'right': {
y = axis.title.width / 2;
break;
}
case 'center': {
y = axisSize / 2;
y = axisHeight / 2;
break;
}
}
Expand Down Expand Up @@ -229,16 +242,26 @@ export const AxisY = (props: Props) => {
.attr('text-anchor', 'middle')
.attr('font-size', (d) => d.title.style.fontSize)
.attr('transform', (d) => {
const {x, y} = getTitlePosition(d, height);
const titleRows = wrapText({
text: d.title.text,
style: d.title.style,
width: height,
});
const rowCount = Math.min(titleRows.length, d.title.maxRowCount);
const {x, y} = getTitlePosition({axis: d, axisHeight: height, rowCount});
const angle = d.position === 'left' ? -90 : 90;
return `translate(${x}, ${y}) rotate(${angle})`;
})
.text((d) => d.title.text)
.each((_d, index, node) => {
return setEllipsisForOverflowText(
select(node[index]) as Selection<SVGTextElement, unknown, null, unknown>,
height,
);
.selectAll('tspan')
.data((d) => getAxisTitleRows({axis: d, textMaxWidth: height}))
.join('tspan')
.attr('x', 0)
.attr('y', (d) => d.y)
.text((d) => d.text)
.each((_d, index, nodes) => {
if (index === nodes.length - 1) {
handleOverflowingText(nodes[index] as SVGTSpanElement, height);
}
});
}, [axes, width, height, scale, split]);

Expand Down
3 changes: 2 additions & 1 deletion src/plugins/d3/renderer/components/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ export const Chart = (props: Props) => {
getPreparedYAxis({
series: data.series.data,
yAxis: data.yAxis,
height,
}),
[data],
[data, height],
);

const {
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/d3/renderer/components/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
alignment-baseline: after-edge;
fill: var(--g-color-text-secondary);
}

&__title tspan {
alignment-baseline: after-edge;
}
}

.chartkit-d3-legend {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/d3/renderer/constants/defaults/axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const axisTitleDefaults: AxisTitleDefaults = {
fontSize: '14px',
},
align: 'center',
maxRowCount: 1,
};

export const xAxisTitleDefaults: AxisTitleDefaults = {
Expand Down
Loading

0 comments on commit 025701e

Please sign in to comment.