Skip to content

Commit d5b3b48

Browse files
authored
feat: Theming - Charts (#1608)
Added ChartThemeProvider, added support for resolving css variables in chart theme, and updated chart theme to use css variables. resolves #1572 BREAKING CHANGE: - ChartThemeProvider is now required to provide ChartTheme - ChartModelFactory and ChartUtils now require chartTheme args
1 parent 35311c8 commit d5b3b48

34 files changed

Lines changed: 575 additions & 197 deletions

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app-utils/src/components/ThemeBootstrap.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useContext, useMemo } from 'react';
2+
import { ChartThemeProvider } from '@deephaven/chart';
23
import { ThemeProvider } from '@deephaven/components';
34
import { PluginsContext } from '@deephaven/plugin';
45
import { getThemeDataFromPlugins } from '../plugins';
@@ -19,7 +20,11 @@ export function ThemeBootstrap({ children }: ThemeBootstrapProps): JSX.Element {
1920
[pluginModules]
2021
);
2122

22-
return <ThemeProvider themes={themes}>{children}</ThemeProvider>;
23+
return (
24+
<ThemeProvider themes={themes}>
25+
<ChartThemeProvider>{children}</ChartThemeProvider>
26+
</ThemeProvider>
27+
);
2328
}
2429

2530
export default ThemeBootstrap;

packages/chart/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
"build:sass": "sass --embed-sources --load-path=../../node_modules ./src:./dist"
2828
},
2929
"dependencies": {
30+
"@deephaven/components": "file:../components",
3031
"@deephaven/icons": "file:../icons",
3132
"@deephaven/jsapi-types": "file:../jsapi-types",
3233
"@deephaven/jsapi-utils": "file:../jsapi-utils",
3334
"@deephaven/log": "file:../log",
35+
"@deephaven/react-hooks": "file:../react-hooks",
3436
"@deephaven/utils": "file:../utils",
3537
"deep-equal": "^2.0.5",
3638
"lodash.debounce": "^4.0.8",

packages/chart/src/Chart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
ModeBarButtonAny,
2929
} from 'plotly.js';
3030
import type { PlotParams } from 'react-plotly.js';
31-
import createPlotlyComponent from 'react-plotly.js/factory.js';
31+
import createPlotlyComponent from './plotly/createPlotlyComponent';
3232
import Plotly from './plotly/Plotly';
3333
import ChartModel from './ChartModel';
3434
import ChartUtils, { ChartModelSettings } from './ChartUtils';

packages/chart/src/ChartModelFactory.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import dh from '@deephaven/jsapi-shim';
2+
import { TestUtils } from '@deephaven/utils';
23
import ChartModelFactory from './ChartModelFactory';
4+
import type { ChartTheme } from './ChartTheme';
35
import FigureChartModel from './FigureChartModel';
46

