Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shapes support for lines in D3 #368

Merged
merged 11 commits into from
Dec 27, 2023
173 changes: 173 additions & 0 deletions src/plugins/d3/__stories__/line/Shapes.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React from 'react';
import {StoryObj} from '@storybook/react';
import {Button} from '@gravity-ui/uikit';
import {settings} from '../../../../libs';
import {D3Plugin} from '../..';
import {ChartKitWidgetData, LineSeriesData, LineSeries, DashStyle} from '../../../../types';
import {ChartKit} from '../../../../components/ChartKit';
import nintendoGames from '../../examples/nintendoGames';
import {HighchartsPlugin} from '../../../highcharts';

const SHAPES = {
[DashStyle.Solid]: 1,
[DashStyle.Dash]: 2,
[DashStyle.Dot]: 3,
[DashStyle.ShortDashDot]: 4,
[DashStyle.LongDash]: 5,
[DashStyle.LongDashDot]: 6,
[DashStyle.ShortDot]: 7,
[DashStyle.LongDashDotDot]: 8,
[DashStyle.ShortDash]: 9,
[DashStyle.DashDot]: 10,
[DashStyle.ShortDashDotDot]: 11,
};
artemipanchuk marked this conversation as resolved.
Show resolved Hide resolved

const selectShapes = (): DashStyle[] => Object.values(DashStyle);
const getShapesOrder = () => selectShapes().sort((a, b) => SHAPES[a] - SHAPES[b]);

const SHAPES_IN_ORDER = getShapesOrder();

function prepareData(): ChartKitWidgetData {
const games = nintendoGames.filter((d) => {
return d.date && d.user_score;
});

const byGenre = (genre: string) => {
return games
.filter((d) => d.genres.includes(genre))
.map((d) => {
return {
x: d.date,
y: d.user_score,
label: d.title,
};
}) as LineSeriesData[];
};

return {
series: {
options: {
line: {
lineWidth: 2,
},
},
data: [
{
name: '3D',
type: 'line',
data: byGenre('3D'),
dataLabels: {
enabled: true,
},
},
{
name: '2D',
type: 'line',
data: byGenre('2D'),
dataLabels: {
enabled: true,
},
},
{
name: 'Strategy',
type: 'line',
data: byGenre('Strategy'),
dataLabels: {
enabled: true,
},
},
{
name: 'Shooter',
type: 'line',
data: byGenre('Shooter'),
dataLabels: {
enabled: true,
},
},
],
},
xAxis: {
type: 'datetime',
title: {
text: 'Release date',
},
},
yAxis: [
{
title: {text: 'User score'},
labels: {
enabled: true,
},
ticks: {
pixelInterval: 120,
},
},
],
};
}

const ChartStory = ({data}: {data: ChartKitWidgetData}) => {
const [shown, setShown] = React.useState(false);

if (!shown) {
settings.set({plugins: [D3Plugin, HighchartsPlugin]});
return <Button onClick={() => setShown(true)}>Show chart</Button>;
}

(data.series.data as LineSeries[]).forEach((graph, i) => {
graph.dashStyle = SHAPES_IN_ORDER[i % SHAPES_IN_ORDER.length];
});

return (
<>
<div
style={{
height: '80vh',
width: '100%',
}}
>
<ChartKit type="d3" data={{...data, title: {text: 'D3'}}} />
</div>
<div
style={{
height: '80vh',
width: '100%',
marginTop: 20,
}}
>
<ChartKit
type="highcharts"
data={{
data: {
graphs: data.series.data,
},
config: {},
libraryConfig: {
title: {text: 'Highcharts'},
xAxis: {
type: 'datetime',
},
},
}}
/>
</div>
</>
);
};

export const ShapesLineChartStory: StoryObj<typeof ChartStory> = {
name: 'Shapes',
args: {
data: prepareData(),
},
argTypes: {
data: {
control: 'object',
},
},
};

export default {
title: 'Plugins/D3/Line',
component: ChartStory,
};
kuzmadom marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 9 additions & 0 deletions src/plugins/d3/renderer/components/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {
LegendConfig,
} from '../hooks';

import {getLineDashArray} from '../hooks/useShapes/utils';

const b = block('d3-legend');

type Props = {
Expand Down Expand Up @@ -139,6 +141,13 @@ function renderLegendSymbol(args: {
.attr('class', className)
.style('stroke', color);

if (d.dashStyle) {
kuzmadom marked this conversation as resolved.
Show resolved Hide resolved
element.attr(
'stroke-dasharray',
getLineDashArray(d.dashStyle, d.symbol.strokeWidth),
);
}

break;
}
case 'rect': {
Expand Down
12 changes: 12 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import merge from 'lodash/merge';
import {
ChartKitWidgetSeries,
ChartKitWidgetSeriesOptions,
DashStyle,
LineCap,
LineSeries,
RectLegendSymbolOptions,
} from '../../../../../types';
Expand Down Expand Up @@ -36,6 +38,14 @@ type PrepareLineSeriesArgs = {
legend: PreparedLegend;
};

function prepareDashStyle(series: LineSeries) {
return series.dashStyle || DashStyle.Solid;
}

function prepareLinecap(series: LineSeries) {
return series.linecap || LineCap.None;
kuzmadom marked this conversation as resolved.
Show resolved Hide resolved
}

function prepareLineLegendSymbol(
series: ChartKitWidgetSeries,
seriesOptions?: ChartKitWidgetSeriesOptions,
Expand Down Expand Up @@ -103,6 +113,8 @@ export function prepareLineSeries(args: PrepareLineSeriesArgs) {
allowOverlap: get(series, 'dataLabels.allowOverlap', false),
},
marker: prepareMarker(series, seriesOptions),
dashStyle: prepareDashStyle(series),
linecap: prepareLinecap(series),
kuzmadom marked this conversation as resolved.
Show resolved Hide resolved
};

return prepared;
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
ConnectorShape,
ConnectorCurve,
PathLegendSymbolOptions,
DashStyle,
LineCap,
AreaSeries,
AreaSeriesData,
} from '../../../../../types';
Expand Down Expand Up @@ -44,6 +46,7 @@ export type LegendItem = {
symbol: PreparedLegendSymbol;
textWidth: number;
visible?: boolean;
dashStyle?: DashStyle;
};

export type LegendConfig = {
Expand Down Expand Up @@ -155,6 +158,8 @@ export type PreparedLineSeries = {
};
};
};
dashStyle: DashStyle;
linecap: LineCap;
} & BasePreparedSeries;

