Skip to content

Commit 8791f30

Browse files
authored
feat: add sankey series type (#46)
1 parent 6aea3bc commit 8791f30

File tree

24 files changed

+627
-16
lines changed

24 files changed

+627
-16
lines changed

package-lock.json

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@gravity-ui/date-utils": "^2.5.4",
6161
"@gravity-ui/i18n": "^1.6.0",
6262
"d3": "^7.9.0",
63+
"d3-sankey": "^0.12.3",
6364
"lodash": "^4.17.21",
6465
"tslib": "^2.6.2"
6566
},
@@ -93,6 +94,7 @@
9394
"@testing-library/react": "^16.0.0",
9495
"@testing-library/user-event": "^14.5.2",
9596
"@types/d3": "^7.4.3",
97+
"@types/d3-sankey": "^0.12.4",
9698
"@types/d3-selection": "^3.0.11",
9799
"@types/jest": "^29.5.12",
98100
"@types/lodash": "^4.17.12",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type {Meta, StoryObj} from '@storybook/react';
2+
3+
import {ChartStory} from '../ChartStory';
4+
import {sankeyPlaygroundData} from '../__data__';
5+
6+
const meta: Meta<typeof ChartStory> = {
7+
title: 'Sankey',
8+
component: ChartStory,
9+
};
10+
11+
export default meta;
12+
13+
type Story = StoryObj<typeof ChartStory>;
14+
15+
export const SankeyPlayground = {
16+
name: 'Playground',
17+
args: {
18+
data: sankeyPlaygroundData,
19+
wrapperProps: {
20+
styles: {
21+
height: 560,
22+
},
23+
},
24+
},
25+
argTypes: {
26+
data: {
27+
control: 'object',
28+
},
29+
},
30+
} satisfies Story;

src/__stories__/Showcase.stories.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import {
2929
lineTwoYAxisData,
3030
pieBasicData,
3131
pieDonutData,
32+
sankeyPlaygroundData,
3233
scatterBasicData,
3334
scatterTwoYAxisData,
35+
treemapPlaygroundData,
3436
} from './__data__';
3537

