Skip to content
This repository was archived by the owner on Jun 6, 2025. It is now read-only.

Commit 6dabbcd

Browse files
committed
Add new LineChartCumulative chart
1 parent 827c6a3 commit 6dabbcd

File tree

22 files changed

+941
-2
lines changed

22 files changed

+941
-2
lines changed

.storybook/preview.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {themes} from '@storybook/theming';
22
import {PolarisVizProvider} from '@shopify/polaris-viz';
33
import {DARK_THEME, LIGHT_THEME} from '../packages/polaris-viz/src/constants';
44
import {useTheme} from '../packages/polaris-viz/src/hooks';
5+
import variables from '../packages/polaris-viz-core/src/styles/shared/_variables.scss';
56

67
// https://github.com/storybookjs/storybook/issues/548
78
const storiesOrder = {
@@ -163,6 +164,20 @@ export const decorators = [
163164
color: 'rgb(0, 164, 124)',
164165
},
165166
},
167+
LinePositive: {
168+
seriesColors: {
169+
comparison: variables.colorGray40,
170+
single: 'rgba(4, 123, 93, 1)',
171+
all: [variables.colorGray40, 'rgba(4, 123, 93, 1)'],
172+
},
173+
},
174+
LineNegative: {
175+
seriesColors: {
176+
comparison: variables.colorGray40,
177+
single: variables.colorGray120,
178+
all: [variables.colorGray40, variables.colorGray120],
179+
},
180+
},
166181
}}
167182
>
168183
<Container theme={context.args.theme}>
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import {useState, Fragment} from 'react';
2+
import {
3+
useYScale,
4+
LineSeries,
5+
DEFAULT_THEME_NAME,
6+
useChartPositions,
7+
useChartContext,
8+
uniqueId,
9+
} from '@shopify/polaris-viz-core';
10+
import type {
11+
XAxisOptions,
12+
YAxisOptions,
13+
LineChartDataSeriesWithDefaults,
14+
LabelFormatter,
15+
} from '@shopify/polaris-viz-core';
16+
17+
import {useIndexForLabels} from '../../hooks/useIndexForLabels';
18+
import {XAxis} from '../XAxis';
19+
import {useLegend} from '../LegendContainer';
20+
import {
21+
useTheme,
22+
useColorVisionEvents,
23+
useLinearLabelsAndDimensions,
24+
} from '../../hooks';
25+
import {ChartElements} from '../ChartElements';
26+
27+
import {PointsAndCrosshair} from './components';
28+
import {useFormatData, useFormattedLabels} from './hooks';
29+
import {yAxisMinMax} from './utilities';
30+
31+
export interface ChartProps {
32+
data: LineChartDataSeriesWithDefaults[];
33+
emptyStateText?: string;
34+
seriesNameFormatter: LabelFormatter;
35+
theme?: string;
36+
xAxisOptions: Required<XAxisOptions>;
37+
yAxisOptions: Required<YAxisOptions>;
38+
fixedActiveIndex: number;
39+
}
40+
41+
export function Chart({
42+
emptyStateText,
43+
data,
44+
seriesNameFormatter,
45+
theme = DEFAULT_THEME_NAME,
46+
xAxisOptions,
47+
yAxisOptions,
48+
fixedActiveIndex,
49+
}: ChartProps) {
50+
const selectedTheme = useTheme(theme);
51+
const {isTouchDevice} = useChartContext();
52+
53+
useColorVisionEvents({
54+
enabled: data.length > 1 && !isTouchDevice,
55+
});
56+
57+
const [xAxisHeight, setXAxisHeight] = useState(0);
58+
59+
const {height, width} = useLegend({
60+
data: [
61+
{
62+
shape: 'Line',
63+
series: data,
64+
},
65+
],
66+
showLegend: false,
67+
seriesNameFormatter,
68+
});
69+
70+
const indexForLabels = useIndexForLabels(data);
71+
72+
const {formattedLabels} = useFormattedLabels({
73+
data: [data[indexForLabels]],
74+
labelFormatter: xAxisOptions.labelFormatter,
75+
indexToKeep: [0, fixedActiveIndex, data[indexForLabels]?.data.length - 2],
76+
});
77+
78+
const emptyState =
79+
data.length === 0 || data.every((series) => series.data.length === 0);
80+
81+
const {minY, maxY} = yAxisMinMax(data);
82+
83+
const yScaleOptions = {
84+
formatYAxisLabel: yAxisOptions.labelFormatter,
85+
integersOnly: yAxisOptions.integersOnly,
86+
fixedWidth: yAxisOptions.fixedWidth,
87+
maxYOverride: yAxisOptions.maxYOverride,
88+
max: maxY,
89+
min: minY,
90+
ticksOverride: yAxisOptions.ticksOverride,
91+
};
92+
93+
const {yAxisLabelWidth} = useYScale({
94+
...yScaleOptions,
95+
drawableHeight: height,
96+
verticalOverflow: selectedTheme.grid.verticalOverflow,
97+
});
98+
99+
const {longestSeriesLength, longestSeriesIndex} = useFormatData(data);
100+
101+
const {
102+
drawableWidth,
103+
drawableHeight,
104+
chartXPosition,
105+
chartYPosition,
106+
xAxisBounds,
107+
} = useChartPositions({
108+
annotationsHeight: 0,
109+
height,
110+
width,
111+
xAxisHeight,
112+
yAxisWidth: yAxisLabelWidth,
113+
});
114+
115+
const {xAxisDetails, xScale, labels} = useLinearLabelsAndDimensions({
116+
data,
117+
drawableWidth,
118+
hideXAxis: false,
119+
labels: formattedLabels,
120+
longestSeriesLength,
121+
});
122+
123+
const {yScale} = useYScale({
124+
...yScaleOptions,
125+
drawableHeight,
126+
verticalOverflow: selectedTheme.grid.verticalOverflow,
127+
});
128+
129+
if (xScale == null || drawableWidth == null || yAxisLabelWidth == null) {
130+
return null;
131+
}
132+
133+
const halfXAxisLabelWidth = xAxisDetails.labelWidth / 2;
134+
135+
return (
136+
<Fragment>
137+
<ChartElements.Svg
138+
emptyState={emptyState}
139+
emptyStateText={emptyStateText}
140+
height={height}
141+
role="table"
142+
width={width}
143+
>
144+
<XAxis
145+
allowLineWrap={xAxisOptions.allowLineWrap}
146+
ariaHidden
147+
isLinearChart
148+
labels={labels}
149+
labelWidth={xAxisDetails.labelWidth}
150+
onHeightChange={setXAxisHeight}
151+
reducedLabelIndexes={xAxisDetails.reducedLabelIndexes}
152+
x={xAxisBounds.x - halfXAxisLabelWidth}
153+
xScale={xScale}
154+
y={xAxisBounds.y}
155+
activeIndex={{
156+
index: fixedActiveIndex,
157+
color: selectedTheme.seriesColors.single as string,
158+
}}
159+
/>
160+
161+
<g transform={`translate(${chartXPosition},${chartYPosition})`}>
162+
<LineSeries
163+
activeLineIndex={-1}
164+
data={data[1]}
165+
index={2}
166+
key={`placeholder-${2}`}
167+
svgDimensions={{height: drawableHeight, width: drawableWidth}}
168+
theme={theme}
169+
xScale={xScale}
170+
yScale={yScale}
171+
type="default"
172+
shouldShowArea={false}
173+
/>
174+
175+
{data.map((singleSeries, index) => {
176+
if (singleSeries.metadata?.isVisuallyHidden === true) {
177+
return null;
178+
}
179+
180+
return (
181+
<LineSeries
182+
activeLineIndex={-1}
183+
data={singleSeries}
184+
index={index}
185+
key={`${name}-${index}`}
186+
svgDimensions={{height: drawableHeight, width: drawableWidth}}
187+
theme={theme}
188+
xScale={xScale}
189+
yScale={yScale}
190+
type="default"
191+
/>
192+
);
193+
})}
194+
195+
<PointsAndCrosshair
196+
activeIndex={fixedActiveIndex}
197+
data={data}
198+
drawableHeight={drawableHeight}
199+
emptyState={emptyState}
200+
longestSeriesIndex={longestSeriesIndex}
201+
theme={theme}
202+
tooltipId={uniqueId('lineChartCumulative')}
203+
xScale={xScale}
204+
yScale={yScale}
205+
/>
206+
</g>
207+
</ChartElements.Svg>
208+
</Fragment>
209+
);
210+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {Fragment} from 'react';
2+
import type {
3+
XAxisOptions,
4+
YAxisOptions,
5+
ChartProps,
6+
} from '@shopify/polaris-viz-core';
7+
import {
8+
InternalChartType,
9+
ChartState,
10+
DEFAULT_CHART_PROPS,
11+
usePolarisVizContext,
12+
useThemeSeriesColors,
13+
} from '@shopify/polaris-viz-core';
14+
15+
import {fillMissingDataPoints} from '../../utilities/fillMissingDataPoints';
16+
import {getLineChartDataWithDefaults} from '../../utilities/getLineChartDataWithDefaults';
17+
import {ChartContainer} from '../ChartContainer';
18+
import {ChartSkeleton} from '../ChartSkeleton';
19+
import {
20+
getXAxisOptionsWithDefaults,
21+
getYAxisOptionsWithDefaults,
22+
} from '../../utilities';
23+
import {useTheme} from '../../hooks';
24+
25+
import {Chart} from './Chart';
26+
27+
export type LineChartCumulativeProps = {
28+
errorText?: string;
29+
emptyStateText?: string;
30+
xAxisOptions?: Partial<XAxisOptions>;
31+
yAxisOptions?: Partial<YAxisOptions>;
32+
scrollContainer?: Element | null;
33+
fixedActiveIndex: number;
34+
} & ChartProps;
35+
36+
export function LineChartCumulative(props: LineChartCumulativeProps) {
37+
const {defaultTheme} = usePolarisVizContext();
38+
39+
const {
40+
data: dataSeries,
41+
emptyStateText,
42+
errorText,
43+
id,
44+
isAnimated,
45+
onError,
46+
seriesNameFormatter = (value) => `${value}`,
47+
state,
48+
theme = defaultTheme,
49+
xAxisOptions,
50+
yAxisOptions,
51+
scrollContainer,
52+
fixedActiveIndex,
53+
} = {
54+
...DEFAULT_CHART_PROPS,
55+
...props,
56+
};
57+
58+
const data = fillMissingDataPoints(dataSeries, true);
59+
60+
const selectedTheme = useTheme(theme);
61+
const seriesColors = useThemeSeriesColors(data, selectedTheme);
62+
63+
const xAxisOptionsWithDefaults = getXAxisOptionsWithDefaults(xAxisOptions);
64+
const yAxisOptionsWithDefaults = getYAxisOptionsWithDefaults(yAxisOptions);
65+
66+
const dataWithDefaults = getLineChartDataWithDefaults(data, seriesColors);
67+
68+
return (
69+
<Fragment>
70+
<ChartContainer
71+
id={id}
72+
data={data}
73+
theme={theme}
74+
isAnimated={isAnimated}
75+
type={InternalChartType.Line}
76+
onError={onError}
77+
scrollContainer={scrollContainer}
78+
>
79+
{state !== ChartState.Success ? (
80+
<ChartSkeleton state={state} errorText={errorText} theme={theme} />
81+
) : (
82+
<Chart
83+
data={dataWithDefaults}
84+
emptyStateText={emptyStateText}
85+
seriesNameFormatter={seriesNameFormatter}
86+
theme={theme}
87+
xAxisOptions={xAxisOptionsWithDefaults}
88+
yAxisOptions={yAxisOptionsWithDefaults}
89+
fixedActiveIndex={fixedActiveIndex}
90+
/>
91+
)}
92+
</ChartContainer>
93+
</Fragment>
94+
);
95+
}

0 commit comments

Comments
 (0)