diff --git a/.changeset/quick-needles-burn.md b/.changeset/quick-needles-burn.md new file mode 100644 index 0000000000..20b4463590 --- /dev/null +++ b/.changeset/quick-needles-burn.md @@ -0,0 +1,7 @@ +--- +'@lg-charts/core': patch +'@lg-charts/drag-provider': patch +--- + +introduce `Series` abstraction as a superclass of `Line` this allows supporting more diverse series +types such as `Bar` in a follow up PR diff --git a/.gitignore b/.gitignore index 2b464ab645..45c2c0456c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# IDE files +.idea +*.iml + # Locally-installed dependencies node_modules/ diff --git a/charts/core/src/Chart.stories.tsx b/charts/core/src/Chart.stories.tsx index b9aca06458..541238d6ed 100644 --- a/charts/core/src/Chart.stories.tsx +++ b/charts/core/src/Chart.stories.tsx @@ -5,7 +5,7 @@ import type { StoryObj } from '@storybook/react'; import { ChartProps } from './Chart/Chart.types'; import { ChartHeaderProps } from './ChartHeader/ChartHeader.types'; import { ChartTooltipProps } from './ChartTooltip/ChartTooltip.types'; -import { LineProps } from './Line'; +import { LineProps } from './Series'; import { makeLineData } from './testUtils'; import { ThresholdLineProps } from './ThresholdLine'; import { diff --git a/charts/core/src/Echart/Echart.types.ts b/charts/core/src/Echart/Echart.types.ts index a617779857..ee3b48a961 100644 --- a/charts/core/src/Echart/Echart.types.ts +++ b/charts/core/src/Echart/Echart.types.ts @@ -1,5 +1,4 @@ import type { XAXisComponentOption, YAXisComponentOption } from 'echarts'; -import type { LineSeriesOption } from 'echarts/charts'; import type { DatasetComponentOption, GridComponentOption, @@ -9,16 +8,45 @@ import type { TooltipComponentOption, } from 'echarts/components'; import type { ComposeOption, EChartsType } from 'echarts/core'; +import { + BarSeriesOption, + LineSeriesOption, + SeriesOption, +} from 'echarts/types/dist/shared'; -import { Theme } from '@leafygreen-ui/lib'; +import { Theme, ValuesOf } from '@leafygreen-ui/lib'; // Type not exported by echarts. // reference: https://github.com/apache/echarts/blob/master/src/coord/axisCommonTypes.ts#L193 export type AxisLabelValueFormatter = (value: number, index?: number) => string; -type RequiredSeriesProps = 'type' | 'name' | 'data'; -export type EChartSeriesOption = Pick & - Partial>; +export interface StylingContext { + seriesColor?: string; +} + +// to convert an SeriesOption type of echarts into a structured form more aligned with LeafyGreen design standards, +// where the 'type', 'name', and 'data' fields are explicitly required and typed, +// and all additional series properties encapsulated within the 'options' object +interface DisciplinedSeriesOption { + type: NonNullable; + name: string; + data: NonNullable; + options: Omit; +} + +// all supported series options types disciplined and grouped into a single interface +export interface EChartSeriesOptions { + line: DisciplinedSeriesOption; + // TODO: to be leveraged in a follow-up PR to add Bar chart support + bar: DisciplinedSeriesOption; +} + +// a disciplined substitute for SeriesOption type of echarts limited to the ones supported here +export type EChartSeriesOption = Omit< + ValuesOf, + 'options' +> & + ValuesOf['options']; /** * TODO: This might need to be improved. `ComposeOption` appears to make most base option @@ -108,6 +136,7 @@ interface EChartsEventHandlerType { callback: (params: any) => void, options?: Partial<{ useCanvasAsTrigger: boolean }>, ): void; + ( event: 'zoomselect', callback: (params: EChartZoomSelectionEvent) => void, diff --git a/charts/core/src/Echart/utils/updateUtils.spec.ts b/charts/core/src/Echart/utils/updateUtils.spec.ts index b5594ad06e..0b9b44609c 100644 --- a/charts/core/src/Echart/utils/updateUtils.spec.ts +++ b/charts/core/src/Echart/utils/updateUtils.spec.ts @@ -1,14 +1,18 @@ -import { EChartOptions } from '../Echart.types'; +import { EChartOptions, EChartSeriesOption } from '../Echart.types'; import { addSeries, removeSeries, updateOptions } from './updateUtils'; describe('@lg-charts/core/Chart/hooks/updateUtils', () => { test('addSeries should add a series to the chart options', () => { const currentOptions: Partial = { - series: [{ name: 'series1' }], + series: [{ type: 'line', name: 'series1', data: [] }], }; const newSeriesName = 'series2'; - const data = { name: newSeriesName }; + const data: EChartSeriesOption = { + type: 'line', + name: newSeriesName, + data: [], + }; const updatedOptions = addSeries(currentOptions, data); expect(updatedOptions.series).toHaveLength(2); expect(updatedOptions.series?.[1].name).toBe(newSeriesName); @@ -16,10 +20,14 @@ describe('@lg-charts/core/Chart/hooks/updateUtils', () => { test('addSeries should not add a series if a chart with the same name exists', () => { const currentOptions: Partial = { - series: [{ name: 'series1' }], + series: [{ type: 'line', name: 'series1', data: [] }], }; const newSeriesName = 'series1'; - const data = { name: newSeriesName }; + const data: EChartSeriesOption = { + type: 'line', + name: newSeriesName, + data: [], + }; const updatedOptions = addSeries(currentOptions, data); expect(updatedOptions.series).toHaveLength(1); expect(updatedOptions.series?.[0].name).toBe(newSeriesName); @@ -27,7 +35,10 @@ describe('@lg-charts/core/Chart/hooks/updateUtils', () => { test('removeSeries should remove a series from the chart options', () => { const currentOptions: Partial = { - series: [{ name: 'series1' }, { name: 'series2' }], + series: [ + { type: 'line', name: 'series1', data: [] }, + { type: 'line', name: 'series2', data: [] }, + ], }; const seriesName1 = 'series1'; const seriesName2 = 'series2'; diff --git a/charts/core/src/EventMarkers/BaseEventMarker/utils.ts b/charts/core/src/EventMarkers/BaseEventMarker/utils.ts index ec046d2e36..38905071d3 100644 --- a/charts/core/src/EventMarkers/BaseEventMarker/utils.ts +++ b/charts/core/src/EventMarkers/BaseEventMarker/utils.ts @@ -109,6 +109,7 @@ export function getMarkConfig({ symbolSize: [16, 16], symbolRotate: 360, // Icon shows upside down without this }, + data: [], } as SeriesOption; } else { return { @@ -130,6 +131,7 @@ export function getMarkConfig({ symbolSize: [16, 16], symbol: generateSymbolDataUri(level, theme), }, + data: [], } as SeriesOption; } } diff --git a/charts/core/src/Line/Line.tsx b/charts/core/src/Line/Line.tsx deleted file mode 100644 index 3ca6124986..0000000000 --- a/charts/core/src/Line/Line.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useEffect } from 'react'; -import { useSeriesContext } from '@lg-charts/series-provider'; - -import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; - -import { useChartContext } from '../ChartContext'; - -import { defaultLineOptions } from './config'; -import { LineProps } from './Line.types'; - -export function Line({ name, data }: LineProps) { - const { theme } = useDarkMode(); - const { - chart: { addSeries, ready, removeSeries }, - } = useChartContext(); - const { getColor, isChecked } = useSeriesContext(); - - const color = getColor(name, theme); - const isVisible = isChecked(name); - - useEffect(() => { - if (!ready) return; - - if (isVisible) { - addSeries({ - ...defaultLineOptions, - name, - data, - lineStyle: { - ...defaultLineOptions.lineStyle, - color: color || undefined, - }, - itemStyle: { - ...defaultLineOptions.itemStyle, - color: color || undefined, - }, - }); - } else { - removeSeries(name); - } - - return () => { - /** - * Remove the series when the component unmounts to make sure the series - * is removed when a `Line` is hidden. - */ - removeSeries(name); - }; - }, [addSeries, color, data, isVisible, name, ready, removeSeries, theme]); - - return null; -} - -Line.displayName = 'Line'; diff --git a/charts/core/src/Line/config/defaultLineOptions.ts b/charts/core/src/Line/config/defaultLineOptions.ts deleted file mode 100644 index 7780c10c3c..0000000000 --- a/charts/core/src/Line/config/defaultLineOptions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SeriesOption } from '../../Chart'; - -export const defaultLineOptions: Omit = { - type: 'line', - showSymbol: false, - symbol: 'circle', - clip: false, - symbolSize: 7, - emphasis: { - focus: 'series', - }, - blur: { - lineStyle: { - opacity: 0.5, - }, - }, - lineStyle: { - width: 1, - }, -}; diff --git a/charts/core/src/Line/config/index.ts b/charts/core/src/Line/config/index.ts deleted file mode 100644 index 82362b422f..0000000000 --- a/charts/core/src/Line/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { defaultLineOptions } from './defaultLineOptions'; diff --git a/charts/core/src/Line/index.ts b/charts/core/src/Line/index.ts deleted file mode 100644 index 89ce84c147..0000000000 --- a/charts/core/src/Line/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Line } from './Line'; -export type { LineProps } from './Line.types'; diff --git a/charts/core/src/Series/Line/Line.tsx b/charts/core/src/Series/Line/Line.tsx new file mode 100644 index 0000000000..533f30b9db --- /dev/null +++ b/charts/core/src/Series/Line/Line.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +import { EChartSeriesOptions, StylingContext } from '../../Echart/Echart.types'; +import { Series } from '../Series'; +import { SeriesProps } from '../Series.types'; + +export type LineProps = SeriesProps; + +function getDefaultLineOptions( + stylingContext: StylingContext, +): EChartSeriesOptions['line']['options'] { + return { + showSymbol: false, + symbol: 'circle', + clip: false, + symbolSize: 7, + emphasis: { + focus: 'series', + }, + blur: { + lineStyle: { + opacity: 0.5, + }, + }, + itemStyle: { + color: stylingContext.seriesColor, + }, + lineStyle: { + color: stylingContext.seriesColor, + width: 1, + }, + }; +} + +export const Line = (props: LineProps) => ( + +); + +Line.displayName = 'Line'; diff --git a/charts/core/src/Series/Line/index.ts b/charts/core/src/Series/Line/index.ts new file mode 100644 index 0000000000..853757d481 --- /dev/null +++ b/charts/core/src/Series/Line/index.ts @@ -0,0 +1,2 @@ +export type { LineProps } from './Line'; +export { Line } from './Line'; diff --git a/charts/core/src/Series/Series.tsx b/charts/core/src/Series/Series.tsx new file mode 100644 index 0000000000..5bd10e7752 --- /dev/null +++ b/charts/core/src/Series/Series.tsx @@ -0,0 +1,66 @@ +import { useEffect } from 'react'; +import { useSeriesContext } from '@lg-charts/series-provider'; + +import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; +import { ValuesOf } from '@leafygreen-ui/lib'; + +import { useChartContext } from '../ChartContext'; +import { EChartSeriesOptions, StylingContext } from '../Echart/Echart.types'; + +export function Series>({ + type, + name, + data, + options, +}: { + type: T['type']; + name: T['name']; + data: T['data']; + options: (ctx: StylingContext) => T['options']; +}) { + const { + chart: { addSeries, ready, removeSeries }, + } = useChartContext(); + const { theme } = useDarkMode(); + const { isChecked, getColor } = useSeriesContext(); + const seriesColor = getColor(name, theme) || undefined; + const isVisible = isChecked(name); + + useEffect(() => { + if (!ready) return; + + if (isVisible) { + const context = { seriesColor }; + addSeries({ + type, + name, + data, + ...options(context), + }); + } else { + removeSeries(name); + } + + return () => { + /** + * Remove the series when the component unmounts to make sure the series + * is removed when a `Series` is hidden. + */ + removeSeries(name); + }; + }, [ + addSeries, + isVisible, + seriesColor, + ready, + removeSeries, + type, + name, + data, + options, + ]); + + return null; +} + +Series.displayName = 'Series'; diff --git a/charts/core/src/Line/Line.types.ts b/charts/core/src/Series/Series.types.ts similarity index 96% rename from charts/core/src/Line/Line.types.ts rename to charts/core/src/Series/Series.types.ts index 1bf6c5105c..07c73866f0 100644 --- a/charts/core/src/Line/Line.types.ts +++ b/charts/core/src/Series/Series.types.ts @@ -3,7 +3,7 @@ import { SeriesName } from '@lg-charts/series-provider'; type XValue = string | number | Date | null | undefined; type YValue = string | number | Date | null | undefined; -export interface LineProps { +export interface SeriesProps { /** * Series name used for displaying in tooltip and filtering with the legend. */ diff --git a/charts/core/src/Series/index.ts b/charts/core/src/Series/index.ts new file mode 100644 index 0000000000..e5a6432e3e --- /dev/null +++ b/charts/core/src/Series/index.ts @@ -0,0 +1,2 @@ +export { Line, type LineProps } from './Line'; +export { Series } from './Series'; diff --git a/charts/core/src/ThresholdLine/ThresholdLine.tsx b/charts/core/src/ThresholdLine/ThresholdLine.tsx index 7e34b499fe..18677f6836 100644 --- a/charts/core/src/ThresholdLine/ThresholdLine.tsx +++ b/charts/core/src/ThresholdLine/ThresholdLine.tsx @@ -88,6 +88,7 @@ function getThresholdLineConfig({ symbolSize: [7, 10], symbolRotate: 360, }, + data: [], }; } diff --git a/charts/core/src/index.ts b/charts/core/src/index.ts index 4c5c915d35..0a60f617f5 100644 --- a/charts/core/src/index.ts +++ b/charts/core/src/index.ts @@ -17,7 +17,7 @@ export { EventMarkerPoint, type EventMarkerPointProps, } from './EventMarkers'; -export { Line, type LineProps } from './Line'; +export { Line, type LineProps } from './Series'; export { ThresholdLine, type ThresholdLineProps } from './ThresholdLine'; export { XAxis, type XAxisProps, type XAxisType } from './XAxis'; export { YAxis, type YAxisProps, type YAxisType } from './YAxis'; diff --git a/charts/core/src/testUtils/makeLineData.testUtils.ts b/charts/core/src/testUtils/makeLineData.testUtils.ts index 3632e4042b..94edcdac6f 100644 --- a/charts/core/src/testUtils/makeLineData.testUtils.ts +++ b/charts/core/src/testUtils/makeLineData.testUtils.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; -import { LineProps } from '../Line/Line.types'; +import { LineProps } from '../Series'; /** * Generates consistent but realistic-looking test data for storybook