Skip to content

Commit 75783d0

Browse files
mofojedmattrunyon
andauthored
fix: Log figure errors, don't show infinite spinner (#1614)
- Requires Core PR deephaven/deephaven-core#4763 - Also need to port that to Enterprise - Actually look at the errors reported by the figure, and log them so we have a clearer idea of what the issue is when plots are reported as not working - Also show the error message in the chart panel button bar. Clicking the button will display the full error message. - Don't show the spinner infinitely if there are no series to load --------- Co-authored-by: Matthew Runyon <mattrunyonstuff@gmail.com>
1 parent 5ee98cd commit 75783d0

11 files changed

Lines changed: 132 additions & 29 deletions

File tree

__mocks__/dh-core.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,7 @@ class Figure extends DeephavenObject {
15891589
updateSize = 10,
15901590
rows = 1,
15911591
cols = 1,
1592+
errors = [],
15921593
} = {}) {
15931594
super();
15941595

@@ -1603,6 +1604,7 @@ class Figure extends DeephavenObject {
16031604
this.rowIndex = 0;
16041605
this.rows = rows;
16051606
this.cols = cols;
1607+
this.errors = errors;
16061608
}
16071609

16081610
addEventListener(...args) {

packages/chart/src/Chart.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,7 @@ $plotly-color-btn-active: rgba(255, 255, 255, 70%);
108108
}
109109
}
110110
}
111+
112+
.chart-error-popper .popper-content {
113+
padding: $spacer-1;
114+
}

