Skip to content

Commit 50da077

Browse files
feat(chart-provider): add component to wrap charts (#4236)
* feat(chart-provider): add component to wrap charts * fix(ci): yarn install error * Update packages/paste-core/components/chart-provider/package.json Co-authored-by: Nora Krantz <75342690+nkrantz@users.noreply.github.com> * feat(chart-provider): remove chart type, not required at this stage * chore(pr): cleanup unused import --------- Co-authored-by: Nora Krantz <75342690+nkrantz@users.noreply.github.com>
1 parent 1d12fda commit 50da077

File tree

20 files changed

+1986
-0
lines changed

20 files changed

+1986
-0
lines changed

.changeset/cyan-lemons-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@twilio-paste/codemods": minor
3+
---
4+
5+
[ChartProvider] added a new component that will wrap chart instances to control and share the state to child charting components

.changeset/popular-plants-search.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@twilio-paste/chart-provider": major
3+
"@twilio-paste/core": minor
4+
---
5+
6+
[ChartProvider] added a new component that will wrap chart instances to control and share the state to child charting components

.codesandbox/ci.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"/packages/paste-core/components/button-group",
2222
"/packages/paste-core/components/callout",
2323
"/packages/paste-core/components/card",
24+
"/packages/paste-core/components/chart-provider",
2425
"/packages/paste-core/components/chat-composer",
2526
"/packages/paste-core/components/chat-log",
2627
"/packages/paste-core/components/checkbox",

packages/paste-codemods/tools/.cache/mappings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"CalloutListItem": "@twilio-paste/core/callout",
4949
"CalloutText": "@twilio-paste/core/callout",
5050
"Card": "@twilio-paste/core/card",
51+
"ChartContext": "@twilio-paste/core/chart-provider",
52+
"ChartProvider": "@twilio-paste/core/chart-provider",
5153
"ChatComposer": "@twilio-paste/core/chat-composer",
5254
"ChatComposerActionGroup": "@twilio-paste/core/chat-composer",
5355
"ChatComposerAttachmentCard": "@twilio-paste/core/chat-composer",

packages/paste-core/components/chart-provider/CHANGELOG.md

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { render } from "@testing-library/react";
2+
import { BoxProps } from "@twilio-paste/box";
3+
import * as React from "react";
4+
5+
import { ChartProvider } from "../src";
6+
7+
const TestChartProvider: React.FC<React.PropsWithChildren<{ element?: BoxProps["element"] }>> = ({
8+
element,
9+
children,
10+
}) => {
11+
return (
12+
<ChartProvider highchartsOptions={{}} data-testid="chart-provider" element={element}>
13+
{children}
14+
</ChartProvider>
15+
);
16+
};
17+
18+
describe("ChartProvider", () => {
19+
it("should render", () => {
20+
const { getByText, getByTestId } = render(<TestChartProvider>test</TestChartProvider>);
21+
expect(getByText("test")).toBeDefined();
22+
expect(getByTestId("chart-provider").getAttribute("data-paste-element")).toEqual("CHART_PROVIDER");
23+
});
24+
25+
describe("Customization", () => {
26+
it("should apply the element prop", () => {
27+
const { getByTestId } = render(<TestChartProvider element="TEST_ELEMENT" />);
28+
expect(getByTestId("chart-provider").getAttribute("data-paste-element")).toEqual("TEST_ELEMENT");
29+
});
30+
});
31+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const { build } = require("../../../../tools/build/esbuild");
2+
3+
build(require("./package.json"));
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"name": "@twilio-paste/chart-provider",
3+
"version": "0.0.0",
4+
"category": "data display",
5+
"status": "beta",
6+
"description": "Chart Provider is a data visualization component used to wrap an individual chart to store and share state to child charting elements.",
7+
"author": "Twilio Inc.",
8+
"license": "MIT",
9+
"main:dev": "src/index.tsx",
10+
"main": "dist/index.js",
11+
"module": "dist/index.es.js",
12+
"types": "dist/index.d.ts",
13+
"sideEffects": false,
14+
"publishConfig": {
15+
"access": "public"
16+
},
17+
"files": [
18+
"dist"
19+
],
20+
"scripts": {
21+
"build": "yarn clean && NODE_ENV=production node build.js && tsc",
22+
"build:js": "NODE_ENV=development node build.js",
23+
"build:typedocs": "tsx ../../../../tools/build/generate-type-docs",
24+
"clean": "rm -rf ./dist",
25+
"tsc": "tsc"
26+
},
27+
"peerDependencies": {
28+
"@twilio-paste/animation-library": "^2.0.0",
29+
"@twilio-paste/box": "^10.2.0",
30+
"@twilio-paste/color-contrast-utils": "^5.0.0",
31+
"@twilio-paste/customization": "^8.1.1",
32+
"@twilio-paste/design-tokens": "^10.3.0",
33+
"@twilio-paste/style-props": "^9.1.1",
34+
"@twilio-paste/styling-library": "^3.0.0",
35+
"@twilio-paste/theme": "^11.0.1",
36+
"@twilio-paste/types": "^6.0.0",
37+
"@types/react": "^16.8.6 || ^17.0.2 || ^18.0.27",
38+
"@types/react-dom": "^16.8.6 || ^17.0.2 || ^18.0.10",
39+
"highcharts": "^9.3.3",
40+
"react": "^16.8.6 || ^17.0.2 || ^18.0.0",
41+
"react-dom": "^16.8.6 || ^17.0.2 || ^18.0.0"
42+
},
43+
"devDependencies": {
44+
"@twilio-paste/animation-library": "^2.0.0",
45+
"@twilio-paste/box": "^10.2.0",
46+
"@twilio-paste/color-contrast-utils": "^5.0.0",
47+
"@twilio-paste/customization": "^8.1.1",
48+
"@twilio-paste/design-tokens": "^10.3.0",
49+
"@twilio-paste/style-props": "^9.1.1",
50+
"@twilio-paste/styling-library": "^3.0.0",
51+
"@twilio-paste/theme": "^11.0.1",
52+
"@twilio-paste/types": "^6.0.0",
53+
"@types/react": "^18.0.27",
54+
"@types/react-dom": "^18.0.10",
55+
"highcharts": "^9.3.3",
56+
"react": "^18.0.0",
57+
"react-dom": "^18.0.0",
58+
"tsx": "^4.0.0",
59+
"typescript": "^4.9.4"
60+
}
61+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as Highcharts from "highcharts";
2+
import * as React from "react";
3+
4+
export interface ChartContextProps {
5+
/**
6+
* The function that will be called by the HighchartsReact callback to set the chart object in the context.
7+
*
8+
* @param {Function} chart - the chart object returned from the HighchartsReact callback
9+
* @memberof ChartContextProps
10+
*/
11+
setChart: (chart: Highcharts.Chart) => void;
12+
/**
13+
* Used to the set the reference to the chart element once it is populated
14+
*
15+
* @param {HTMLElement} ref - React.MutableRefObject.current of base chart component
16+
* @memberof ChartContextProps
17+
*/
18+
setChartRef: (ref: HTMLElement) => void;
19+
/**
20+
* The options that will be passed to the ReactHighcharts component. It will be enriched with tracking events that wil be used by
21+
* other Paste components if using the ChartProvider.
22+
*
23+
* @type {Highcharts.Options}
24+
* @memberof ChartContextProps
25+
*/
26+
options: Highcharts.Options;
27+
/**
28+
* The rendered chart returned from the HighchartsReact callback. Use this object to get the rendered properties of
29+
* series and points when calculating poitioning of custom elements. It can also be used to interact
30+
* with the chart in ways such as setting zoom levels and using chart.update to trigger changes.
31+
*
32+
* @type {Highcharts.Chart}
33+
* @memberof ChartContextProps
34+
*/
35+
chart?: Highcharts.Chart;
36+
37+
/**
38+
* The current reference to the base chart component. Needed for positioning custom elements relative to points.
39+
*
40+
* @type {string}
41+
* @memberof ChartContextProps
42+
*/
43+
chartRef?: HTMLElement;
44+
/**
45+
* The current chart type. Used to trigger rerenders of other components inside ChartProvider.
46+
*
47+
* @type {string}
48+
* @memberof ChartContextProps
49+
*/
50+
chartType?: string;
51+
}
52+
53+
/**
54+
* Setting the default values to log errors is an alternative to throwing runtime errors that still allow engineers
55+
* to debug any potential issues.
56+
*/
57+
58+
export const ChartContext = React.createContext<ChartContextProps>({
59+
options: {},
60+
setChart: () => {
61+
// eslint-disable-next-line no-console
62+
console.error("setChart not implemented. Is this component wrapped in the ChartProvider component?");
63+
},
64+
setChartRef: () => {
65+
// eslint-disable-next-line no-console
66+
console.error("setChartRef not implemented. Is this component wrapped in the ChartProvider component?");
67+
},
68+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Box, safelySpreadBoxProps } from "@twilio-paste/box";
2+
import type { BoxProps } from "@twilio-paste/box";
3+
import type { HTMLPasteProps } from "@twilio-paste/types";
4+
import * as Highcharts from "highcharts";
5+
import * as React from "react";
6+
7+
import { ChartContext } from "./ChartContext";
8+
9+
interface BaseChartProviderProps extends HTMLPasteProps<"div"> {
10+
children?: React.ReactNode;
11+
/**
12+
* Overrides the default element name to apply unique styles with the Customization Provider
13+
* @default 'CHART_PROVIDER'
14+
* @type {BoxProps['element']}
15+
* @memberof ChartProviderProps
16+
*/
17+
element?: BoxProps["element"];
18+
}
19+
20+
interface HighchartsOptions extends BaseChartProviderProps {
21+
/**
22+
* Overrides the default element name to apply unique styles with the Customization Provider
23+
* @default null
24+
* @type {BoxProps['element']}
25+
* @memberof ChartProviderProps
26+
*/
27+
highchartsOptions: Highcharts.Options;
28+
pasteOptions?: never;
29+
}
30+
31+
export type ChartProviderProps = HighchartsOptions;
32+
33+
const ChartProvider = React.forwardRef<HTMLDivElement, ChartProviderProps>(
34+
({ element = "CHART_PROVIDER", children, highchartsOptions, ...props }, ref) => {
35+
const [chart, setChart] = React.useState<Highcharts.Chart>();
36+
const [chartRef, setChartRef] = React.useState<HTMLElement>();
37+
38+
return (
39+
<Box {...safelySpreadBoxProps(props)} ref={ref} element={element} position="relative">
40+
<ChartContext.Provider
41+
value={{
42+
chart,
43+
setChart,
44+
chartRef,
45+
setChartRef,
46+
options: highchartsOptions,
47+
}}
48+
>
49+
{children}
50+
</ChartContext.Provider>
51+
</Box>
52+
);
53+
},
54+
);
55+
56+
ChartProvider.displayName = "ChartProvider";
57+
58+
export { ChartProvider };
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { ChartProvider } from "./ChartProvider";
2+
export type { ChartProviderProps } from "./ChartProvider";
3+
export { ChartContext } from "./ChartContext";
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Box } from "@twilio-paste/box";
2+
import { usePasteHighchartsTheme } from "@twilio-paste/data-visualization-library";
3+
import * as Highcharts from "highcharts";
4+
import HighchartsReact from "highcharts-react-official";
5+
import * as React from "react";
6+
7+
import { ChartContext } from "../src";
8+
9+
const Chart: React.FC = () => {
10+
const chartRef = React.useRef<HTMLElement | null>(null);
11+
const { options, setChart, setChartRef } = React.useContext(ChartContext);
12+
const [chartOptions, setChartOptions] = React.useState<Highcharts.Options>(
13+
usePasteHighchartsTheme({ ...options, plotOptions: { series: { animation: false } } }),
14+
);
15+
16+
React.useLayoutEffect(() => {
17+
setChartOptions(Highcharts.merge(chartOptions, options));
18+
}, [options]);
19+
20+
React.useEffect(() => {
21+
if (chartRef.current) {
22+
setChartRef(chartRef.current);
23+
}
24+
}, [chartRef.current]);
25+
26+
const callback = (chart: Highcharts.Chart): void => {
27+
if (chart?.series?.length > 0) {
28+
setChart(chart);
29+
}
30+
};
31+
32+
return (
33+
<Box gridArea="base-chart" ref={chartRef} position="relative">
34+
<HighchartsReact
35+
highcharts={Highcharts}
36+
options={chartOptions}
37+
constructorType={chartOptions.chart?.map ? "mapChart" : undefined}
38+
updateArgs={[true, true, false]}
39+
callback={callback}
40+
/>
41+
</Box>
42+
);
43+
};
44+
45+
export const BaseChart = React.memo(Chart);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { StoryFn } from "@storybook/react";
2+
import { Box } from "@twilio-paste/box";
3+
import { Button } from "@twilio-paste/button";
4+
import { Paragraph } from "@twilio-paste/paragraph";
5+
import { Theme } from "@twilio-paste/theme";
6+
import * as React from "react";
7+
8+
import { ChartContext, ChartProvider } from "../src";
9+
import { BaseChart } from "./BaseChart";
10+
11+
const lineSeries: Highcharts.SeriesLineOptions[] = [
12+
{
13+
name: "Installation",
14+
data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175],
15+
type: "line",
16+
},
17+
{
18+
name: "Manufacturing",
19+
data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434],
20+
type: "line",
21+
},
22+
{
23+
name: "Sales & Distribution",
24+
data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387],
25+
type: "line",
26+
},
27+
{
28+
name: "Project Development",
29+
data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227],
30+
type: "line",
31+
},
32+
{
33+
name: "Other",
34+
data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111],
35+
type: "line",
36+
},
37+
];
38+
39+
// eslint-disable-next-line import/no-default-export
40+
export default {
41+
title: "Components/ChartProvider",
42+
};
43+
44+
export const Default: StoryFn = () => {
45+
return (
46+
<ChartProvider highchartsOptions={{ chart: { type: "line" }, series: lineSeries }}>
47+
<BaseChart />
48+
</ChartProvider>
49+
);
50+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist/",
5+
},
6+
"include": [
7+
"src/**/*",
8+
],
9+
"exclude": [
10+
"node_modules"
11+
]
12+
}

0 commit comments

Comments
 (0)