Skip to content

Commit

Permalink
feat(D3 plugin): bar-x stacking option (#255)
Browse files Browse the repository at this point in the history
  • Loading branch information
kuzmadom authored Aug 30, 2023
1 parent 091bc5a commit 2b9f47d
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 63 deletions.
100 changes: 100 additions & 0 deletions src/plugins/d3/__stories__/bar-x/stacked.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import {Meta, Story} from '@storybook/react';
import {object, withKnobs} from '@storybook/addon-knobs';
import {Button} from '@gravity-ui/uikit';
import {settings} from '../../../../libs';
import {ChartKit} from '../../../../components/ChartKit';
import type {ChartKitRef} from '../../../../types';
import type {ChartKitWidgetData} from '../../../../types/widget-data';
import {D3Plugin} from '../..';

const Template: Story = () => {
const [shown, setShown] = React.useState(false);
const chartkitRef = React.useRef<ChartKitRef>();
const data: ChartKitWidgetData = {
legend: {enabled: true},
tooltip: {enabled: true},
title: {text: 'Category axis'},
xAxis: {
type: 'category',
categories: ['A', 'B', 'C'],
labels: {enabled: true},
},
yAxis: [
{
type: 'linear',
labels: {enabled: true},
min: 0,
},
],
series: {
data: [
{
type: 'bar-x',
visible: true,
stacking: 'normal',
data: [
{
category: 'A',
y: 100,
},
{
category: 'B',
y: 80,
},
{
category: 'C',
y: 120,
},
],
name: 'Sales',
},
{
type: 'bar-x',
visible: true,
stacking: 'normal',
data: [
{
category: 'A',
y: 5,
},
{
category: 'B',
y: 25,
},
{
category: 'C',
y: 0,
},
],
name: 'Discount',
},
],
},
};

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

return (
<div
style={{
height: '80vh',
width: '100%',
}}
>
<ChartKit ref={chartkitRef} type="d3" data={object<ChartKitWidgetData>('data', data)} />
</div>
);
};

export const Stacked = Template.bind({});

const meta: Meta = {
title: 'Plugins/D3/Bar-X',
decorators: [withKnobs],
};

export default meta;
27 changes: 16 additions & 11 deletions src/plugins/d3/renderer/hooks/useSeries/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {scaleOrdinal} from 'd3';
import {group, scaleOrdinal} from 'd3';

import type {ChartKitWidgetData} from '../../../../../types/widget-data';

Expand All @@ -26,16 +26,21 @@ export const useSeries = (args: Args) => {
const seriesNames = getSeriesNames(series);
const colorScale = scaleOrdinal(seriesNames, DEFAULT_PALETTE);

return series.reduce<PreparedSeries[]>((acc, singleSeries) => {
acc.push(
...prepareSeries({
series: singleSeries,
legend,
colorScale,
}),
);
return acc;
}, []);
const groupedSeries = group(series, (item) => item.type);
return Array.from(groupedSeries).reduce<PreparedSeries[]>(
(acc, [seriesType, seriesList]) => {
acc.push(
...prepareSeries({
type: seriesType,
series: seriesList,
legend,
colorScale,
}),
);
return acc;
},
[],
);
}, [series, legend]);
const [activeLegendItems, setActiveLegendItems] = React.useState(
getActiveLegendItems(preparedSeries),
Expand Down
61 changes: 53 additions & 8 deletions src/plugins/d3/renderer/hooks/useSeries/prepareSeries.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type {ScaleOrdinal} from 'd3';
import {scaleOrdinal} from 'd3';
import type {
BarXSeries,
ChartKitWidgetSeries,
PieSeries,
RectLegendSymbolOptions,
} from '../../../../../types/widget-data';
import type {PreparedLegend} from '../useChartOptions/types';
import cloneDeep from 'lodash/cloneDeep';
import type {PreparedLegendSymbol, PreparedPieSeries, PreparedSeries} from './types';
import type {
PreparedBarXSeries,
PreparedLegendSymbol,
PreparedPieSeries,
PreparedSeries,
} from './types';
import get from 'lodash/get';
import {DEFAULT_PALETTE} from '../../constants';
import {DEFAULT_LEGEND_SYMBOL_SIZE} from './constants';
Expand Down Expand Up @@ -52,6 +58,36 @@ function prepareAxisRelatedSeries(args: PrepareAxisRelatedSeriesArgs): PreparedS
return [preparedSeries];
}

