Skip to content

Commit

Permalink
feat(D3 plugin): basic area chart
Browse files Browse the repository at this point in the history
  • Loading branch information
kuzmadom committed Dec 11, 2023
1 parent 721a12b commit fadf6f7
Show file tree
Hide file tree
Showing 18 changed files with 780 additions and 10 deletions.
12 changes: 12 additions & 0 deletions src/plugins/d3/__stories__/Showcase.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {Container, Row, Col, Text} from '@gravity-ui/uikit';
import {BasicPie} from '../examples/pie/Basic';
import {Basic as BasicScatter} from '../examples/scatter/Basic';
import {Basic as BasicLine} from '../examples/line/Basic';
import {Basic as BasicArea} from '../examples/area/Basic';
import {DataLabels as LineWithDataLabels} from '../examples/line/DataLabels';
import {Donut} from '../examples/pie/Donut';
import {LineAndBarXCombinedChart} from '../examples/combined/LineAndBarX';
Expand Down Expand Up @@ -50,6 +51,17 @@ const ShowcaseStory = () => {
<LineWithMarkers />
</Col>
</Row>
<Row space={1}>
<Text variant="header-2">Area charts</Text>
</Row>
<Row space={3} style={{minHeight: 280}}>
<Col>
<Text variant="subheader-1">Basic area chart</Text>
<BasicArea />
</Col>
<Col></Col>
<Col></Col>
</Row>
<Row space={1}>
<Text variant="header-2">Bar-x charts</Text>
</Row>
Expand Down
40 changes: 40 additions & 0 deletions src/plugins/d3/__stories__/area/Basic.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import {StoryObj} from '@storybook/react';
import {withKnobs} from '@storybook/addon-knobs';
import {Button} from '@gravity-ui/uikit';
import {settings} from '../../../../libs';
import {D3Plugin} from '../..';
import {Basic} from '../../examples/area/Basic';

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

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

return (
<div
style={{
height: '80vh',
width: '100%',
}}
>
<Chart />
</div>
);
};

export const BasicAreaChartStory: StoryObj<typeof ChartStory> = {
name: 'Basic',
args: {
Chart: Basic,
},
};

export default {
title: 'Plugins/D3/Area',
decorators: [withKnobs],
component: ChartStory,
};
85 changes: 85 additions & 0 deletions src/plugins/d3/examples/area/Basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import {ChartKit} from '../../../../components/ChartKit';
import type {ChartKitWidgetData, AreaSeries, AreaSeriesData} from '../../../../types';
import nintendoGames from '../nintendoGames';
import {dateTime} from '@gravity-ui/date-utils';

