Skip to content

Commit

Permalink
LG-4621: Add zooming to Chart (#2561)
Browse files Browse the repository at this point in the history
* Working zoom

* Update naming

* Update

* Update README

* Changeset

* Fix deps

* Fix storybook

* Fix tests

* Enable based on handler presence

* Fix README

* Fix type and feedback
  • Loading branch information
tsck authored Dec 4, 2024
1 parent 737f879 commit 2393c0c
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/pretty-waves-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lg-charts/core': minor
---

Adds zooming functionality to `Chart`
16 changes: 12 additions & 4 deletions charts/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ npm install @lg-charts/core
```js
import { Chart, Line, Grid, XAxis, YAxis } from '@lg-charts/core';

<Chart>
<Chart onZoomSelect={handleZoom}>
<Header title="My Chart" />
<Grid vertical={false}>
<XAxis type="time" />
Expand Down Expand Up @@ -84,9 +84,17 @@ Chart container component.

#### Props

| Name | Description | Type | Default |
| -------------- | ------------------------------------------------------- | ------------ | ------- |
| `onChartReady` | Callback to be called when chart is finished rendering. | `() => void` | |
| Name | Description | Type | Default |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------- |
| `onChartReady` | Callback to be called when chart is finished rendering. | `() => void` | |
| `onZoomSelect` _(optional)_ | Callback to be called when a user clicks and drags on a chart to zoom. Click and drag action will only be enabled if this handler is present. | `(ZoomSelectionEvent) => void` | |

```ts
ZoomSelectionEvent = {
xAxis: { startValue: number; endValue: number };
yAxis: { startValue: number; endValue: number };
}
```

## Child Components

Expand Down
1 change: 1 addition & 0 deletions charts/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@leafygreen-ui/icon": "^12.8.0",
"@leafygreen-ui/icon-button": "^15.0.23",
"@leafygreen-ui/lib": "13.8.0",
"@leafygreen-ui/palette": "^4.1.1",
"@leafygreen-ui/tokens": "^2.11.0",
"@leafygreen-ui/typography": "^19.3.0",
"@lg-tools/storybook-utils": "^0.1.1",
Expand Down
8 changes: 8 additions & 0 deletions charts/core/src/Chart.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ export default {
category: 'Header',
},
},
onZoomSelect: {
description: 'Zoom handler',
name: 'onZoomSelect',
table: {
category: 'Chart',
disable: true,
},
},
},
};

Expand Down
2 changes: 2 additions & 0 deletions charts/core/src/Chart/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function Chart({
children,
darkMode: darkModeProp,
onChartReady,
onZoomSelect,
className,
...rest
}: ChartProps) {
Expand All @@ -35,6 +36,7 @@ export function Chart({
} = useChart({
theme,
onChartReady,
onZoomSelect,
});

return (
Expand Down
21 changes: 17 additions & 4 deletions charts/core/src/Chart/Chart.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PropsWithChildren } from 'react';
import type { XAXisComponentOption, YAXisComponentOption } from 'echarts';
import type { LineSeriesOption } from 'echarts/charts';
import type {
Expand All @@ -12,6 +13,8 @@ import type { ComposeOption } from 'echarts/core';

import { DarkModeProps, type HTMLElementProps } from '@leafygreen-ui/lib';

import { ZoomSelectionEvent } from './hooks/useChart.types';

type RequiredSeriesProps = 'type' | 'name' | 'data';
export type SeriesOption = Pick<LineSeriesOption, RequiredSeriesProps> &
Partial<Omit<LineSeriesOption, RequiredSeriesProps>>;
Expand All @@ -33,10 +36,20 @@ export type ChartOptions = ComposeOption<
| YAXisComponentOption
> & { series?: Array<SeriesOption> };

export interface ChartProps extends HTMLElementProps<'div'>, DarkModeProps {
children?: React.ReactNode;
onChartReady?: () => void;
}
export type ChartProps = HTMLElementProps<'div'> &
DarkModeProps &
PropsWithChildren<{
/**
* Callback to be called when chart is finished rendering.
*/
onChartReady?: () => void;

/**
* Callback to be called when a user clicks and drags on a chart to zoom.
* Click and drag action will only be enabled if this handler is present.
*/
onZoomSelect?: (e: ZoomSelectionEvent) => void;
}>;

export const ChartActionType = {
addChartSeries: 'addChartSeries',
Expand Down
18 changes: 18 additions & 0 deletions charts/core/src/Chart/config/getDefaultChartOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Theme } from '@leafygreen-ui/lib';
import { palette } from '@leafygreen-ui/palette';
import {
color,
InteractionState,
Expand Down Expand Up @@ -72,4 +73,21 @@ export const getDefaultChartOptions = (
type: 'value',
...commonAxisOptions,
},

// Sets up zooming
toolbox: {
feature: {
dataZoom: {
show: true,
icon: {
zoom: 'path://', // hack to remove zoom button
back: 'path://', // hack to remove restore button
},
brushStyle: {
color: palette.gray.light1,
opacity: 0.2,
},
},
},
},
});
1 change: 1 addition & 0 deletions charts/core/src/Chart/hooks/useChart.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jest.mock('echarts/core', () => ({
setOption: jest.fn(),
dispose: jest.fn(),
resize: jest.fn(),
on: jest.fn(),
})),
use: jest.fn(),
}));
Expand Down
88 changes: 77 additions & 11 deletions charts/core/src/Chart/hooks/useChart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { LineChart as EchartsLineChart } from 'echarts/charts';
import {
DataZoomComponent,
DataZoomInsideComponent,
GridComponent,
LegendComponent,
TitleComponent,
Expand All @@ -11,13 +13,12 @@ import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import debounce from 'lodash.debounce';

import { Theme } from '@leafygreen-ui/lib';

import { ChartOptions, SeriesOption } from '../../Chart/Chart.types';
import { chartSeriesColors } from '../chartSeriesColors';
import { getDefaultChartOptions } from '../config';

import { addSeries, removeSeries, updateOptions } from './updateUtils';
import { ChartHookProps } from './useChart.types';

/**
* Register the required components. By using separate imports, we can avoid
Expand All @@ -32,24 +33,26 @@ echarts.use([
EchartsLineChart,
CanvasRenderer,
ToolboxComponent,
DataZoomComponent,
DataZoomInsideComponent,
]);

interface ChartHookProps {
onChartReady?: () => void;
theme: Theme;
}

/**
* Creates a generic Apache ECharts options object with default values for those not set
* that are in line with the designs and needs of the design system.
*/
export function useChart({ theme, onChartReady }: ChartHookProps) {
export function useChart({
theme,
onChartReady,
onZoomSelect,
}: ChartHookProps) {
const chartRef = useRef(null);
const chartInstanceRef = useRef<echarts.EChartsType | undefined>();
const [chartOptions, setChartOptions] = useState(
getDefaultChartOptions(theme),
);

// Initialize the chart
useEffect(() => {
const chartInstance = echarts.init(chartRef.current);
chartInstanceRef.current = chartInstance;
Expand All @@ -61,15 +64,78 @@ export function useChart({ theme, onChartReady }: ChartHookProps) {
};
window.addEventListener('resize', resizeHandler);

if (onChartReady) {
chartInstance.on('finished', onChartReady);
function onInitialRender() {
/**
* IMPORTANT NOTE: We use the presence of the handler to determine if the zoom click
* and drag is enabled or not. This is an exception! Typically we'd want a `zoomable` prop,
* or something similar to enable/disable, and a handler just to handle the action.
* We don't want to set a precedent with the pattern of implying functionality (like
* the click and drag action) based on the presence of a handler like this.
* This was deemed an exception to the rule due to the fact that the data is always
* controlled and there never exists a scenario where one would be useful without the other.
*/
if (onZoomSelect) {
/**
* Zooming is built into echart via the toolbar. By default, a user
* has to click the "dataZoom" button to enable zooming. We however hide
* this button and want it turned on by default. This is done by dispatching
* an action to enable the "dataZoomSelect" feature, as if it were clicked.
*/
chartInstance.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true,
});

chartInstance.on('dataZoom', (params: any) => {
if (params.batch) {
const xAxisIndex = 0;
const yAxisIndex = 1;

const { startValue: xStart, endValue: xEnd } =
params.batch[xAxisIndex];
const { startValue: yStart, endValue: yEnd } =
params.batch[yAxisIndex];

onZoomSelect({
xAxis: { startValue: xStart, endValue: xEnd },
yAxis: { startValue: yStart, endValue: yEnd },
});
}

/**
* If start is not 0% or end is not 100%, that means that the 'dataZoom'
* event was triggered by an actual zoom. Since we don't want to actually
* zoom on the current data, but rather provide the new values to the passed
* in handler, we dispatch an action to essentially override the zoom.
*/
const isZoomed = params?.start !== 0 || params?.end !== 100;

if (isZoomed) {
chartInstance.dispatchAction({
type: 'dataZoom',
start: 0, // percentage of starting position
end: 100, // percentage of ending position
});
}
});
}

if (onChartReady) {
onChartReady();
}

// Remove so it doesn't get called on every render
chartInstance.off('rendered', onInitialRender);
}

chartInstance.on('rendered', onInitialRender);

return () => {
window.removeEventListener('resize', resizeHandler);
chartInstance.dispose();
};
}, []);
}, [chartOptions, onChartReady, onZoomSelect]);

const updateChartRef = useMemo(
() =>
Expand Down
12 changes: 12 additions & 0 deletions charts/core/src/Chart/hooks/useChart.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Theme } from '@leafygreen-ui/lib';

export interface ZoomSelectionEvent {
xAxis: { startValue: number; endValue: number };
yAxis: { startValue: number; endValue: number };
}

export interface ChartHookProps {
onChartReady?: () => void;
onZoomSelect?: (e: ZoomSelectionEvent) => void;
theme: Theme;
}

0 comments on commit 2393c0c

Please sign in to comment.