Skip to content

Commit

Permalink
feat(D3 plugin): marker for line charts (#361)
Browse files Browse the repository at this point in the history
* feat(D3 plugin): marker for line charts

* add unit test for prepareLineSeries
  • Loading branch information
kuzmadom authored Dec 7, 2023
1 parent 64bcd0b commit 721a12b
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 13 deletions.
10 changes: 9 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type {Config} from '@jest/types';

const esModules = ['@gravity-ui/yagr', 'uplot'].join('|');
const esModules = [
'@gravity-ui/yagr',
'uplot',
'd3',
'd3-array',
'internmap',
'delaunator',
'robust-predicates',
].join('|');

const cfg: Config.InitialOptions = {
verbose: true,
Expand Down
6 changes: 5 additions & 1 deletion src/plugins/d3/__stories__/Showcase.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {Basic as BasicLine} from '../examples/line/Basic';
import {DataLabels as LineWithDataLabels} from '../examples/line/DataLabels';
import {Donut} from '../examples/pie/Donut';
import {LineAndBarXCombinedChart} from '../examples/combined/LineAndBarX';
import {LineWithMarkers} from '../examples/line/LineWithMarkers';

const ShowcaseStory = () => {
const [loading, setLoading] = React.useState(true);
Expand All @@ -44,7 +45,10 @@ const ShowcaseStory = () => {
<Text variant="subheader-1">With data labels</Text>
<LineWithDataLabels />
</Col>
<Col></Col>
<Col>
<Text variant="subheader-1">With markers</Text>
<LineWithMarkers />
</Col>
</Row>
<Row space={1}>
<Text variant="header-2">Bar-x charts</Text>
Expand Down
78 changes: 78 additions & 0 deletions src/plugins/d3/examples/line/LineWithMarkers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import {ChartKit} from '../../../../components/ChartKit';
import type {ChartKitWidgetData, LineSeries, LineSeriesData} from '../../../../types';
import nintendoGames from '../nintendoGames';
import {dateTime} from '@gravity-ui/date-utils';

function prepareData() {
const dataset = nintendoGames.filter(
(d) => d.date && d.user_score && new Date(d.date) > new Date(2022, 0, 1),
);
const data = dataset.map((d) => ({
x: d.date || undefined,
y: d.user_score || undefined,
custom: d,
}));

return {
series: [
{
data,
name: 'Nintendo games',
},
],
};
}

export const LineWithMarkers = () => {
const {series} = prepareData();

const widgetData: ChartKitWidgetData = {
series: {
data: series.map<LineSeries>((s) => ({
type: 'line',
data: s.data.filter((d) => d.x),
name: s.name,
marker: {enabled: true, symbol: 'square'},
})),
},
yAxis: [
{
title: {
text: 'User score',
},
},
],
xAxis: {
type: 'datetime',
title: {
text: 'Release dates',
},
},
tooltip: {
renderer: (d) => {
const point = d.hovered[0]?.data as LineSeriesData;

if (!point) {
return null;
}

const title = point.custom.title;
const score = point.custom.user_score;
const date = dateTime({input: point.custom.date}).format('DD MMM YYYY');

return (
<React.Fragment>
<b>{title}</b>
<br />
Release date: {date}
<br />
User score: {score}
</React.Fragment>
);
},
},
};

return <ChartKit type="d3" data={widgetData} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {DEFAULT_MARKER, prepareLineSeries} from '../prepare-line-series';
import {scaleOrdinal} from 'd3';
import type {LineSeries} from '../../../../../../types';
import type {PreparedLegend} from '../types';
import {DEFAULT_PALETTE} from '../../../constants';

describe('prepareLineSeries', () => {
describe('marker', () => {
const commonArgs = {
colorScale: scaleOrdinal([] as string[], DEFAULT_PALETTE),
legend: {} as PreparedLegend,
};

it('If the marker parameters are not specified, the default values should be applied', () => {
const preparedSeries = prepareLineSeries({
...commonArgs,
series: [{}] as LineSeries[],
});

const actual = preparedSeries.map((s) => s.marker.states.normal);
const expected = [DEFAULT_MARKER];

expect(actual).toEqual(expected);
});

it('Normal state. The settings of a specific series should be prioritized over the seriesOptions', () => {
const preparedSeries = prepareLineSeries({
...commonArgs,
seriesOptions: {
line: {
marker: {
enabled: true,
radius: 100,
symbol: 'square',
},
},
},
series: [
{},
{
marker: {
radius: 200,
symbol: 'circle',
},
},
{marker: {enabled: false}},
] as LineSeries[],
});

const actual = preparedSeries.map((s) => s.marker.states.normal);
const expected = [
{enabled: true, radius: 100, symbol: 'square', borderColor: '', borderWidth: 0},
{enabled: true, radius: 200, symbol: 'circle', borderColor: '', borderWidth: 0},
{enabled: false, radius: 100, symbol: 'square', borderColor: '', borderWidth: 0},
];

expect(actual).toEqual(expected);
});
});
});
46 changes: 43 additions & 3 deletions src/plugins/d3/renderer/hooks/useSeries/prepare-line-series.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {ScaleOrdinal} from 'd3';
import get from 'lodash/get';
import merge from 'lodash/merge';

import {
ChartKitWidgetSeries,
ChartKitWidgetSeriesOptions,
LineSeries,
RectLegendSymbolOptions,
} from '../../../../../types';
import {PreparedLineSeries, PreparedLegend, PreparedSeries, PreparedLegendSymbol} from './types';
import {PreparedLineSeries, PreparedLegend, PreparedLegendSymbol} from './types';

import {
DEFAULT_DATALABELS_PADDING,
Expand All @@ -19,6 +20,14 @@ import {getRandomCKId} from '../../../../../utils';
export const DEFAULT_LEGEND_SYMBOL_SIZE = 16;
export const DEFAULT_LINE_WIDTH = 1;

export const DEFAULT_MARKER = {
enabled: false,
symbol: 'circle',
radius: 4,
borderWidth: 0,
borderColor: '',
};

type PrepareLineSeriesArgs = {
colorScale: ScaleOrdinal<string, string>;
series: LineSeries[];
Expand All @@ -41,7 +50,35 @@ function prepareLineLegendSymbol(
};
}

export function prepareLineSeries(args: PrepareLineSeriesArgs): PreparedSeries[] {
function prepareMarker(series: LineSeries, seriesOptions?: ChartKitWidgetSeriesOptions) {
const seriesHoverState = get(seriesOptions, 'line.states.hover');
const markerNormalState = Object.assign(
{},
DEFAULT_MARKER,
seriesOptions?.line?.marker,
series.marker,
);
const hoveredMarkerDefaultOptions = {
enabled: true,
radius: markerNormalState.radius,
borderWidth: 1,
borderColor: '#ffffff',
halo: {
enabled: true,
opacity: 0.25,
radius: 10,
},
};

return {
states: {
normal: markerNormalState,
hover: merge(hoveredMarkerDefaultOptions, seriesHoverState?.marker),
},
};
}

export function prepareLineSeries(args: PrepareLineSeriesArgs) {
const {colorScale, series: seriesList, seriesOptions, legend} = args;
const defaultLineWidth = get(seriesOptions, 'line.lineWidth', DEFAULT_LINE_WIDTH);

Expand All @@ -50,7 +87,7 @@ export function prepareLineSeries(args: PrepareLineSeriesArgs): PreparedSeries[]
const name = series.name || '';
const color = series.color || colorScale(name);

return {
const prepared: PreparedLineSeries = {
type: series.type,
color,
lineWidth: get(series, 'lineWidth', defaultLineWidth),
Expand All @@ -68,6 +105,9 @@ export function prepareLineSeries(args: PrepareLineSeriesArgs): PreparedSeries[]
padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
allowOverlap: get(series, 'dataLabels.allowOverlap', false),
},
marker: prepareMarker(series, seriesOptions),
};

return prepared;
}, []);
}
22 changes: 22 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ export type PreparedLineSeries = {
padding: number;
allowOverlap: boolean;
};
marker: {
states: {
normal: {
symbol: string;
enabled: boolean;
radius: number;
borderWidth: number;
borderColor: string;
};
hover: {
enabled: boolean;
radius: number;
borderWidth: number;
borderColor: string;
halo: {
enabled: boolean;
opacity: number;
radius: number;
};
};
};
};
} & BasePreparedSeries;

export type PreparedSeries =
Expand Down
Loading

0 comments on commit 721a12b

Please sign in to comment.