Skip to content

Commit 07a91d8

Browse files
feat(sankey): add @visx/sankey (#1880)
* initialize package * add `Sankey` component * add exports * add default color * add docs page * fix package * fix import and tsconfig * extract to types * add example * change version to 1.0.0 * add to visx-visx * pin version * add controls to sankey example * add sankey tile * add styles to example * add example tile to docs page * add usage to readme * remove console.log * add class names * add tests * fix readme * add tooltip to example * update references * pin d3-shape version d3-sankey dep * revert automatically applied demo tsconfig changes * remove newline * build sizes * remove console.log * format Co-authored-by: Chris Williams <[email protected]> --------- Co-authored-by: Chris Williams <[email protected]>
1 parent d920798 commit 07a91d8

26 files changed

+856
-5
lines changed

packages/sizes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"visx-annotation":{"esm":31002,"lib":43431},"visx-axis":{"esm":21783,"lib":26434},"visx-bounds":{"esm":2948,"lib":3371},"visx-brush":{"esm":56194,"lib":60810},"visx-chord":{"esm":3478,"lib":4691},"visx-clip-path":{"esm":4524,"lib":6062},"visx-curve":{"esm":323,"lib":1462},"visx-delaunay":{"esm":2599,"lib":3428},"visx-demo":{"esm":0,"lib":36731},"visx-drag":{"esm":12756,"lib":14402},"visx-event":{"esm":3878,"lib":5194},"visx-geo":{"esm":13662,"lib":16903},"visx-glyph":{"esm":15177,"lib":19992},"visx-gradient":{"esm":18202,"lib":22847},"visx-grid":{"esm":18982,"lib":22665},"visx-group":{"esm":1648,"lib":2267},"visx-heatmap":{"esm":7394,"lib":8731},"visx-hierarchy":{"esm":12093,"lib":17910},"visx-legend":{"esm":26975,"lib":34055},"visx-marker":{"esm":9152,"lib":11350},"visx-mock-data":{"esm":326040,"lib":329480},"visx-network":{"esm":4674,"lib":6809},"visx-pattern":{"esm":11689,"lib":15763},"visx-point":{"esm":1003,"lib":1818},"visx-react-spring":{"esm":14040,"lib":17765},"visx-responsive":{"esm":16350,"lib":18791},"visx-scale":{"esm":18870,"lib":30555},"visx-shape":{"esm":86912,"lib":108820},"visx-stats":{"esm":13738,"lib":15320},"visx-text":{"esm":8567,"lib":10114},"visx-threshold":{"esm":2907,"lib":3806},"visx-tooltip":{"esm":15233,"lib":21734},"visx-vendor":{"esm":2492,"lib":2702},"visx-visx":{"esm":1524,"lib":4487},"visx-voronoi":{"esm":2314,"lib":3021},"visx-wordcloud":{"esm":2620,"lib":3455},"visx-xychart":{"esm":178473,"lib":240315},"visx-zoom":{"esm":16239,"lib":19297}}
1+
{"visx-annotation":{"esm":31002,"lib":43431},"visx-axis":{"esm":21783,"lib":26434},"visx-bounds":{"esm":2948,"lib":3371},"visx-brush":{"esm":56194,"lib":60810},"visx-chord":{"esm":3478,"lib":4691},"visx-clip-path":{"esm":4524,"lib":6062},"visx-curve":{"esm":323,"lib":1462},"visx-delaunay":{"esm":2599,"lib":3428},"visx-demo":{"esm":0,"lib":36869},"visx-drag":{"esm":12756,"lib":14402},"visx-event":{"esm":3878,"lib":5194},"visx-geo":{"esm":13662,"lib":16903},"visx-glyph":{"esm":15177,"lib":19992},"visx-gradient":{"esm":18202,"lib":22847},"visx-grid":{"esm":18982,"lib":22665},"visx-group":{"esm":1648,"lib":2267},"visx-heatmap":{"esm":7394,"lib":8731},"visx-hierarchy":{"esm":12093,"lib":17910},"visx-legend":{"esm":26975,"lib":34055},"visx-marker":{"esm":9152,"lib":11350},"visx-mock-data":{"esm":326040,"lib":329480},"visx-network":{"esm":4674,"lib":6809},"visx-pattern":{"esm":11689,"lib":15763},"visx-point":{"esm":1003,"lib":1818},"visx-react-spring":{"esm":14040,"lib":17765},"visx-responsive":{"esm":16350,"lib":18791},"visx-sankey":{"esm":3606,"lib":4677},"visx-scale":{"esm":18870,"lib":30555},"visx-shape":{"esm":86912,"lib":108820},"visx-stats":{"esm":13738,"lib":15320},"visx-text":{"esm":8567,"lib":10114},"visx-threshold":{"esm":2907,"lib":3806},"visx-tooltip":{"esm":15233,"lib":21734},"visx-vendor":{"esm":2492,"lib":2702},"visx-visx":{"esm":1524,"lib":4487},"visx-voronoi":{"esm":2314,"lib":3021},"visx-wordcloud":{"esm":2620,"lib":3455},"visx-xychart":{"esm":178473,"lib":240315},"visx-zoom":{"esm":16239,"lib":19297}}

