Skip to content

Commit 0460e92

Browse files
committed
Bar chart race WIP
1 parent 9bfb7d0 commit 0460e92

File tree

4 files changed

+2222
-0
lines changed

4 files changed

+2222
-0
lines changed

packages/layerchart/src/routes/_NavMenu.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'AreaStack',
1212
{ label: 'Bar Chart (Vertical)', value: 'Columns' },
1313
{ label: 'Bar Chart (Horizontal)', value: 'Bars' },
14+
{ label: 'Bar Chart Race', value: 'BarChartRace' },
1415
'Candlestick',
1516
'DotPlot',
1617
'Histogram',
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<script lang="ts">
2+
import { scaleBand, scaleOrdinal } from 'd3-scale';
3+
import { rollup } from 'd3-array';
4+
import { quantize } from 'd3-interpolate';
5+
import {
6+
interpolateInferno,
7+
interpolateRainbow,
8+
interpolateRdBu,
9+
interpolateSpectral,
10+
interpolateViridis,
11+
schemeSpectral,
12+
schemeTableau10,
13+
} from 'd3-scale-chromatic';
14+
15+
import {
16+
PeriodType,
17+
NumberStepper,
18+
sort,
19+
format,
20+
timerStore,
21+
ButtonGroup,
22+
Button,
23+
Field,
24+
Switch,
25+
} from 'svelte-ux';
26+
27+
import Chart, { Svg } from '$lib/components/Chart.svelte';
28+
import Axis from '$lib/components/Axis.svelte';
29+
import Bar from '$lib/components/Bar.svelte';
30+
import Group from '$lib/components/Group.svelte';
31+
import Highlight from '$lib/components/Highlight.svelte';
32+
import Preview from '$lib/docs/Preview.svelte';
33+
import Text from '$lib/components/Text.svelte';
34+
import Tooltip from '$lib/components/Tooltip.svelte';
35+
import TooltipItem from '$lib/components/TooltipItem.svelte';
36+
37+
import Labels from '$lib/components/Labels.svelte';
38+
import Rule from '$lib/components/Rule.svelte';
39+
import ChartClipPath from '$lib/components/ChartClipPath.svelte';
40+
41+
export let data;
42+
43+
const duration = 250;
44+
let xNice = false;
45+
46+
const frameTimer = timerStore({
47+
initial: 0,
48+
onTick: (value) => {
49+
if (value == null || value >= data.keyframes.length - 1) {
50+
frameTimer.stop();
51+
return value;
52+
} else {
53+
return value + 1;
54+
}
55+
},
56+
delay: duration,
57+
disabled: true,
58+
});
59+
$: ({ isRunning } = frameTimer);
60+
61+
$: keyframe = data.keyframes[$frameTimer];
62+
$: chartData = sort(keyframe?.data ?? [], (d) => d.value, 'desc');
63+
64+
const categoryByName = rollup(
65+
data.data,
66+
(values) => values[0].category,
67+
(d) => d.name
68+
);
69+
// const colors = schemeTableau10;
70+
const colors = schemeSpectral[10];
71+
// const colors = quantize(interpolateSpectral, 10);
72+
const colorScale = scaleOrdinal()
73+
.domain(Array.from(categoryByName.values()).sort())
74+
.range(colors);
75+
76+
$: console.log({ data, keyframe, chartData });
77+
</script>
78+
79+
<h1>Examples</h1>
80+
81+
<div class="grid grid-cols-[1fr,auto,auto] items-center mb-2">
82+
<div class="flex items-center gap-3">
83+
<ButtonGroup variant="fill-light" class="ml-3">
84+
<Button on:click={frameTimer.start} disabled={$isRunning}>Start</Button>
85+
<Button on:click={frameTimer.stop} disabled={!$isRunning}>Stop</Button>
86+
</ButtonGroup>
87+
<Button on:click={frameTimer.reset}>Reset</Button>
88+
89+
<NumberStepper
90+
value={$frameTimer}
91+
on:change={(e) => {
92+
$frameTimer = e.detail.value;
93+
}}
94+
/>
95+
96+
<Field let:id>
97+
<label class="flex gap-2 items-center text-sm">
98+
Nice
99+
<Switch bind:checked={xNice} {id} />
100+
</label>
101+
</Field>
102+
</div>
103+
104+
<div class="text-xl">{format(keyframe?.date, PeriodType.MonthYear)}</div>
105+
</div>
106+
107+
<Preview data={chartData}>
108+
<div class="h-[500px] p-4 border rounded">
109+
<Chart
110+
data={chartData}
111+
x="value"
112+
xDomain={[0, null]}
113+
{xNice}
114+
y="name"
115+
yScale={scaleBand().padding(0.1)}
116+
yDomain={chartData.map((d) => d.name)}
117+
padding={{ top: 14, left: 4, right: 24 }}
118+
tooltip={{ mode: 'band' }}
119+
>
120+
<Svg>
121+
<ChartClipPath _height={272}>
122+
<g>
123+
{#each chartData as d (d.name)}
124+
<Bar
125+
bar={d}
126+
radius={2}
127+
fill={colorScale(categoryByName.get(d.name))}
128+
fill-opacity={0.9}
129+
class="stroke-1 stroke-surface-content/50"
130+
tweened={{ duration }}
131+
/>
132+
{/each}
133+
</g>
134+
<!-- <Axis placement="left" rule tweened={{ duration }} /> -->
135+
<Rule x />
136+
<Axis placement="top" grid rule tweened={{ duration }} />
137+
<Highlight area />
138+
<!-- <Labels tweened format="integer" placement="inside" /> -->
139+
<Labels tweened format="integer" placement="inside" let:data let:position let:textProps>
140+
<Group {...position} tweened={{ duration }}>
141+
<Text value={data.name} class="fill-black/50 mix-blend-multiply" {...textProps} />
142+
</Group>
143+
</Labels>
144+
</ChartClipPath>
145+
</Svg>
146+
147+
<Tooltip header={(d) => d.name} let:data>
148+
<TooltipItem label="value" value={data.value} format="integer" />
149+
<TooltipItem label="category" value={categoryByName.get(data.name)} />
150+
</Tooltip>
151+
</Chart>
152+
</div>
153+
</Preview>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { csvParse, autoType } from 'd3-dsv';
2+
import { group, pairs, rollup } from 'd3-array';
3+
4+
import { sortFunc } from 'svelte-ux';
5+
6+
import pageSource from './+page.svelte?raw';
7+
8+
let numBars = 12;
9+
10+
function chartData(names: Set<string>, valueFunction: Function) {
11+
const data = Array.from(names, (name) => {
12+
return { name, value: valueFunction(name) };
13+
}).sort(sortFunc((d) => d.value, 'desc'));
14+
15+
let chartNames = [];
16+
for (let i = 0; i < data.length; ++i) {
17+
data[i].rank = i;
18+
if (i < numBars) chartNames.push(data[i].name);
19+
}
20+
return { names: chartNames, data: data };
21+
}
22+
23+
export async function load() {
24+
let data = await fetch('/data/examples/category-brands.csv').then(async (r) => {
25+
return csvParse(await r.text(), autoType);
26+
});
27+
28+
const dataByDateAndName = Array.from(
29+
rollup(
30+
data,
31+
([d]) => d.value,
32+
(d) => d.date,
33+
(d) => d.name
34+
)
35+
)
36+
.map(([date, data]) => [date, data])
37+
.sort(sortFunc((d) => d[0]));
38+
39+
// all brand names in the dataset
40+
let names = new Set(data.map((d) => d.name));
41+
42+
// create keyframes that interpolate between each date (year) in the dataset
43+
let keyframes = [];
44+
let dateLeft: Date;
45+
let dataByNameLeft: Map<string, number>;
46+
let dateRight: Date;
47+
let dataByNameRight: Map<string, number>;
48+
let k = 10;
49+
let allChartNames = [];
50+
51+
for ([[dateLeft, dataByNameLeft], [dateRight, dataByNameRight]] of pairs(dataByDateAndName)) {
52+
for (let i = 0; i < k; ++i) {
53+
const t = i / k;
54+
let tmp = chartData(
55+
names,
56+
(name) => (dataByNameLeft.get(name) || 0) * (1 - t) + (dataByNameRight.get(name) || 0) * t
57+
);
58+
allChartNames = allChartNames.concat(tmp.names);
59+
keyframes.push({
60+
date: new Date(dateLeft * (1 - t) + dateRight * t),
61+
data: tmp.data,
62+
});
63+
}
64+
}
65+
66+
let tmp = chartData(names, (name) => dataByNameRight.get(name) || 0);
67+
allChartNames = allChartNames.concat(tmp.names);
68+
keyframes.push({ date: new Date(dateRight), data: tmp.data });
69+
let namesInChart = Array.from(new Set(allChartNames));
70+
71+
let finalKeyframes = [];
72+
for (let i = 0; i < keyframes.length; ++i) {
73+
let newKeyframe = { date: keyframes[i].date, data: [] };
74+
let keyframeMap = new Map(keyframes[i].data.map((d) => [d.name, d]));
75+
for (let j = 0; j < namesInChart.length; ++j) {
76+
newKeyframe.data.push(keyframeMap.get(namesInChart[j]));
77+
}
78+
finalKeyframes.push(newKeyframe);
79+
}
80+
81+
keyframes = finalKeyframes;
82+
83+
return {
84+
data,
85+
namesInChart,
86+
keyframes,
87+
meta: {
88+
pageSource,
89+
hideTableOfContents: true,
90+
},
91+
};
92+
}

0 commit comments

Comments
 (0)