function prepareData(): AreaSeries[] {
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} (${d.user_score})`,
custom: d,
};
}) as AreaSeriesData[];
};

return [
{
name: 'Strategy',
type: 'area',
data: byGenre('Strategy'),
},
{
name: 'Shooter',
type: 'area',
data: byGenre('Shooter'),
},
];
}

export const Basic = () => {
const series = prepareData();

const widgetData: ChartKitWidgetData = {
series: {
data: series,
},
yAxis: [
{
title: {
text: 'User score',
},
},
],
xAxis: {
type: 'datetime',
title: {
text: 'Release dates',
},
},
tooltip: {
renderer: (d) => {
const point = d.hovered[0]?.data as AreaSeriesData;

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
Expand Up @@ -50,6 +50,7 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => {
switch (series.type) {
case 'scatter':
case 'line':
case 'area':
case 'bar-x': {
const xRow = getXRowData(xAxis, data);
const yRow = getYRowData(yAxis, data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export const TooltipTriggerArea = (args: Args) => {

const xLineData = React.useMemo(() => {
const result = shapesData
.filter((sd) => sd.series.type === 'line')
.filter((sd) => ['line', 'area'].includes(sd.series.type))
.reduce((acc, sd) => {
return acc.concat(
(sd as PreparedLineData).points.map<XLineData>((d) => ({
Expand Down
12 changes: 12 additions & 0 deletions src/plugins/d3/renderer/constants/defaults/series-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,16 @@ export const seriesOptionsDefaults: SeriesOptionsDefaults = {
},
},
},
area: {
states: {
hover: {
enabled: true,
brightness: 0.3,
},
inactive: {
enabled: true,
opacity: 0.5,
},
},
},
};
89 changes: 89 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/prepare-area-series.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {ScaleOrdinal} from 'd3';
import get from 'lodash/get';
import merge from 'lodash/merge';

import {ChartKitWidgetSeriesOptions, AreaSeries} from '../../../../../types';
import {PreparedAreaSeries, PreparedLegend} from './types';

import {DEFAULT_DATALABELS_PADDING, DEFAULT_DATALABELS_STYLE} from './constants';
import {getRandomCKId} from '../../../../../utils';
import {prepareLegendSymbol} from './utils';

export const DEFAULT_LINE_WIDTH = 1;

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

type PrepareAreaSeriesArgs = {
colorScale: ScaleOrdinal<string, string>;
series: AreaSeries[];
seriesOptions?: ChartKitWidgetSeriesOptions;
legend: PreparedLegend;
};

function prepareMarker(series: AreaSeries, seriesOptions?: ChartKitWidgetSeriesOptions) {
const seriesHoverState = get(seriesOptions, 'area.states.hover');
const markerNormalState = Object.assign(
{},
DEFAULT_MARKER,
seriesOptions?.area?.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 prepareAreaSeries(args: PrepareAreaSeriesArgs) {
const {colorScale, series: seriesList, seriesOptions, legend} = args;
const defaultAreaWidth = get(seriesOptions, 'area.lineWidth', DEFAULT_LINE_WIDTH);

return seriesList.map<PreparedAreaSeries>((series) => {
const id = getRandomCKId();
const name = series.name || '';
const color = series.color || colorScale(name);

const prepared: PreparedAreaSeries = {
type: series.type,
color,
lineWidth: get(series, 'lineWidth', defaultAreaWidth),
name,
id,
visible: get(series, 'visible', true),
legend: {
enabled: get(series, 'legend.enabled', legend.enabled),
symbol: prepareLegendSymbol(series),
},
data: series.data,
dataLabels: {
enabled: series.dataLabels?.enabled || false,
style: Object.assign({}, DEFAULT_DATALABELS_STYLE, series.dataLabels?.style),
padding: get(series, 'dataLabels.padding', DEFAULT_DATALABELS_PADDING),
allowOverlap: get(series, 'dataLabels.allowOverlap', false),
},
marker: prepareMarker(series, seriesOptions),
};

return prepared;
}, []);
}
10 changes: 10 additions & 0 deletions src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import get from 'lodash/get';
import type {ScaleOrdinal} from 'd3';

import type {
AreaSeries,
BarXSeries,
BarYSeries,
ChartKitWidgetSeries,
Expand All @@ -18,6 +19,7 @@ import {prepareBarYSeries} from './prepare-bar-y';
import {prepareLegendSymbol} from './utils';
import {ChartKitError} from '../../../../../libs';
import {preparePieSeries} from './prepare-pie';
import {prepareAreaSeries} from './prepare-area-series';

type PrepareAxisRelatedSeriesArgs = {
colorScale: ScaleOrdinal<string, string>;
Expand Down Expand Up @@ -76,6 +78,14 @@ export function prepareSeries(args: {
colorScale,
});
}
case 'area': {
return prepareAreaSeries({
series: series as AreaSeries[],
seriesOptions,
legend,
colorScale,
});
}
default: {
throw new ChartKitError({
message: `Series type "${type}" does not support data preparation for series that do not support the presence of axes`,
Expand Down
39 changes: 38 additions & 1 deletion 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,
AreaSeries,
AreaSeriesData,
} from '../../../../../types';
import type {SeriesOptionsDefaults} from '../../constants';

Expand Down Expand Up @@ -155,11 +157,46 @@ export type PreparedLineSeries = {
};
} & BasePreparedSeries;

export type PreparedAreaSeries = {
type: AreaSeries['type'];
data: AreaSeriesData[];
lineWidth: number;
dataLabels: {
enabled: boolean;
style: BaseTextStyle;
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 =
| PreparedScatterSeries
| PreparedBarXSeries
| PreparedBarYSeries
| PreparedPieSeries
| PreparedLineSeries;
| PreparedLineSeries
| PreparedAreaSeries;

export type PreparedSeriesOptions = SeriesOptionsDefaults;
Loading

0 comments on commit fadf6f7

Please sign in to comment.