7+
const { createMockProxy } = TestUtils;
8+
59
describe('creating model from metadata', () => {
610
it('handles loading a FigureChartModel from table settings', async () => {
711
const columns = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
812
// eslint-disable-next-line @typescript-eslint/no-explicit-any
913
const table = new (dh as any).Table({ columns });
1014
const settings = { series: ['C'], xAxis: 'name', type: 'PIE' as const };
15+
const chartTheme = createMockProxy<ChartTheme>();
1116
const model = await ChartModelFactory.makeModelFromSettings(
1217
dh,
1318
settings,
14-
table
19+
table,
20+
chartTheme
1521
);
1622

1723
expect(model).toBeInstanceOf(FigureChartModel);

packages/chart/src/ChartModelFactory.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { dh as DhType, Figure, Table } from '@deephaven/jsapi-types';
22
import ChartUtils, { ChartModelSettings } from './ChartUtils';
33
import FigureChartModel from './FigureChartModel';
4-
import ChartTheme from './ChartTheme';
4+
import { ChartTheme } from './ChartTheme';
55
import ChartModel from './ChartModel';
66

77
class ChartModelFactory {
@@ -16,7 +16,7 @@ class ChartModelFactory {
1616
* @param settings.xAxis The column name to use for the x-axis
1717
* @param [settings.hiddenSeries] Array of hidden series names
1818
* @param table The table to build the model for
19-
* @param theme The theme for the figure. Defaults to ChartTheme
19+
* @param theme The theme for the figure
2020
* @returns The ChartModel Promise representing the figure
2121
* CRA sets tsconfig to type check JS based on jsdoc comments. It isn't able to figure out FigureChartModel extends ChartModel
2222
* This causes TS issues in 1 or 2 spots. Once this is TS it can be returned to just FigureChartModel
@@ -25,14 +25,14 @@ class ChartModelFactory {
2525
dh: DhType,
2626
settings: ChartModelSettings,
2727
table: Table,
28-
theme = ChartTheme
28+
theme: ChartTheme
2929
): Promise<ChartModel> {
3030
const figure = await ChartModelFactory.makeFigureFromSettings(
3131
dh,
3232
settings,
3333
table
3434
);
35-
return new FigureChartModel(dh, figure, settings, theme);
35+
return new FigureChartModel(dh, figure, theme, settings);
3636
}
3737

3838
/**
@@ -78,7 +78,7 @@ class ChartModelFactory {
7878
* @param settings.xAxis The column name to use for the x-axis
7979
* @param [settings.hiddenSeries] Array of hidden series names
8080
* @param figure The figure to build the model for
81-
* @param theme The theme for the figure. Defaults to ChartTheme
81+
* @param theme The theme for the figure
8282
* @returns The FigureChartModel representing the figure
8383
* CRA sets tsconfig to type check JS based on jsdoc comments. It isn't able to figure out FigureChartModel extends ChartModel
8484
* This causes TS issues in 1 or 2 spots. Once this is TS it can be returned to just FigureChartModel
@@ -87,9 +87,9 @@ class ChartModelFactory {
8787
dh: DhType,
8888
settings: ChartModelSettings | undefined,
8989
figure: Figure,
90-
theme = ChartTheme
90+
theme: ChartTheme
9191
): Promise<ChartModel> {
92-
return new FigureChartModel(dh, figure, settings, theme);
92+
return new FigureChartModel(dh, figure, theme, settings);
9393
}
9494
}
9595

packages/chart/src/ChartTheme.module.scss

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
@import '@deephaven/components/scss/custom.scss';
33

44
:export {
5-
paper-bgcolor: $content-bg;
6-
plot-bgcolor: $gray-850;
7-
title-color: $white;
8-
colorway: $blue $green $yellow $purple $orange $red $white;
9-
gridcolor: $gray-700;
10-
linecolor: $gray-500;
11-
zerolinecolor: $gray-300;
12-
activecolor: $primary;
13-
rangebgcolor: rgba($gray-500, 0.7);
14-
area-color: $blue;
15-
trend-color: lighten($green, 20%);
16-
line-color: $green;
17-
error-band-line-color: lighten($green, 40%);
18-
error-band-fill-color: rgba(lighten($green, 20%), 0.1);
19-
ohlc-increasing: $green;
20-
ohlc-decreasing: $red;
5+
paper-bgcolor: var(--dh-color-chart-bg);
6+
plot-bgcolor: var(--dh-color-chart-plot-bg);
7+
title-color: var(--dh-color-chart-title);
8+
colorway: var(--dh-color-chart-colorway);
9+
gridcolor: var(--dh-color-chart-grid);
10+
linecolor: var(--dh-color-chart-axis-line);
11+
zerolinecolor: var(--dh-color-chart-axis-line-zero);
12+
activecolor: var(--dh-color-chart-active);
13+
rangebgcolor: var(--dh-color-chart-range-bg);
14+
area-color: var(--dh-color-chart-area);
15+
trend-color: var(--dh-color-chart-trend);
16+
line-color: var(--dh-color-chart-line-deprecated);
17+
error-band-line-color: var(--dh-color-chart-error-band-line);
18+
error-band-fill-color: var(--dh-color-chart-error-band-fill);
19+
ohlc-increasing: var(--dh-color-chart-ohlc-increase);
20+
ohlc-decreasing: var(--dh-color-chart-ohlc-decrease);
2121
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/// <reference types="./declaration" />
2+
3+
import { TestUtils } from '@deephaven/utils';
4+
import { resolveCssVariablesInRecord } from '@deephaven/components';
5+
import { defaultChartTheme } from './ChartTheme';
6+
import chartThemeRaw from './ChartTheme.module.scss';
7+
8+
jest.mock('@deephaven/components', () => ({
9+
...jest.requireActual('@deephaven/components'),
10+
resolveCssVariablesInRecord: jest.fn(),
11+
}));
12+
13+
const { asMock } = TestUtils;
14+
15+
const mockChartTheme = new Proxy(
16+
{},
17+
{ get: (_target, name) => `chartTheme['${String(name)}']` }
18+
);
19+
20+
beforeEach(() => {
21+
jest.clearAllMocks();
22+
expect.hasAssertions();
23+
24+
asMock(resolveCssVariablesInRecord)
25+
.mockName('resolveCssVariablesInRecord')
26+
.mockReturnValue(mockChartTheme);
27+
});
28+
29+
describe('defaultChartTheme', () => {
30+
it('should create the default chart theme', () => {
31+
const actual = defaultChartTheme();
32+
33+
expect(resolveCssVariablesInRecord).toHaveBeenCalledWith(chartThemeRaw);
34+
expect(actual).toMatchSnapshot();
35+
});
36+
});

packages/chart/src/ChartTheme.ts

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,67 @@
1-
import ChartTheme from './ChartTheme.module.scss';
1+
import {
2+
getExpressionRanges,
3+
resolveCssVariablesInRecord,
4+
} from '@deephaven/components';
5+
import Log from '@deephaven/log';
6+
import { ColorUtils } from '@deephaven/utils';
7+
import chartThemeRaw from './ChartTheme.module.scss';
28

3-
export default Object.freeze({
4-
paper_bgcolor: ChartTheme['paper-bgcolor'],
5-
plot_bgcolor: ChartTheme['plot-bgcolor'],
6-
title_color: ChartTheme['title-color'],
7-
colorway: ChartTheme.colorway,
8-
gridcolor: ChartTheme.gridcolor,
9-
linecolor: ChartTheme.linecolor,
10-
zerolinecolor: ChartTheme.zerolinecolor,
11-
activecolor: ChartTheme.activecolor,
12-
rangebgcolor: ChartTheme.rangebgcolor,
13-
area_color: ChartTheme['area-color'],
14-
trend_color: ChartTheme['trend-color'],
15-
line_color: ChartTheme['line-color'],
16-
error_band_line_color: ChartTheme['error-band-line-color'],
17-
error_band_fill_color: ChartTheme['error-band-fill-color'],
18-
ohlc_increasing: ChartTheme['ohlc-increasing'],
19-
ohlc_decreasing: ChartTheme['ohlc-decreasing'],
20-
});
9+
const log = Log.module('ChartTheme');
10+
11+
export interface ChartTheme {
12+
paper_bgcolor: string;
13+
plot_bgcolor: string;
14+
title_color: string;
15+
colorway: string;
16+
gridcolor: string;
17+
linecolor: string;
18+
zerolinecolor: string;
19+
activecolor: string;
20+
rangebgcolor: string;
21+
area_color: string;
22+
trend_color: string;
23+
line_color: string;
24+
error_band_line_color: string;
25+
error_band_fill_color: string;
26+
ohlc_increasing: string;
27+
ohlc_decreasing: string;
28+
}
29+
30+
export function defaultChartTheme(): Readonly<ChartTheme> {
31+
const chartTheme = resolveCssVariablesInRecord(chartThemeRaw);
32+
33+
// The color normalization in `resolveCssVariablesInRecord` won't work for
34+
// colorway since it is an array of colors. We need to explicitly normalize
35+
// each color expression
36+
chartTheme.colorway = getExpressionRanges(chartTheme.colorway ?? '')
37+
.map(([start, end]) =>
38+
ColorUtils.normalizeCssColor(
39+
chartTheme.colorway.substring(start, end + 1)
40+
)
41+
)
42+
.join(' ');
43+
44+
log.debug2('Chart theme:', chartThemeRaw);
45+
log.debug2('Chart theme derived:', chartTheme);
46+
47+
return Object.freeze({
48+
paper_bgcolor: chartTheme['paper-bgcolor'],
49+
plot_bgcolor: chartTheme['plot-bgcolor'],
50+
title_color: chartTheme['title-color'],
51+
colorway: chartTheme.colorway,
52+
gridcolor: chartTheme.gridcolor,
53+
linecolor: chartTheme.linecolor,
54+
zerolinecolor: chartTheme.zerolinecolor,
55+
activecolor: chartTheme.activecolor,
56+
rangebgcolor: chartTheme.rangebgcolor,
57+
area_color: chartTheme['area-color'],
58+
trend_color: chartTheme['trend-color'],
59+
line_color: chartTheme['line-color'],
60+
error_band_line_color: chartTheme['error-band-line-color'],
61+
error_band_fill_color: chartTheme['error-band-fill-color'],
62+
ohlc_increasing: chartTheme['ohlc-increasing'],
63+
ohlc_decreasing: chartTheme['ohlc-decreasing'],
64+
});
65+
}
66+
67+
export default defaultChartTheme;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createContext, ReactNode, useEffect, useState } from 'react';
2+
import { useTheme } from '@deephaven/components';
3+
import defaultChartTheme, { ChartTheme } from './ChartTheme';
4+
5+
export type ChartThemeContextValue = ChartTheme;
6+
7+
export const ChartThemeContext = createContext<ChartThemeContextValue | null>(
8+
null
9+
);
10+
11+
export interface ChartThemeProviderProps {
12+
children: ReactNode;
13+
}
14+
15+
/*
16+
* Provides a chart theme based on the active themes from the ThemeProvider.
17+
*/
18+
export function ChartThemeProvider({
19+
children,
20+
}: ChartThemeProviderProps): JSX.Element {
21+
const { activeThemes } = useTheme();
22+
23+
const [chartTheme, setChartTheme] = useState<ChartTheme | null>(null);
24+
25+
// The `ThemeProvider` that supplies `activeThemes` also provides the corresponding
26+
// CSS theme variables to the DOM by dynamically rendering <style> tags whenever
27+
// the `activeThemes` change. Painting the latest CSS variables to the DOM may
28+
// not happen until after `ChartThemeProvider` is rendered, but they should be
29+
// available by the time the effect runs. Therefore, it is important to derive
30+
// the chart theme in an effect instead of deriving in a `useMemo` to ensure
31+
// we have the latest CSS variables.
32+
useEffect(() => {
33+
if (activeThemes != null) {
34+
setChartTheme(defaultChartTheme());
35+
}
36+
}, [activeThemes]);
37+
38+
return (
39+
<ChartThemeContext.Provider value={chartTheme}>
40+
{children}
41+
</ChartThemeContext.Provider>
42+
);
43+
}

0 commit comments

Comments
 (0)