Skip to content

Commit fe65461

Browse files
committed
feat: grouped bar chart (aka stacked primary axis)
1 parent f067ca5 commit fe65461

File tree

10 files changed

+195
-26
lines changed

10 files changed

+195
-26
lines changed

examples/simple/src/components/Bar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AxisOptions, Chart } from "react-charts";
55

66
export default function Bar() {
77
const { data, randomizeData } = useDemoConfig({
8-
series: 10,
8+
series: 3,
99
dataType: "ordinal",
1010
});
1111

examples/simple/src/components/BarHorizontal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AxisOptions, Chart } from "react-charts";
55

66
export default function Bar() {
77
const { data, randomizeData } = useDemoConfig({
8-
series: 10,
8+
series: 3,
99
dataType: "ordinal",
1010
});
1111

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import ResizableBox from "../ResizableBox";
2+
import useDemoConfig from "../useDemoConfig";
3+
import React from "react";
4+
import { AxisOptions, Chart } from "react-charts";
5+
6+
export default function BarHorizontalStacked() {
7+
const { data, randomizeData } = useDemoConfig({
8+
series: 10,
9+
dataType: "ordinal",
10+
});
11+
12+
const primaryAxis = React.useMemo<
13+
AxisOptions<typeof data[number]["data"][number]>
14+
>(
15+
() => ({
16+
position: "left",
17+
getValue: (datum) => datum.primary,
18+
}),
19+
[]
20+
);
21+
22+
const secondaryAxes = React.useMemo<
23+
AxisOptions<typeof data[number]["data"][number]>[]
24+
>(
25+
() => [
26+
{
27+
position: "bottom",
28+
getValue: (datum) => datum.secondary,
29+
stacked: true,
30+
},
31+
],
32+
[]
33+
);
34+
35+
return (
36+
<>
37+
<button onClick={randomizeData}>Randomize Data</button>
38+
<br />
39+
<br />
40+
<ResizableBox>
41+
<Chart
42+
options={{
43+
data,
44+
primaryAxis,
45+
secondaryAxes,
46+
}}
47+
/>
48+
</ResizableBox>
49+
</>
50+
);
51+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import ResizableBox from "../ResizableBox";
2+
import useDemoConfig from "../useDemoConfig";
3+
import React from "react";
4+
import { AxisOptions, Chart } from "react-charts";
5+
6+
export default function BarStacked() {
7+
const { data, randomizeData } = useDemoConfig({
8+
series: 10,
9+
dataType: "ordinal",
10+
});
11+
12+
const primaryAxis = React.useMemo<
13+
AxisOptions<typeof data[number]["data"][number]>
14+
>(
15+
() => ({
16+
getValue: (datum) => datum.primary,
17+
}),
18+
[]
19+
);
20+
21+
const secondaryAxes = React.useMemo<
22+
AxisOptions<typeof data[number]["data"][number]>[]
23+
>(
24+
() => [
25+
{
26+
getValue: (datum) => datum.secondary,
27+
stacked: true,
28+
},
29+
],
30+
[]
31+
);
32+
33+
return (
34+
<>
35+
<button onClick={randomizeData}>Randomize Data</button>
36+
<br />
37+
<br />
38+
<ResizableBox>
39+
<Chart
40+
options={{
41+
data,
42+
primaryAxis,
43+
secondaryAxes,
44+
}}
45+
/>
46+
</ResizableBox>
47+
</>
48+
);
49+
}

examples/simple/src/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ReactDOM from "react-dom";
88
import Area from "./components/Area";
99
import Band from "./components/Band";
1010
import Bar from "./components/Bar";
11+
import BarStacked from "./components/BarStacked";
1112
import Bubble from "./components/Bubble";
1213
import CustomStyles from "./components/CustomStyles";
1314
import DarkMode from "./components/DarkMode";
@@ -17,14 +18,17 @@ import Line from "./components/Line";
1718
import MultipleAxes from "./components/MultipleAxes";
1819
import Steam from "./components/Steam";
1920
import BarHorizontal from "./components/BarHorizontal";
21+
import BarHorizontalStacked from "./components/BarHorizontalStacked";
2022
import SparkChart from "./components/SparkChart";
2123
import SyncedCursors from "./components/SyncedCursors";
2224
import StressTest from "./components/StressTest";
2325

2426
const components = [
2527
["Line", Line],
2628
["Bar", Bar],
29+
["Bar (Stacked)", BarStacked],
2730
["Bar (Horizontal)", BarHorizontal],
31+
["Bar (Horizontal + Stacked)", BarHorizontalStacked],
2832
["Band", Band],
2933
["Area", Area],
3034
["Bubble", Bubble],

examples/simple/src/styles.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
body {
2+
background: rgba(0, 0, 0, 0.02);
3+
}
4+
15
.react-resizable {
26
max-width: 100%;
7+
background: white;
38
}
49

510
.react-resizable-handle {

src/components/Chart.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import Voronoi from './Voronoi'
4141
const defaultColorScheme = [
4242
'#0f83ab',
4343
'#faa43a',
44-
'#ff4e4e',
44+
'#fd6868',
4545
'#53cfc9',
4646
'#a2d925',
4747
'#decf3f',
@@ -263,7 +263,7 @@ function ChartInner<TDatum>({
263263
if (
264264
typeof optionsWithScaleType.stacked === 'undefined' &&
265265
optionsWithScaleType.elementType &&
266-
['bar', 'area'].includes(optionsWithScaleType.elementType)
266+
['area'].includes(optionsWithScaleType.elementType)
267267
) {
268268
optionsWithScaleType.stacked = true
269269
}
@@ -276,6 +276,13 @@ function ChartInner<TDatum>({
276276
)
277277
}, [options.data, options.secondaryAxes, primaryAxisOptions])
278278

279+
if (
280+
primaryAxisOptions.scaleType === 'band' &&
281+
!secondaryAxesOptions.some(axisOptions => axisOptions.stacked)
282+
) {
283+
primaryAxisOptions.stacked = primaryAxisOptions.stacked ?? true
284+
}
285+
279286
// Resolve Tooltip Option
280287
const tooltipOptions = React.useMemo(() => {
281288
const tooltipOptions = defaultTooltip(options?.tooltip)

src/types.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ export type AxisOptionsBase = {
242242
tickCount?: number
243243
innerBandPadding?: number
244244
outerBandPadding?: number
245+
innerSeriesBandPadding?: number
246+
outerSeriesBandPadding?: number
245247
minBandSize?: number
246248
maxBandSize?: number
247249
minDomainLength?: number
@@ -346,6 +348,8 @@ export type ResolvedAxisOptions<TAxisOptions> = TSTB.Object.Required<
346348
| 'tickLabelRotationDeg'
347349
| 'innerBandPadding'
348350
| 'outerBandPadding'
351+
| 'innerSeriesBandPadding'
352+
| 'outerSeriesBandPadding'
349353
| 'show'
350354
| 'stacked'
351355
>
@@ -365,7 +369,8 @@ export type AxisTime<TDatum> = Omit<
365369
axisFamily: 'time'
366370
scale: ScaleTime<number, number, never>
367371
outerScale: ScaleTime<number, number, never>
368-
bandScale?: ScaleBand<number>
372+
primaryBandScale?: ScaleBand<number>
373+
seriesBandScale?: ScaleBand<number>
369374
formatters: {
370375
default: (value: Date) => string
371376
scale: (value: Date) => string
@@ -381,7 +386,8 @@ export type AxisLinear<TDatum> = Omit<
381386
axisFamily: 'linear'
382387
scale: ScaleLinear<number, number, never>
383388
outerScale: ScaleLinear<number, number, never>
384-
bandScale?: ScaleBand<number>
389+
primaryBandScale?: ScaleBand<number>
390+
seriesBandScale?: ScaleBand<number>
385391
formatters: {
386392
default: (value: ChartValue<any>) => string
387393
scale: (value: number) => string
@@ -397,6 +403,7 @@ export type AxisBand<TDatum> = Omit<
397403
axisFamily: 'band'
398404
scale: ScaleBand<any>
399405
outerScale: ScaleBand<any>
406+
seriesBandScale: ScaleBand<number>
400407
formatters: {
401408
default: (value: any) => string
402409
scale: (value: any) => string

src/utils/Utils.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,14 @@ export function getPrimary<TDatum>(
117117
datum: Datum<TDatum>,
118118
primaryAxis: Axis<TDatum>
119119
): number {
120-
let primary: number
121-
122-
if (primaryAxis.stacked) {
123-
primary = primaryAxis.scale(datum.stackData?.[1] ?? NaN) ?? NaN
124-
} else {
125-
primary = primaryAxis.scale(datum.primaryValue) ?? NaN
126-
}
120+
let primary = primaryAxis.scale(datum.primaryValue) ?? NaN
127121

128122
if (primaryAxis.axisFamily === 'band') {
123+
if (primaryAxis.stacked) {
124+
primary =
125+
primary + (primaryAxis.seriesBandScale(datum.seriesIndex) ?? NaN)
126+
}
127+
129128
primary = primary + getPrimaryLength(datum, primaryAxis) / 2
130129
}
131130

@@ -146,13 +145,17 @@ export function getPrimaryLength<TDatum>(
146145
primaryAxis: Axis<TDatum>
147146
) {
148147
if (primaryAxis.axisFamily === 'band') {
148+
const bandWidth = primaryAxis.stacked
149+
? primaryAxis.seriesBandScale.bandwidth()
150+
: primaryAxis.scale.bandwidth()
151+
149152
return Math.min(
150-
Math.max(primaryAxis.scale.bandwidth(), primaryAxis.minBandSize ?? 1),
153+
Math.max(bandWidth, primaryAxis.minBandSize ?? 1),
151154
primaryAxis.maxBandSize ?? 99999999
152155
)
153156
}
154157

155-
return Math.max(primaryAxis.bandScale!.bandwidth(), 1)
158+
return Math.max(primaryAxis.primaryBandScale!.bandwidth(), 1)
156159
}
157160

158161
export function getSecondaryLength<TDatum>(

0 commit comments

Comments
 (0)