Skip to content

Commit fabb055

Browse files
authored
feat: Chart responsible for its own theme (#1772)
This PR - Move theme colors down to `layout.template.layout` so that they are used as defaults. Some of the theme colors had to be set in `layout.template.data` for specific trace types - Chart component now responds to ChartTheme context changes (this should enable chart theme switching, but it depends on some follow up plugins repo work to complete it). **Testing** For now, we should just make sure charts behave as they did before. Not all charts will update in response to a theme change, but re-running queries should show the theme change. I have tested this and have not seen any changes to existing behavior introduced by this PR Once the following plugins repo PRs have merged, the chart should fully respond to theme changes: deephaven/deephaven-plugins#243 deephaven/deephaven-plugins#251 resolves #1728 BREAKING CHANGE: - Renamed `ColorUtils.getColorwayFromTheme` to `normalizeColorway` - Removed `chartTheme` arg from functions in `ChartUtils`, `ChartModelFactory` and `FigureChartModel` in @deephaven/chart
1 parent eddd160 commit fabb055

21 files changed

Lines changed: 533 additions & 194 deletions

packages/chart/src/Chart.tsx

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ import {
2929
ModeBarButtonAny,
3030
} from 'plotly.js';
3131
import type { PlotParams } from 'react-plotly.js';
32+
import { bindAllMethods } from '@deephaven/utils';
3233
import createPlotlyComponent from './plotly/createPlotlyComponent';
3334
import Plotly from './plotly/Plotly';
3435
import ChartModel from './ChartModel';
36+
import { ChartTheme } from './ChartTheme';
3537
import ChartUtils, { ChartModelSettings } from './ChartUtils';
3638
import './Chart.scss';
3739
import DownsamplingError from './DownsamplingError';
40+
import useChartTheme from './useChartTheme';
3841

3942
const log = Log.module('Chart');
4043

@@ -46,6 +49,7 @@ type FormatterSettings = ColumnFormatSettings &
4649

4750
interface ChartProps {
4851
model: ChartModel;
52+
theme: ChartTheme;
4953
settings: FormatterSettings;
5054
isActive: boolean;
5155
Plotly: typeof Plotly;
@@ -57,6 +61,12 @@ interface ChartProps {
5761
onSettingsChanged: (settings: Partial<ChartModelSettings>) => void;
5862
}
5963

64+
// All of the ChartProps have default values except for model in the Chart
65+
// component, hence the Partial here.
66+
interface ChartContainerProps extends Partial<Omit<ChartProps, 'theme'>> {
67+
model: ChartModel;
68+
}
69+
6070
interface ChartState {
6171
data: Partial<Data>[] | null;
6272
/** An error specific to downsampling */
@@ -72,7 +82,7 @@ interface ChartState {
7282
revision: number;
7383
}
7484

75-
export class Chart extends Component<ChartProps, ChartState> {
85+
class Chart extends Component<ChartProps, ChartState> {
7686
static defaultProps = {
7787
isActive: true,
7888
settings: {
@@ -135,14 +145,7 @@ export class Chart extends Component<ChartProps, ChartState> {
135145
constructor(props: ChartProps) {
136146
super(props);
137147

138-
this.handleAfterPlot = this.handleAfterPlot.bind(this);
139-
this.handleDownsampleClick = this.handleDownsampleClick.bind(this);
140-
this.handleErrorClose = this.handleErrorClose.bind(this);
141-
this.handleModelEvent = this.handleModelEvent.bind(this);
142-
this.handlePlotUpdate = this.handlePlotUpdate.bind(this);
143-
this.handleRelayout = this.handleRelayout.bind(this);
144-
this.handleResize = this.handleResize.bind(this);
145-
this.handleRestyle = this.handleRestyle.bind(this);
148+
bindAllMethods(this);
146149

147150
this.PlotComponent = createPlotlyComponent(props.Plotly);
148151
this.plot = React.createRef();
@@ -186,10 +189,12 @@ export class Chart extends Component<ChartProps, ChartState> {
186189
if (this.plotWrapper.current != null) {
187190
this.resizeObserver.observe(this.plotWrapper.current);
188191
}
192+
193+
this.handleThemeChange();
189194
}
190195

191196
componentDidUpdate(prevProps: ChartProps): void {
192-
const { isActive, model, settings } = this.props;
197+
const { isActive, model, settings, theme } = this.props;
193198
this.updateFormatterSettings(settings as FormatterSettings);
194199

195200
if (model !== prevProps.model) {
@@ -205,6 +210,10 @@ export class Chart extends Component<ChartProps, ChartState> {
205210
this.unsubscribe(model);
206211
}
207212
}
213+
214+
if (theme !== prevProps.theme) {
215+
this.handleThemeChange();
216+
}
208217
}
209218

210219
componentWillUnmount(): void {
@@ -348,14 +357,14 @@ export class Chart extends Component<ChartProps, ChartState> {
348357

349358
initData(): void {
350359
const { model } = this.props;
351-
const { layout } = this.state;
352-
this.setState({
360+
361+
this.setState(({ layout }) => ({
353362
data: model.getData(),
354363
layout: {
355364
...layout,
356365
...model.getLayout(),
357366
},
358-
});
367+
}));
359368
}
360369

361370
subscribe(model: ChartModel): void {
@@ -539,6 +548,19 @@ export class Chart extends Component<ChartProps, ChartState> {
539548
}
540549
}
541550

551+
handleThemeChange(): void {
552+
const { theme, model } = this.props;
553+
const { dh } = model;
554+
const chartUtils = new ChartUtils(dh);
555+
556+
this.setState(({ layout }) => ({
557+
layout: {
558+
...layout,
559+
template: chartUtils.makeDefaultTemplate(theme),
560+
},
561+
}));
562+
}
563+
542564
/**
543565
* Toggle the error message. If it is already being displayed, then hide it.
544566
*/
@@ -661,6 +683,7 @@ export class Chart extends Component<ChartProps, ChartState> {
661683
error
662684
);
663685
const isPlotShown = data != null;
686+
664687
return (
665688
<div className="h-100 w-100 chart-wrapper" ref={this.plotWrapper}>
666689
{isPlotShown && (
@@ -703,4 +726,10 @@ export class Chart extends Component<ChartProps, ChartState> {
703726
}
704727
}
705728

706-
export default Chart;
729+
export default function ChartContainer(
730+
props: ChartContainerProps
731+
): JSX.Element {
732+
const chartTheme = useChartTheme();
733+
// eslint-disable-next-line react/jsx-props-no-spreading
734+
return <Chart {...props} theme={chartTheme} />;
735+
}

packages/chart/src/ChartModelFactory.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
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';
54
import ChartModel from './ChartModel';
65

76
class ChartModelFactory {
@@ -16,23 +15,21 @@ class ChartModelFactory {
1615
* @param settings.xAxis The column name to use for the x-axis
1716
* @param [settings.hiddenSeries] Array of hidden series names
1817
* @param table The table to build the model for
19-
* @param theme The theme for the figure
2018
* @returns The ChartModel Promise representing the figure
2119
* CRA sets tsconfig to type check JS based on jsdoc comments. It isn't able to figure out FigureChartModel extends ChartModel
2220
* This causes TS issues in 1 or 2 spots. Once this is TS it can be returned to just FigureChartModel
2321
*/
2422
static async makeModelFromSettings(
2523
dh: DhType,
2624
settings: ChartModelSettings,
27-
table: Table,
28-
theme: ChartTheme
25+
table: Table
2926
): Promise<ChartModel> {
3027
const figure = await ChartModelFactory.makeFigureFromSettings(
3128
dh,
3229
settings,
3330
table
3431
);
35-
return new FigureChartModel(dh, figure, theme, settings);
32+
return new FigureChartModel(dh, figure, settings);
3633
}
3734

3835
/**
@@ -78,18 +75,16 @@ class ChartModelFactory {
7875
* @param settings.xAxis The column name to use for the x-axis
7976
* @param [settings.hiddenSeries] Array of hidden series names
8077
* @param figure The figure to build the model for
81-
* @param theme The theme for the figure
8278
* @returns The FigureChartModel representing the figure
8379
* CRA sets tsconfig to type check JS based on jsdoc comments. It isn't able to figure out FigureChartModel extends ChartModel
8480
* This causes TS issues in 1 or 2 spots. Once this is TS it can be returned to just FigureChartModel
8581
*/
8682
static async makeModel(
8783
dh: DhType,
8884
settings: ChartModelSettings | undefined,
89-
figure: Figure,
90-
theme: ChartTheme
85+
figure: Figure
9186
): Promise<ChartModel> {
92-
return new FigureChartModel(dh, figure, theme, settings);
87+
return new FigureChartModel(dh, figure, settings);
9388
}
9489
}
9590

packages/chart/src/ChartTestUtils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
Figure,
99
Series,
1010
SeriesDataSource,
11+
SeriesPlotStyle,
1112
} from '@deephaven/jsapi-types';
1213

1314
class ChartTestUtils {
@@ -84,6 +85,12 @@ class ChartTestUtils {
8485
sources = this.makeDefaultSources(),
8586
lineColor = null,
8687
shapeColor = null,
88+
}: {
89+
name?: string | null;
90+
lineColor?: string | null;
91+
plotStyle?: SeriesPlotStyle | null;
92+
shapeColor?: string | null;
93+
sources?: SeriesDataSource[];
8794
} = {}): Series {
8895
const { dh } = this;
8996
// eslint-disable-next-line @typescript-eslint/no-explicit-any

packages/chart/src/ChartUtils.test.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import dh from '@deephaven/jsapi-shim';
22
import { Formatter } from '@deephaven/jsapi-utils';
3-
import { TestUtils } from '@deephaven/utils';
43
import { Layout } from 'plotly.js';
54
import ChartUtils from './ChartUtils';
65
import ChartTestUtils from './ChartTestUtils';
@@ -9,16 +8,29 @@ import type { ChartTheme } from './ChartTheme';
98
const chartUtils = new ChartUtils(dh);
109
const chartTestUtils = new ChartTestUtils(dh);
1110

12-
const { createMockProxy } = TestUtils;
13-
1411
function makeFormatter() {
1512
return new Formatter(dh);
1613
}
1714

18-
const chartTheme = createMockProxy<ChartTheme>();
15+
// Proxy for ChartTheme
16+
const chartTheme = new Proxy(
17+
{},
18+
{
19+
get(_target, name) {
20+
if (name === 'colorway') {
21+
return 'ChartTheme[colorway0] ChartTheme[colorway1] ChartTheme[colorway2]';
22+
}
23+
24+
return `ChartTheme['${String(name)}']`;
25+
},
26+
}
27+
) as ChartTheme;
1928

2029
it('groups the axes by type properly', () => {
21-
const testAxes = (axes, expectedResult) => {
30+
const testAxes = (
31+
axes: { type: string; label: string }[],
32+
expectedResult
33+
) => {
2234
const chart = { axes };
2335
const result = ChartUtils.groupArray(chart.axes, 'type');
2436
expect(result).toEqual(expectedResult);
@@ -199,7 +211,7 @@ describe('updating layout axes', () => {
199211
it('adds new axes', () => {
200212
const layout = {};
201213
const axes = makeTwinAxes();
202-
chartUtils.updateLayoutAxes(layout, axes, axes, chartTheme);
214+
chartUtils.updateLayoutAxes(layout, axes, axes);
203215
expect(layout).toEqual(
204216
expect.objectContaining({
205217
xaxis: expect.objectContaining({
@@ -221,22 +233,26 @@ describe('updating layout axes', () => {
221233
it('keeps the same axis objects, updates domain', () => {
222234
const layout: Partial<Layout> = {};
223235
const axes = makeTwinAxes();
224-
chartUtils.updateLayoutAxes(layout, axes, axes, chartTheme, 10);
236+
chartUtils.updateLayoutAxes(layout, axes, axes, 10);
225237

226238
const { xaxis } = layout;
227239
const xDomain = [...(xaxis?.domain ?? [])];
228-
chartUtils.updateLayoutAxes(layout, axes, axes, chartTheme, 1000);
240+
chartUtils.updateLayoutAxes(layout, axes, axes, 1000);
229241

230242
expect(layout.xaxis).toBe(xaxis);
231243
expect(xDomain).not.toBe(xaxis?.domain);
232244
});
233245

234246
it('removes stale axes', () => {
235-
const layout = {};
247+
const layout = {
248+
xaxis: {},
249+
yaxis: {},
250+
yaxis2: {},
251+
};
236252
const axes = makeTwinAxes();
237253
const chart = chartTestUtils.makeChart({ axes });
238254
const figure = chartTestUtils.makeFigure({ charts: [chart] });
239-
chartUtils.updateFigureAxes(layout, figure, chartTheme);
255+
chartUtils.updateFigureAxes(layout, figure);
240256
expect(layout).toEqual(
241257
expect.objectContaining({
242258
xaxis: expect.objectContaining({}),
@@ -246,7 +262,7 @@ describe('updating layout axes', () => {
246262
);
247263

248264
axes.pop();
249-
chartUtils.updateFigureAxes(layout, figure, chartTheme);
265+
chartUtils.updateFigureAxes(layout, figure);
250266
expect(layout).toEqual(
251267
expect.objectContaining({
252268
xaxis: expect.objectContaining({}),
@@ -305,7 +321,6 @@ describe('updating layout axes', () => {
305321
layout,
306322
axes,
307323
figureAxes,
308-
chartTheme,
309324
width,
310325
height,
311326
bounds
@@ -646,3 +661,10 @@ describe('getMarkerSymbol', () => {
646661
expect(() => getMarkerSymbol('&$*(#@&')).toThrow();
647662
});
648663
});
664+
665+
describe('makeDefaultTemplate', () => {
666+
it('should create a default template', () => {
667+
const template = chartUtils.makeDefaultTemplate(chartTheme);
668+
expect(template).toMatchSnapshot();
669+
});
670+
});

0 commit comments

Comments
 (0)