type PrepareBarXSeriesArgs = {
colorScale: ScaleOrdinal<string, string>;
series: BarXSeries[];
legend: PreparedLegend;
};

function prepareBarXSeries(args: PrepareBarXSeriesArgs): PreparedSeries[] {
const {colorScale, series, legend} = args;
const commonStackId = getRandomCKId();

return series.map<PreparedBarXSeries>((singleSeries) => {
const name = singleSeries.name || '';
const color = singleSeries.color || colorScale(name);

return {
type: singleSeries.type,
color: color,
name: name,
visible: get(singleSeries, 'visible', true),
legend: {
enabled: get(singleSeries, 'legend.enabled', legend.enabled),
symbol: prepareLegendSymbol(singleSeries),
},
data: singleSeries.data,
stacking: singleSeries.stacking,
stackId: singleSeries.stacking === 'normal' ? commonStackId : getRandomCKId(),
};
}, []);
}

type PreparePieSeriesArgs = {
series: PieSeries;
legend: PreparedLegend;
Expand Down Expand Up @@ -94,19 +130,28 @@ function preparePieSeries(args: PreparePieSeriesArgs) {
}

export function prepareSeries(args: {
series: ChartKitWidgetSeries;
type: ChartKitWidgetSeries['type'];
series: ChartKitWidgetSeries[];
legend: PreparedLegend;
colorScale: ScaleOrdinal<string, string>;
}) {
const {series, legend, colorScale} = args;
}): PreparedSeries[] {
const {type, series, legend, colorScale} = args;

switch (series.type) {
switch (type) {
case 'pie': {
return preparePieSeries({series, legend});
return series.reduce<PreparedSeries[]>((acc, singleSeries) => {
acc.push(...preparePieSeries({series: singleSeries as PieSeries, legend}));
return acc;
}, []);
}
case 'scatter':
case 'bar-x': {
return prepareAxisRelatedSeries({series, legend, colorScale});
return prepareBarXSeries({series: series as BarXSeries[], legend, colorScale});
}
case 'scatter': {
return series.reduce<PreparedSeries[]>((acc, singleSeries) => {
acc.push(...prepareAxisRelatedSeries({series: singleSeries, legend, colorScale}));
return acc;
}, []);
}
default: {
const seriesType = get(series, 'type');
Expand Down
1 change: 1 addition & 0 deletions src/plugins/d3/renderer/hooks/useSeries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type PreparedScatterSeries = {
export type PreparedBarXSeries = {
type: BarXSeries['type'];
data: BarXSeriesData[];
stackId: string;
} & BasePreparedSeries;

export type PreparedPieSeries = BasePreparedSeries &
Expand Down
92 changes: 55 additions & 37 deletions src/plugins/d3/renderer/hooks/useShapes/bar-x.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import React from 'react';
import {ChartOptions} from '../useChartOptions/types';
import {ChartScale} from '../useAxisScales';
import {OnSeriesMouseLeave, OnSeriesMouseMove} from '../useTooltip/types';
import {BarXSeries, BarXSeriesData} from '../../../../../types/widget-data';
import {BarXSeriesData} from '../../../../../types/widget-data';
import {block} from '../../../../../utils/cn';
import {pointer, ScaleBand, ScaleLinear, ScaleTime} from 'd3';
import {getRandomCKId} from '../../../../../utils';
import {group, pointer, ScaleBand, ScaleLinear, ScaleTime} from 'd3';
import {PreparedBarXSeries} from '../useSeries/types';

const DEFAULT_BAR_RECT_WIDTH = 50;
const DEFAULT_LINEAR_BAR_RECT_WIDTH = 20;
Expand All @@ -16,7 +16,7 @@ const b = block('d3-bar');
type Args = {
top: number;
left: number;
series: BarXSeries[];
series: PreparedBarXSeries[];
xAxis: ChartOptions['xAxis'];
xScale: ChartScale;
yAxis: ChartOptions['yAxis'];
Expand Down Expand Up @@ -95,43 +95,61 @@ export function prepareBarXSeries(args: Args) {
onSeriesMouseLeave,
svgContainer,
} = args;

const stackedSeriesMap = group(series, (item) => item.stackId);

const seriesData = series.map(({data}) => data).flat(2);
const minPointDistance = minDiff(seriesData.map((item) => Number(item.x)));

return series.reduce<React.ReactElement[]>((result, item) => {
const randomKey = getRandomCKId();

item.data.forEach((point, i) => {
const rectProps = getRectProperties({
point,
xAxis,
xScale,
yAxis,
yScale,
minPointDistance,
const result: React.ReactElement[] = [];

Array.from(stackedSeriesMap).forEach(([stackId, stackedSeries]) => {
const stackHeights: Record<string, number> = {};
stackedSeries.forEach((item, seriesIndex) => {
item.data.forEach((point, i) => {
const rectProps = getRectProperties({
point,
xAxis,
xScale,
yAxis,
yScale,
minPointDistance,
});

if (!stackHeights[rectProps.x]) {
stackHeights[rectProps.x] = 0;
}

const rectY = rectProps.y - stackHeights[rectProps.x];
stackHeights[rectProps.x] += rectProps.height + 1;

if (!rectProps.height) {
return;
}

result.push(
<rect
key={`${i}-${seriesIndex}-${stackId}`}
className={b('rect')}
fill={item.color}
{...rectProps}
y={rectY}
onMouseMove={function (e) {
const [x, y] = pointer(e, svgContainer);
onSeriesMouseMove?.({
hovered: {
data: point,
series: item,
},
pointerPosition: [x - left, y - top],
});
}}
onMouseLeave={onSeriesMouseLeave}
/>,
);
});

result.push(
<rect
key={`${i}-${randomKey}`}
className={b('rect')}
fill={item.color}
{...rectProps}
onMouseMove={function (e) {
const [x, y] = pointer(e, svgContainer);
onSeriesMouseMove?.({
hovered: {
data: point,
series: item,
},
pointerPosition: [x - left, y - top],
});
}}
onMouseLeave={onSeriesMouseLeave}
/>,
);
});
});

return result;
}, []);
return result;
}
6 changes: 3 additions & 3 deletions src/plugins/d3/renderer/hooks/useShapes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import {group} from 'd3';

import type {BarXSeries, ScatterSeries} from '../../../../../types/widget-data';
import type {ScatterSeries} from '../../../../../types/widget-data';

import {getOnlyVisibleSeries} from '../../utils';
import type {ChartOptions} from '../useChartOptions/types';
import type {ChartScale} from '../useAxisScales';
import type {PreparedPieSeries, PreparedSeries} from '../';
import type {PreparedBarXSeries, PreparedPieSeries, PreparedSeries} from '../';
import type {OnSeriesMouseMove, OnSeriesMouseLeave} from '../useTooltip/types';
import {prepareBarXSeries} from './bar-x';
import {prepareScatterSeries} from './scatter';
Expand Down Expand Up @@ -58,7 +58,7 @@ export const useShapes = (args: Args) => {
...prepareBarXSeries({
top,
left,
series: chartSeries as BarXSeries[],
series: chartSeries as PreparedBarXSeries[],
xAxis,
xScale,
yAxis,
Expand Down
Loading

0 comments on commit 2b9f47d

Please sign in to comment.