export type PreparedAreaSeries = {
Expand Down
7 changes: 4 additions & 3 deletions src/plugins/d3/renderer/hooks/useShapes/line/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {MarkerData, PointData, PreparedLineData} from './types';
import type {TooltipDataChunkLine} from '../../../../../../types';
import type {LabelData} from '../../../types';
import {filterOverlappingLabels} from '../../../utils';
import {setActiveState} from '../utils';
import {getLineDashArray, setActiveState} from '../utils';

const b = block('d3-line');

Expand Down Expand Up @@ -88,8 +88,9 @@ export const LineSeriesShapes = (args: Args) => {
.attr('fill', 'none')
.attr('stroke', (d) => d.color)
.attr('stroke-width', (d) => d.width)
.attr('stroke-linejoin', 'round')
.attr('stroke-linecap', 'round');
.attr('stroke-linejoin', (d) => d.linecap)
.attr('stroke-linecap', (d) => d.linecap)
.attr('stroke-dasharray', (d) => getLineDashArray(d.dashStyle, d.width));
kuzmadom marked this conversation as resolved.
Show resolved Hide resolved

let dataLabels = preparedData.reduce((acc, d) => {
return acc.concat(d.labels);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const prepareLineData = (args: {
}));
}

acc.push({
const result: PreparedLineData = {
points,
markers,
labels,
Expand All @@ -80,7 +80,11 @@ export const prepareLineData = (args: {
hovered: false,
active: true,
id: s.id,
});
dashStyle: s.dashStyle,
linecap: s.linecap,
};

acc.push(result);

return acc;
}, []);
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/d3/renderer/hooks/useShapes/line/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {PreparedLineSeries} from '../../useSeries/types';
import {LineSeriesData} from '../../../../../../types';
import {DashStyle, LineCap, LineSeriesData} from '../../../../../../types';
import {LabelData} from '../../../types';

export type PointData = {
Expand All @@ -25,4 +25,6 @@ export type PreparedLineData = {
hovered: boolean;
active: boolean;
labels: LabelData[];
dashStyle: DashStyle;
linecap: LineCap;
};
22 changes: 21 additions & 1 deletion src/plugins/d3/renderer/hooks/useShapes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {BaseType, ScaleBand, ScaleLinear, ScaleTime} from 'd3';
import {select} from 'd3';
import get from 'lodash/get';

import type {BasicInactiveState} from '../../../../../types';
import type {BasicInactiveState, DashStyle} from '../../../../../types';
import {getDataCategoryValue} from '../../utils';
import type {ChartScale} from '../useAxisScales';
import type {PreparedAxis} from '../useChartOptions/types';
Expand Down Expand Up @@ -64,3 +64,23 @@ export function setActiveState<T extends {active?: boolean}>(args: {

return datum;
}

export const getLineDashArray = (dashStyle: DashStyle, strokeWidth = 2) => {
const value = dashStyle.toLowerCase();

const arrayValue = value
.replace('shortdashdotdot', '3,1,1,1,1,1,')
.replace('shortdashdot', '3,1,1,1')
.replace('shortdot', '1,1,')
.replace('shortdash', '3,1,')
.replace('longdash', '8,3,')
.replace(/dot/g, '1,3,')
.replace('dash', '4,3,')
.replace(/,$/, '')
.split(',')
.map((part) => {
return `${parseInt(part, 10) * strokeWidth}`;
});

return arrayValue.join(',').replace(/NaN/g, 'none');
};
3 changes: 3 additions & 0 deletions src/types/widget-data/line.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {BaseSeries, BaseSeriesData} from './base';
import type {ChartKitWidgetLegend, RectLegendSymbolOptions} from './legend';
import type {PointMarkerOptions} from './marker';
import type {DashStyle, LineCap} from './series';

export type LineSeriesData<T = any> = BaseSeriesData<T> & {
/**
Expand Down Expand Up @@ -45,4 +46,6 @@ export type LineSeries<T = any> = BaseSeries & {
};
/** Options for the point markers of line series */
marker?: LineMarkerOptions;
dashStyle?: DashStyle;
linecap?: LineCap;
kuzmadom marked this conversation as resolved.
Show resolved Hide resolved
};
Loading
Loading