packages/chart/src/Chart.tsx

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { Component, ReactElement, RefObject } from 'react';
22
import deepEqual from 'deep-equal';
33
import memoize from 'memoize-one';
4+
import { CopyButton, Popper } from '@deephaven/components';
45
import {
56
vsLoading,
67
dhGraphLineDown,
@@ -33,6 +34,7 @@ import Plotly from './plotly/Plotly';
3334
import ChartModel from './ChartModel';
3435
import ChartUtils, { ChartModelSettings } from './ChartUtils';
3536
import './Chart.scss';
37+
import DownsamplingError from './DownsamplingError';
3638

3739
const log = Log.module('Chart');
3840

@@ -56,10 +58,15 @@ interface ChartProps {
5658

5759
interface ChartState {
5860
data: Partial<Data>[] | null;
61+
/** An error specific to downsampling */
5962
downsamplingError: unknown;
6063
isDownsampleFinished: boolean;
6164
isDownsampleInProgress: boolean;
6265
isDownsamplingDisabled: boolean;
66+
67+
/** Any other kind of error */
68+
error: unknown;
69+
shownError: string | null;
6370
layout: Partial<Layout>;
6471
revision: number;
6572
}
@@ -129,6 +136,7 @@ export class Chart extends Component<ChartProps, ChartState> {
129136

130137
this.handleAfterPlot = this.handleAfterPlot.bind(this);
131138
this.handleDownsampleClick = this.handleDownsampleClick.bind(this);
139+
this.handleErrorClose = this.handleErrorClose.bind(this);
132140
this.handleModelEvent = this.handleModelEvent.bind(this);
133141
this.handlePlotUpdate = this.handlePlotUpdate.bind(this);
134142
this.handleRelayout = this.handleRelayout.bind(this);
@@ -153,6 +161,8 @@ export class Chart extends Component<ChartProps, ChartState> {
153161
isDownsampleFinished: false,
154162
isDownsampleInProgress: false,
155163
isDownsamplingDisabled: false,
164+
error: null,
165+
shownError: null,
156166
layout: {
157167
datarevision: 0,
158168
},
@@ -236,15 +246,30 @@ export class Chart extends Component<ChartProps, ChartState> {
236246
isDownsampleFinished: boolean,
237247
isDownsampleInProgress: boolean,
238248
isDownsamplingDisabled: boolean,
239-
data: Partial<Data>[]
249+
data: Partial<Data>[],
250+
error: unknown
240251
): Partial<PlotlyConfig> => {
241252
const customButtons: ModeBarButtonAny[] = [];
242253
const hasDownsampleError = Boolean(downsamplingError);
243254
if (hasDownsampleError) {
244255
customButtons.push({
245256
name: `Downsampling failed: ${downsamplingError}`,
246257
title: 'Downsampling failed',
247-
click: () => undefined,
258+
click: () => {
259+
this.toggleErrorMessage(`${downsamplingError}`);
260+
},
261+
icon: Chart.convertIcon(dhWarningFilled),
262+
attr: 'fill-warning',
263+
});
264+
}
265+
const hasError = Boolean(error);
266+
if (hasError) {
267+
customButtons.push({
268+
name: `Error: ${error}`,
269+
title: `Error`,
270+
click: () => {
271+
this.toggleErrorMessage(`${error}`);
272+
},
248273
icon: Chart.convertIcon(dhWarningFilled),
249274
attr: 'fill-warning',
250275
});
@@ -301,7 +326,7 @@ export class Chart extends Component<ChartProps, ChartState> {
301326
// Yes, the value is a boolean or the string 'hover': https://github.com/plotly/plotly.js/blob/master/src/plot_api/plot_config.js#L249
302327
displayModeBar:
303328
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
304-
isDownsampleInProgress || hasDownsampleError
329+
isDownsampleInProgress || hasDownsampleError || hasError
305330
? true
306331
: ('hover' as const),
307332

@@ -376,6 +401,10 @@ export class Chart extends Component<ChartProps, ChartState> {
376401
);
377402
}
378403

404+
handleErrorClose(): void {
405+
this.setState({ shownError: null });
406+
}
407+
379408
handleModelEvent(event: CustomEvent): void {
380409
const { type, detail } = event;
381410
log.debug2('Received data update', type, detail);
@@ -442,7 +471,14 @@ export class Chart extends Component<ChartProps, ChartState> {
442471
});
443472

444473
const { onError } = this.props;
445-
onError(new Error(downsamplingError));
474+
onError(new DownsamplingError(downsamplingError));
475+
break;
476+
}
477+
case ChartModel.EVENT_ERROR: {
478+
const error = `${detail}`;
479+
this.setState({ error });
480+
const { onError } = this.props;
481+
onError(new Error(error));
446482
break;
447483
}
448484
default:
@@ -502,6 +538,15 @@ export class Chart extends Component<ChartProps, ChartState> {
502538
}
503539
}
504540

541+
/**
542+
* Toggle the error message. If it is already being displayed, then hide it.
543+
*/
544+
toggleErrorMessage(error: string): void {
545+
this.setState(({ shownError }) => ({
546+
shownError: shownError === error ? null : error,
547+
}));
548+
}
549+
505550
/**
506551
* Update the models dimensions and ranges.
507552
* Note that this will update it all whether the plot size changes OR the range
@@ -601,6 +646,8 @@ export class Chart extends Component<ChartProps, ChartState> {
601646
isDownsampleFinished,
602647
isDownsampleInProgress,
603648
isDownsamplingDisabled,
649+
error,
650+
shownError,
604651
layout,
605652
revision,
606653
} = this.state;
@@ -609,7 +656,8 @@ export class Chart extends Component<ChartProps, ChartState> {
609656
isDownsampleFinished,
610657
isDownsampleInProgress,
611658
isDownsamplingDisabled,
612-
data ?? []
659+
data ?? [],
660+
error
613661
);
614662
const isPlotShown = data != null;
615663
return (
@@ -632,6 +680,23 @@ export class Chart extends Component<ChartProps, ChartState> {
632680
style={{ height: '100%', width: '100%' }}
633681
/>
634682
)}
683+
<Popper
684+
className="chart-error-popper"
685+
options={{ placement: 'top' }}
686+
isShown={shownError != null}
687+
onExited={this.handleErrorClose}
688+
closeOnBlur
689+
interactive
690+
>
691+
{shownError != null && (
692+
<>
693+
<div className="chart-error">{shownError}</div>
694+
<CopyButton tooltip="Copy Error" copy={shownError}>
695+
Copy Error
696+
</CopyButton>
697+
</>
698+
)}
699+
</Popper>
635700
</div>
636701
);
637702
}

packages/chart/src/ChartModel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class ChartModel {
2929

3030
static EVENT_LOADFINISHED = 'ChartModel.EVENT_LOADFINISHED';
3131

32+
static EVENT_ERROR = 'ChartModel.EVENT_ERROR';
33+
3234
constructor(dh: DhType) {
3335
this.dh = dh;
3436
this.listeners = [];
@@ -161,6 +163,10 @@ class ChartModel {
161163
fireLoadFinished(): void {
162164
this.fireEvent(new CustomEvent(ChartModel.EVENT_LOADFINISHED));
163165
}
166+
167+
fireError(detail: string[]): void {
168+
this.fireEvent(new CustomEvent(ChartModel.EVENT_ERROR, { detail }));
169+
}
164170
}
165171

166172
export default ChartModel;

packages/chart/src/ChartTestUtils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,16 @@ class ChartTestUtils {
131131
charts = [this.makeChart()],
132132
rows = 1,
133133
cols = 1,
134+
errors = [],
134135
} = {}): Figure {
135136
// eslint-disable-next-line @typescript-eslint/no-explicit-any
136-
return new (this.dh as any).plot.Figure({ title, charts, rows, cols });
137+
return new (this.dh as any).plot.Figure({
138+
title,
139+
charts,
140+
rows,
141+
cols,
142+
errors,
143+
});
137144
}
138145
}
139146

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class DownsamplingError extends Error {
2+
isDownsamplingError = true;
3+
}
4+
5+
export default DownsamplingError;

packages/chart/src/FigureChartModel.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dh from '@deephaven/jsapi-shim';
22
import { TestUtils } from '@deephaven/utils';
3-
import { PlotData } from 'plotly.js';
3+
import { Data } from 'plotly.js';
44
import ChartTestUtils from './ChartTestUtils';
55
import type { ChartTheme } from './ChartTheme';
66
import FigureChartModel from './FigureChartModel';
@@ -464,8 +464,25 @@ it('adds new series', () => {
464464
]);
465465
});
466466

467+
it('emits finished loading if no series are added', () => {
468+
const figure = chartTestUtils.makeFigure({
469+
charts: [],
470+
});
471+
const model = new FigureChartModel(dh, figure, chartTheme);
472+
const callback = jest.fn();
473+
model.subscribe(callback);
474+
475+
jest.runOnlyPendingTimers();
476+
477+
expect(callback).toHaveBeenCalledWith(
478+
expect.objectContaining({
479+
type: FigureChartModel.EVENT_LOADFINISHED,
480+
})
481+
);
482+
});
483+
467484
describe('legend visibility', () => {
468-
function testLegend(showLegend: boolean | null): Partial<PlotData>[] {
485+
function testLegend(showLegend: boolean | null): Partial<Data>[] {
469486
const series1 = chartTestUtils.makeSeries({ name: 'S1' });
470487
const chart = chartTestUtils.makeChart({ series: [series1], showLegend });
471488
const figure = chartTestUtils.makeFigure({

packages/chart/src/FigureChartModel.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,11 @@ class FigureChartModel extends ChartModel {
266266
? this.dh.plot.DownsampleOptions.DISABLE
267267
: this.dh.plot.DownsampleOptions.DEFAULT
268268
);
269+
270+
if (this.figure.errors.length > 0) {
271+
log.error('Errors in figure', this.figure.errors);
272+
this.fireError(this.figure.errors);
273+
}
269274
}
270275

271276
unsubscribeFigure(): void {
@@ -459,19 +464,20 @@ class FigureChartModel extends ChartModel {
459464
}
460465

461466
this.seriesToProcess.delete(series.name);
462-
if (this.seriesToProcess.size === 0) {
463-
this.fireLoadFinished();
464-
}
465467

466468
this.cleanSeries(series);
467469
}
470+
if (this.seriesToProcess.size === 0) {
471+
this.fireLoadFinished();
472+
}
468473

469474
const { data } = this;
470475
this.fireUpdate(data);
471476
}
472477

473478
handleRequestFailed(event: ChartEvent): void {
474479
log.error('Request failed', event);
480+
this.fireError([`${event.detail}`]);
475481
}
476482

477483
/**

packages/chart/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { default as ChartModelFactory } from './ChartModelFactory';
33
export { default as ChartModel } from './ChartModel';
44
export { default as ChartUtils } from './ChartUtils';
55
export * from './ChartUtils';
6+
export * from './DownsamplingError';
67
export { default as FigureChartModel } from './FigureChartModel';
78
export { default as MockChartModel } from './MockChartModel';
89
export { default as Plot } from './plotly/Plot';

packages/chart/tsconfig.json

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,12 @@
77
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
88
"exclude": ["node_modules", "src/**/*.test.*", "src/**/__mocks__/*"],
99
"references": [
10-
{
11-
"path": "../components"
12-
},
13-
{
14-
"path": "../jsapi-shim"
15-
},
16-
{
17-
"path": "../jsapi-utils"
18-
},
19-
{
20-
"path": "../log"
21-
},
22-
{
23-
"path": "../react-hooks"
24-
},
25-
{
26-
"path": "../utils"
27-
}
10+
{ "path": "../components" },
11+
{ "path": "../jsapi-shim" },
12+
{ "path": "../jsapi-types" },
13+
{ "path": "../jsapi-utils" },
14+
{ "path": "../log" },
15+
{ "path": "../react-hooks" },
16+
{ "path": "../utils" }
2817
]
2918
}

0 commit comments

Comments
 (0)