Skip to content

Commit

Permalink
feat: grouped bar chart (aka stacked primary axis)
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Jul 29, 2021
1 parent f067ca5 commit fe65461
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 26 deletions.
2 changes: 1 addition & 1 deletion examples/simple/src/components/Bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AxisOptions, Chart } from "react-charts";

export default function Bar() {
const { data, randomizeData } = useDemoConfig({
series: 10,
series: 3,
dataType: "ordinal",
});

Expand Down
2 changes: 1 addition & 1 deletion examples/simple/src/components/BarHorizontal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AxisOptions, Chart } from "react-charts";

export default function Bar() {
const { data, randomizeData } = useDemoConfig({
series: 10,
series: 3,
dataType: "ordinal",
});

Expand Down
51 changes: 51 additions & 0 deletions examples/simple/src/components/BarHorizontalStacked.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import ResizableBox from "../ResizableBox";
import useDemoConfig from "../useDemoConfig";
import React from "react";
import { AxisOptions, Chart } from "react-charts";

export default function BarHorizontalStacked() {
const { data, randomizeData } = useDemoConfig({
series: 10,
dataType: "ordinal",
});

const primaryAxis = React.useMemo<
AxisOptions<typeof data[number]["data"][number]>
>(
() => ({
position: "left",
getValue: (datum) => datum.primary,
}),
[]
);

const secondaryAxes = React.useMemo<
AxisOptions<typeof data[number]["data"][number]>[]
>(
() => [
{
position: "bottom",
getValue: (datum) => datum.secondary,
stacked: true,
},
],
[]
);

return (
<>
<button onClick={randomizeData}>Randomize Data</button>
<br />
<br />
<ResizableBox>
<Chart
options={{
data,
primaryAxis,
secondaryAxes,
}}
/>
</ResizableBox>
</>
);
}
49 changes: 49 additions & 0 deletions examples/simple/src/components/BarStacked.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import ResizableBox from "../ResizableBox";
import useDemoConfig from "../useDemoConfig";
import React from "react";
import { AxisOptions, Chart } from "react-charts";

export default function BarStacked() {
const { data, randomizeData } = useDemoConfig({
series: 10,
dataType: "ordinal",
});

const primaryAxis = React.useMemo<
AxisOptions<typeof data[number]["data"][number]>
>(
() => ({
getValue: (datum) => datum.primary,
}),
[]
);

const secondaryAxes = React.useMemo<
AxisOptions<typeof data[number]["data"][number]>[]
>(
() => [
{
getValue: (datum) => datum.secondary,
stacked: true,
},
],
[]
);

return (
<>
<button onClick={randomizeData}>Randomize Data</button>
<br />
<br />
<ResizableBox>
<Chart
options={{
data,
primaryAxis,
secondaryAxes,
}}
/>
</ResizableBox>
</>
);
}
4 changes: 4 additions & 0 deletions examples/simple/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ReactDOM from "react-dom";
import Area from "./components/Area";
import Band from "./components/Band";
import Bar from "./components/Bar";
import BarStacked from "./components/BarStacked";
import Bubble from "./components/Bubble";
import CustomStyles from "./components/CustomStyles";
import DarkMode from "./components/DarkMode";
Expand All @@ -17,14 +18,17 @@ import Line from "./components/Line";
import MultipleAxes from "./components/MultipleAxes";
import Steam from "./components/Steam";
import BarHorizontal from "./components/BarHorizontal";
import BarHorizontalStacked from "./components/BarHorizontalStacked";
import SparkChart from "./components/SparkChart";
import SyncedCursors from "./components/SyncedCursors";
import StressTest from "./components/StressTest";

const components = [
["Line", Line],
["Bar", Bar],
["Bar (Stacked)", BarStacked],
["Bar (Horizontal)", BarHorizontal],
["Bar (Horizontal + Stacked)", BarHorizontalStacked],
["Band", Band],
["Area", Area],
["Bubble", Bubble],
Expand Down
5 changes: 5 additions & 0 deletions examples/simple/src/styles.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
body {
background: rgba(0, 0, 0, 0.02);
}

.react-resizable {
max-width: 100%;
background: white;
}

.react-resizable-handle {
Expand Down
11 changes: 9 additions & 2 deletions src/components/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import Voronoi from './Voronoi'
const defaultColorScheme = [
'#0f83ab',
'#faa43a',
'#ff4e4e',
'#fd6868',
'#53cfc9',
'#a2d925',
'#decf3f',
Expand Down Expand Up @@ -263,7 +263,7 @@ function ChartInner<TDatum>({
if (
typeof optionsWithScaleType.stacked === 'undefined' &&
optionsWithScaleType.elementType &&
['bar', 'area'].includes(optionsWithScaleType.elementType)
['area'].includes(optionsWithScaleType.elementType)
) {
optionsWithScaleType.stacked = true
}
Expand All @@ -276,6 +276,13 @@ function ChartInner<TDatum>({
)
}, [options.data, options.secondaryAxes, primaryAxisOptions])

if (
primaryAxisOptions.scaleType === 'band' &&
!secondaryAxesOptions.some(axisOptions => axisOptions.stacked)
) {
primaryAxisOptions.stacked = primaryAxisOptions.stacked ?? true
}

// Resolve Tooltip Option
const tooltipOptions = React.useMemo(() => {
const tooltipOptions = defaultTooltip(options?.tooltip)
Expand Down
11 changes: 9 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ export type AxisOptionsBase = {
tickCount?: number
innerBandPadding?: number
outerBandPadding?: number
innerSeriesBandPadding?: number
outerSeriesBandPadding?: number
minBandSize?: number
maxBandSize?: number
minDomainLength?: number
Expand Down Expand Up @@ -346,6 +348,8 @@ export type ResolvedAxisOptions<TAxisOptions> = TSTB.Object.Required<
| 'tickLabelRotationDeg'
| 'innerBandPadding'
| 'outerBandPadding'
| 'innerSeriesBandPadding'
| 'outerSeriesBandPadding'
| 'show'
| 'stacked'
>
Expand All @@ -365,7 +369,8 @@ export type AxisTime<TDatum> = Omit<
axisFamily: 'time'
scale: ScaleTime<number, number, never>
outerScale: ScaleTime<number, number, never>
bandScale?: ScaleBand<number>
primaryBandScale?: ScaleBand<number>
seriesBandScale?: ScaleBand<number>
formatters: {
default: (value: Date) => string
scale: (value: Date) => string
Expand All @@ -381,7 +386,8 @@ export type AxisLinear<TDatum> = Omit<
axisFamily: 'linear'
scale: ScaleLinear<number, number, never>
outerScale: ScaleLinear<number, number, never>
bandScale?: ScaleBand<number>
primaryBandScale?: ScaleBand<number>
seriesBandScale?: ScaleBand<number>
formatters: {
default: (value: ChartValue<any>) => string
scale: (value: number) => string
Expand All @@ -397,6 +403,7 @@ export type AxisBand<TDatum> = Omit<
axisFamily: 'band'
scale: ScaleBand<any>
outerScale: ScaleBand<any>
seriesBandScale: ScaleBand<number>
formatters: {
default: (value: any) => string
scale: (value: any) => string
Expand Down
21 changes: 12 additions & 9 deletions src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,14 @@ export function getPrimary<TDatum>(
datum: Datum<TDatum>,
primaryAxis: Axis<TDatum>
): number {
let primary: number

if (primaryAxis.stacked) {
primary = primaryAxis.scale(datum.stackData?.[1] ?? NaN) ?? NaN
} else {
primary = primaryAxis.scale(datum.primaryValue) ?? NaN
}
let primary = primaryAxis.scale(datum.primaryValue) ?? NaN

if (primaryAxis.axisFamily === 'band') {
if (primaryAxis.stacked) {
primary =
primary + (primaryAxis.seriesBandScale(datum.seriesIndex) ?? NaN)
}

primary = primary + getPrimaryLength(datum, primaryAxis) / 2
}

Expand All @@ -146,13 +145,17 @@ export function getPrimaryLength<TDatum>(
primaryAxis: Axis<TDatum>
) {
if (primaryAxis.axisFamily === 'band') {
const bandWidth = primaryAxis.stacked
? primaryAxis.seriesBandScale.bandwidth()
: primaryAxis.scale.bandwidth()

return Math.min(
Math.max(primaryAxis.scale.bandwidth(), primaryAxis.minBandSize ?? 1),
Math.max(bandWidth, primaryAxis.minBandSize ?? 1),
primaryAxis.maxBandSize ?? 99999999
)
}

return Math.max(primaryAxis.bandScale!.bandwidth(), 1)
return Math.max(primaryAxis.primaryBandScale!.bandwidth(), 1)
}

export function getSecondaryLength<TDatum>(
Expand Down
Loading

1 comment on commit fe65461

@vercel
Copy link

@vercel vercel bot commented on fe65461 Jul 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.