3638
const ShowcaseStory = () => {
@@ -171,15 +173,19 @@ const ShowcaseStory = () => {
171173
<ChartStory data={scatterTwoYAxisData} />
172174
</Col>
173175
</Row>
174-
{/* <Row space={1}>
175-
<Text variant="header-2">Combined charts</Text>
176+
<Row space={1}>
177+
<Text variant="header-2">Other</Text>
176178
</Row>
177-
<Row space={3} style={{minHeight: 280}}>
178-
<Col s={12}>
179-
<Text variant="subheader-1">Line + Bar-X</Text>
180-
<LineAndBarXCombinedChart />
179+
<Row space={3}>
180+
<Col s={12} m={6} l={6}>
181+
<Text variant="subheader-1">Treemap</Text>
182+
<ChartStory data={treemapPlaygroundData} />
181183
</Col>
182-
</Row> */}
184+
<Col s={12} m={6} l={6}>
185+
<Text variant="subheader-1">Sankey</Text>
186+
<ChartStory data={sankeyPlaygroundData} />
187+
</Col>
188+
</Row>
183189
</Container>
184190
</div>
185191
);

src/__stories__/__data__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './pie';
77
export * from './scatter';
88
export * from './treemap';
99
export * from './waterfall';
10+
export * from './sankey';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './playground';
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {groups, sort} from 'd3';
2+
3+
import type {ChartData} from '../../../types';
4+
import nintendoGames from '../nintendoGames';
5+
6+
function prepareData(): ChartData {
7+
const gamesWithScore = nintendoGames.filter((game) => Math.round(game.user_score ?? 0));
8+
const gamesByPlatform = groups(gamesWithScore, (item) => item.platform);
9+
const data = gamesByPlatform.map(([platform, games]) => {
10+
const links = sort(
11+
groups(games, (game) => Math.round(game.user_score ?? 0)).map(([score, items]) => ({
12+
name: String(score),
13+
value: items.length,
14+
})),
15+
(d) => d.name,
16+
);
17+
18+
return {
19+
name: platform,
20+
links,
21+
};
22+
});
23+
24+
const scores = sort(
25+
Array.from(new Set(gamesWithScore.map((game) => Math.round(game.user_score ?? 0)))),
26+
(d) => d,
27+
).map((d) => ({name: String(d), links: []}));
28+
29+
return {
30+
title: {
31+
text: 'Average user score by platform',
32+
},
33+
tooltip: {enabled: true},
34+
series: {
35+
data: [
36+
{
37+
type: 'sankey',
38+
data: [...data, ...scores],
39+
name: 'Series 1',
40+
},
41+
],
42+
},
43+
};
44+
}
45+
46+
export const sankeyPlaygroundData = prepareData();

src/components/ChartInner/useChartInnerHandlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export function useChartInnerHandlers(props: Props) {
7373
const closest = getClosestPoints({
7474
position: [x, y],
7575
shapesData,
76+
boundsHeight,
77+
boundsWidth,
7678
});
7779
dispatcher.call(EventType.HOVER_SHAPE, event.target, closest, [pointerX, pointerY]);
7880
dispatcher.call(
@@ -128,6 +130,8 @@ export function useChartInnerHandlers(props: Props) {
128130
const items = getClosestPoints({
129131
position: [x, y],
130132
shapesData,
133+
boundsHeight,
134+
boundsWidth,
131135
});
132136
const selected = items?.find((item) => item.closest);
133137
if (!selected) {

src/components/Tooltip/DefaultContent.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import type {
1010
ChartXAxis,
1111
ChartYAxis,
1212
TooltipDataChunk,
13+
TooltipDataChunkSankey,
1314
TreemapSeriesData,
1415
WaterfallSeriesData,
1516
} from '../../types';
1617
import {block, getDataCategoryValue, getWaterfallPointSubtotal} from '../../utils';
1718

18-
const b = block('d3-tooltip');
19+
const b = block('tooltip');
1920

2021
type Props = {
2122
hovered: TooltipDataChunk[];
@@ -55,7 +56,9 @@ const getXRowData = (data: ChartSeriesData, xAxis?: ChartXAxis) => getRowData('x
5556
const getYRowData = (data: ChartSeriesData, yAxis?: ChartYAxis) => getRowData('y', data, yAxis);
5657

5758
const getMeasureValue = (data: TooltipDataChunk[], xAxis?: ChartXAxis, yAxis?: ChartYAxis) => {
58-
if (data.every((item) => ['pie', 'treemap', 'waterfall'].includes(item.series.type))) {
59+
if (
60+
data.every((item) => ['pie', 'treemap', 'waterfall', 'sankey'].includes(item.series.type))
61+
) {
5962
return null;
6063
}
6164

@@ -72,7 +75,8 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => {
7275
return (
7376
<React.Fragment>
7477
{measureValue && <div>{measureValue}</div>}
75-
{hovered.map(({data, series, closest}, i) => {
78+
{hovered.map((seriesItem, i) => {
79+
const {data, series, closest} = seriesItem;
7680
const id = `${get(series, 'id')}_${i}`;
7781
const color = get(series, 'color');
7882

@@ -144,6 +148,22 @@ export const DefaultContent = ({hovered, xAxis, yAxis}: Props) => {
144148
</div>
145149
);
146150
}
151+
case 'sankey': {
152+
const {target, data: source} = seriesItem as TooltipDataChunkSankey;
153+
const value = source.links.find((d) => d.name === target?.name)?.value;
154+
155+
return (
156+
<div key={id} className={b('content-row')}>
157+
<div
158+
className={b('color')}
159+
style={{backgroundColor: source.color}}
160+
/>
161+
<div style={{display: 'flex', gap: 8, verticalAlign: 'center'}}>
162+
{source.name} <span></span> {target?.name}: {value}
163+
</div>
164+
</div>
165+
);
166+
}
147167
default: {
148168
return null;
149169
}

src/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const SeriesType = {
1010
Scatter: 'scatter',
1111
Treemap: 'treemap',
1212
Waterfall: 'waterfall',
13+
Sankey: 'sankey',
1314
} as const;
1415

1516
export enum DashStyle {

0 commit comments

Comments
 (0)