packages/visx-demo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@visx/point": "3.3.0",
5959
"@visx/react-spring": "3.10.1",
6060
"@visx/responsive": "3.10.2",
61+
"@visx/sankey": "1.0.0",
6162
"@visx/scale": "3.5.0",
6263
"@visx/shape": "3.5.0",
6364
"@visx/stats": "3.5.0",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import Sankey, { SankeyDemoProps, background, color } from '../../sandboxes/visx-sankey/Example';
3+
import GalleryTile from '../GalleryTile';
4+
5+
export { default as packageJson } from '../../sandboxes/visx-sankey/package.json';
6+
7+
const tileStyles = { background };
8+
const detailsStyles = { color };
9+
const exampleProps = { showControls: false };
10+
11+
export default function SankeyTile() {
12+
return (
13+
<GalleryTile<SankeyDemoProps>
14+
title="Sankey"
15+
description="<Sankey.Sankey />"
16+
exampleProps={exampleProps}
17+
exampleRenderer={Sankey}
18+
exampleUrl="/sankey"
19+
tileStyles={tileStyles}
20+
detailsStyles={detailsStyles}
21+
/>
22+
);
23+
}

packages/visx-demo/src/components/Gallery/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import * as PolygonsTile from './PolygonsTile';
3838
import * as RadarTile from './RadarTile';
3939
import * as RadialBarsTile from './RadialBarsTile';
4040
import * as ResponsiveTile from './ResponsiveTile';
41+
import * as SankeyTile from './SankeyTile';
4142
import * as SplitLinePathTile from './SplitLinePathTile';
4243
import * as StackedAreasTile from './StackedAreasTile';
4344
import * as StatsPlotTile from './StatsPlotTile';
@@ -98,6 +99,7 @@ export const tiles = [
9899
RadarTile,
99100
RadialBarsTile,
100101
ResponsiveTile,
102+
SankeyTile,
101103
SplitLinePathTile,
102104
StatsPlotTile,
103105
TextTile,

packages/visx-demo/src/components/PackageList.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ export default function PackageList({
120120
</Link>
121121
{!compact && <p>Animated visx primitives</p>}
122122
</li>
123+
<li className={cx(emphasizePackage === 'sankey' && 'emphasize')}>
124+
<Link href="/docs/sankey">
125+
<a>sankey</a>
126+
</Link>
127+
{!compact && <p>Components to visualize sankey charts</p>}
128+
</li>
123129
<li className={cx(emphasizePackage === 'stats' && 'emphasize')}>
124130
<Link href="/docs/stats">
125131
<a>stats</a>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
import SankeyReadme from '!!raw-loader!../../../../visx-sankey/Readme.md';
3+
import Sankey from '../../../../visx-sankey/src/Sankey';
4+
import DocPage from '../../components/DocPage';
5+
import SankeyTile from '../../components/Gallery/SankeyTile';
6+
7+
const components = [Sankey];
8+
9+
const examples = [SankeyTile];
10+
11+
function SankeyDocs() {
12+
return (
13+
<DocPage
14+
components={components}
15+
examples={examples}
16+
readme={SankeyReadme}
17+
visxPackage="sankey"
18+
/>
19+
);
20+
}
21+
export default SankeyDocs;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import Sankey from '../sandboxes/visx-sankey/Example';
3+
import packageJson from '../sandboxes/visx-sankey/package.json';
4+
import Show from '../components/Show';
5+
import TreemapSource from '!!raw-loader!../sandboxes/visx-sankey/Example';
6+
7+
function SankeyPage() {
8+
return (
9+
<Show
10+
component={Sankey}
11+
title="Sankey"
12+
codeSandboxDirectoryName="visx-sankey"
13+
packageJson={packageJson}
14+
>
15+
{TreemapSource}
16+
</Show>
17+
);
18+
}
19+
export default SankeyPage;

packages/visx-demo/src/sandboxes/exampleToVisxDependencyLookup.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import radarPackageJson from './visx-radar/package.json';
3030
import responsivePackageJson from './visx-responsive/package.json';
3131
import lineRadialPackageJson from './visx-shape-line-radial/package.json';
3232
import piePackageJson from './visx-shape-pie/package.json';
33+
import sankeyPackageJson from './visx-sankey/package.json';
3334
import splitLinePathPackageJson from './visx-shape-splitlinepath/package.json';
3435
import stackedAreasPackageJson from './visx-stacked-areas/package.json';
3536
import statsPackageJson from './visx-stats/package.json';
@@ -78,6 +79,7 @@ const examples = [
7879
polygonsPackageJson,
7980
radarPackageJson,
8081
responsivePackageJson,
82+
sankeyPackageJson,
8183
splitLinePathPackageJson,
8284
stackedAreasPackageJson,
8385
statsPackageJson,
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import React, { useState } from 'react';
2+
import {
3+
Sankey,
4+
sankeyCenter,
5+
sankeyRight,
6+
sankeyLeft,
7+
sankeyJustify,
8+
SankeyNode,
9+
} from '@visx/sankey';
10+
import { Group } from '@visx/group';
11+
import { BarRounded, LinkHorizontal } from '@visx/shape';
12+
import { useTooltip, TooltipWithBounds } from '@visx/tooltip';
13+
import { localPoint } from '@visx/event';
14+
15+
import energy from './energy.json';
16+
17+
export const background = '#84dccf';
18+
export const color = '#392f5a';
19+
20+
type NodeDatum = { name: string };
21+
type LinkDatum = {};
22+
23+
const nodeAlignments = {
24+
sankeyCenter,
25+
sankeyJustify,
26+
sankeyLeft,
27+
sankeyRight,
28+
} as const;
29+
30+
const defaultMargin = { top: 10, left: 10, right: 10, bottom: 10 };
31+
32+
export type SankeyDemoProps = {
33+
width: number;
34+
height: number;
35+
showControls?: boolean;
36+
margin?: { top: number; right: number; bottom: number; left: number };
37+
};
38+
39+
export default function SankeyDemo({
40+
width,
41+
height,
42+
showControls = true,
43+
margin = defaultMargin,
44+
}: SankeyDemoProps) {
45+
const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, showTooltip, hideTooltip } =
46+
useTooltip();
47+
const xMax = width - margin.left - margin.right;
48+
const yMax = height - margin.top - margin.bottom;
49+
50+
const [nodeAlignment, setTileMethod] = useState<keyof typeof nodeAlignments>('sankeyCenter');
51+
const [nodePadding, setNodePadding] = useState(10);
52+
const [nodeWidth, setNodeWidth] = useState(10);
53+
54+
if (width < 10) return null;
55+
56+
return (
57+
<div>
58+
<style>{`
59+
.visx-sankey-link:hover {
60+
stroke-opacity: 0.7;
61+
}
62+
.visx-sankey-node:hover {
63+
filter: brightness(1.3);
64+
}
65+
.visx-sankey-demo-container {
66+
background: ${background};
67+
padding: ${margin.top}px ${margin.right}px ${margin.bottom}px ${margin.left}px;
68+
border-radius: 5px;
69+
position: relative;
70+
}
71+
.visx-sankey-demo-controls {
72+
font-size: 12px;
73+
}
74+
`}</style>
75+
{showControls && (
76+
<div className="visx-sankey-demo-controls">
77+
<label>
78+
node alignment{' '}
79+
<select
80+
onClick={(e) => e.stopPropagation()}
81+
onChange={(e) => setTileMethod(e.target.value as keyof typeof nodeAlignments)}
82+
value={nodeAlignment}
83+
>
84+
{Object.keys(nodeAlignments).map((alignment) => (
85+
<option key={alignment} value={alignment}>
86+
{alignment}
87+
</option>
88+
))}
89+
</select>
90+
</label>{' '}
91+
<label>
92+
node padding{' '}
93+
<input
94+
type="number"
95+
value={nodePadding}
96+
onChange={(e) => setNodePadding(Number(e.target.value))}
97+
/>
98+
</label>{' '}
99+
<label>
100+
node width{' '}
101+
<input
102+
type="number"
103+
value={nodeWidth}
104+
onChange={(e) => setNodeWidth(Number(e.target.value))}
105+
/>
106+
</label>
107+
</div>
108+
)}
109+
<div className="visx-sankey-demo-container">
110+
<svg width={xMax} height={yMax}>
111+
<Sankey<NodeDatum, LinkDatum>
112+
root={energy}
113+
nodeWidth={nodeWidth}
114+
size={[xMax, yMax]}
115+
nodePadding={nodePadding}
116+
nodeAlign={nodeAlignments[nodeAlignment]}
117+
>
118+
{({ graph, createPath }) => (
119+
<>
120+
<Group>
121+
{graph.links.map((link, i) => (
122+
<LinkHorizontal
123+
key={i}
124+
className="visx-sankey-link"
125+
data={link}
126+
path={createPath}
127+
fill="transparent"
128+
stroke={color}
129+
strokeWidth={link.width}
130+
strokeOpacity={0.5}
131+
onPointerMove={(event) => {
132+
const coords = localPoint(
133+
(event.target as SVGElement).ownerSVGElement,
134+
event,
135+
);
136+
showTooltip({
137+
tooltipData: `${
138+
(link.source as SankeyNode<NodeDatum, LinkDatum>).name
139+
} > ${(link.target as SankeyNode<NodeDatum, LinkDatum>).name} = ${
140+
link.value
141+
}`,
142+
tooltipTop: (coords?.y ?? 0) + 10,
143+
tooltipLeft: (coords?.x ?? 0) + 10,
144+
});
145+
}}
146+
onMouseOut={hideTooltip}
147+
/>
148+
))}
149+
</Group>
150+
<Group>
151+
{graph.nodes.map(({ y0, y1, x0, x1, name }, i) => (
152+
<BarRounded
153+
key={i}
154+
className="visx-sankey-node"
155+
width={x1 - x0}
156+
height={y1 - y0}
157+
x={x0}
158+
y={y0}
159+
radius={3}
160+
all
161+
fill={color}
162+
onPointerMove={(event) => {
163+
const coords = localPoint(
164+
(event.target as SVGElement).ownerSVGElement,
165+
event,
166+
);
167+
showTooltip({
168+
tooltipData: name,
169+
tooltipTop: (coords?.y ?? 0) + 10,
170+
tooltipLeft: (coords?.x ?? 0) + 10,
171+
});
172+
}}
173+
onMouseOut={hideTooltip}
174+
/>
175+
))}
176+
</Group>
177+
</>
178+
)}
179+
</Sankey>
180+
</svg>
181+
{tooltipOpen && (
182+
<TooltipWithBounds key={Math.random()} top={tooltipTop} left={tooltipLeft}>
183+
{tooltipData}
184+
</TooltipWithBounds>
185+
)}
186+
</div>
187+
</div>
188+
);
189+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"nodes":[{"name":"Agricultural 'waste'"},{"name":"Bio-conversion"},{"name":"Liquid"},{"name":"Losses"},{"name":"Solid"},{"name":"Gas"},{"name":"Biofuel imports"},{"name":"Biomass imports"},{"name":"Coal imports"},{"name":"Coal"},{"name":"Coal reserves"},{"name":"District heating"},{"name":"Industry"},{"name":"Heating and cooling - commercial"},{"name":"Heating and cooling - homes"},{"name":"Electricity grid"},{"name":"Over generation / exports"},{"name":"H2 conversion"},{"name":"Road transport"},{"name":"Agriculture"},{"name":"Rail transport"},{"name":"Lighting & appliances - commercial"},{"name":"Lighting & appliances - homes"},{"name":"Gas imports"},{"name":"Ngas"},{"name":"Gas reserves"},{"name":"Thermal generation"},{"name":"Geothermal"},{"name":"H2"},{"name":"Hydro"},{"name":"International shipping"},{"name":"Domestic aviation"},{"name":"International aviation"},{"name":"National navigation"},{"name":"Marine algae"},{"name":"Nuclear"},{"name":"Oil imports"},{"name":"Oil"},{"name":"Oil reserves"},{"name":"Other waste"},{"name":"Pumped heat"},{"name":"Solar PV"},{"name":"Solar Thermal"},{"name":"Solar"},{"name":"Tidal"},{"name":"UK land based bioenergy"},{"name":"Wave"},{"name":"Wind"}],"links":[{"source":0,"target":1,"value":124.729},{"source":1,"target":2,"value":0.597},{"source":1,"target":3,"value":26.862},{"source":1,"target":4,"value":280.322},{"source":1,"target":5,"value":81.144},{"source":6,"target":2,"value":35},{"source":7,"target":4,"value":35},{"source":8,"target":9,"value":11.606},{"source":10,"target":9,"value":63.965},{"source":9,"target":4,"value":75.571},{"source":11,"target":12,"value":10.639},{"source":11,"target":13,"value":22.505},{"source":11,"target":14,"value":46.184},{"source":15,"target":16,"value":104.453},{"source":15,"target":14,"value":113.726},{"source":15,"target":17,"value":27.14},{"source":15,"target":12,"value":342.165},{"source":15,"target":18,"value":37.797},{"source":15,"target":19,"value":4.412},{"source":15,"target":13,"value":40.858},{"source":15,"target":3,"value":56.691},{"source":15,"target":20,"value":7.863},{"source":15,"target":21,"value":90.008},{"source":15,"target":22,"value":93.494},{"source":23,"target":24,"value":40.719},{"source":25,"target":24,"value":82.233},{"source":5,"target":13,"value":0.129},{"source":5,"target":3,"value":1.401},{"source":5,"target":26,"value":151.891},{"source":5,"target":19,"value":2.096},{"source":5,"target":12,"value":48.58},{"source":27,"target":15,"value":7.013},{"source":17,"target":28,"value":20.897},{"source":17,"target":3,"value":6.242},{"source":28,"target":18,"value":20.897},{"source":29,"target":15,"value":6.995},{"source":2,"target":12,"value":121.066},{"source":2,"target":30,"value":128.69},{"source":2,"target":18,"value":135.835},{"source":2,"target":31,"value":14.458},{"source":2,"target":32,"value":206.267},{"source":2,"target":19,"value":3.64},{"source":2,"target":33,"value":33.218},{"source":2,"target":20,"value":4.413},{"source":34,"target":1,"value":4.375},{"source":24,"target":5,"value":122.952},{"source":35,"target":26,"value":839.978},{"source":36,"target":37,"value":504.287},{"source":38,"target":37,"value":107.703},{"source":37,"target":2,"value":611.99},{"source":39,"target":4,"value":56.587},{"source":39,"target":1,"value":77.81},{"source":40,"target":14,"value":193.026},{"source":40,"target":13,"value":70.672},{"source":41,"target":15,"value":59.901},{"source":42,"target":14,"value":19.263},{"source":43,"target":42,"value":19.263},{"source":43,"target":41,"value":59.901},{"source":4,"target":19,"value":0.882},{"source":4,"target":26,"value":400.12},{"source":4,"target":12,"value":46.477},{"source":26,"target":15,"value":525.531},{"source":26,"target":3,"value":787.129},{"source":26,"target":11,"value":79.329},{"source":44,"target":15,"value":9.452},{"source":45,"target":1,"value":182.01},{"source":46,"target":15,"value":19.013},{"source":47,"target":15,"value":289.366}]}

0 commit comments

Comments
 (0)