Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions src/components/chart/chart.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,18 @@ export default class ZnChart extends ZincElement {

private chart?: ECharts;
private liveTimer?: number;
private pendingRender = false;

private readonly resizeObserver = new ResizeController(this, {
target: null,
skipInitial: true,
callback: () => this.chart?.resize(),
callback: () => {
if (!this.chart) return;
this.chart.resize();
// The container may have just gained a non-zero size (e.g. a hidden tab
// became visible). If we deferred the initial render, do it now.
if (this.pendingRender) this.renderChart();
},
});

protected firstUpdated(_changedProperties: PropertyValues) {
Expand Down Expand Up @@ -144,12 +151,30 @@ export default class ZnChart extends ZincElement {
}
}

/**
* Applies the current option to the chart, but only once the container has a
* non-zero size. ECharts' sankey layout crashes ("Cannot read properties of
* null") when asked to render into a zero-width or zero-height container —
* which happens when a chart is created inside a hidden tab or a flex/grid
* cell that has not been laid out yet. In that case we defer the render and
* let the ResizeController trigger it once the container gains a size.
*/
private renderChart() {
if (!this.chart) return;
if (this.chart.getWidth() === 0 || this.chart.getHeight() === 0) {
this.pendingRender = true;
return;
}
this.pendingRender = false;
this.chart.setOption(this.buildOption());
}

private initChart() {
const host = this.shadowRoot?.getElementById('chart') as HTMLElement | null;
if (!host) return;
host.style.height = `${this.height}px`;
this.chart = echarts.init(host);
this.chart.setOption(this.buildOption());
this.renderChart();

if (this.syncGroup) {
this.chart.group = this.syncGroup;
Expand All @@ -167,7 +192,7 @@ export default class ZnChart extends ZincElement {
const res = await fetch(this.dataUrl);
const newData = await res.json() as SeriesItem[];
this.data = newData;
this.chart?.setOption({ ...this.buildOption() });
this.renderChart();
} catch {
/* swallow transient fetch errors */
}
Expand Down Expand Up @@ -201,7 +226,7 @@ export default class ZnChart extends ZincElement {
}
return;
}
this.chart.setOption({ ...this.buildOption() });
this.renderChart();
}

disconnectedCallback() {
Expand Down
30 changes: 30 additions & 0 deletions src/components/chart/chart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ describe('<zn-chart>', () => {
expect(canvas).to.exist;
});

it('does not crash when a sankey chart starts in a zero-width container', async () => {
const wrapper: any = await fixture(html`
<div style="width: 0">
<zn-chart
type="sankey"
.data=${[{
name: 'Flow',
data: [
{ source: 'A', target: 'B', value: 10 },
{ source: 'B', target: 'C', value: 5 },
],
}]}
></zn-chart>
</div>
`);
const el: any = wrapper.querySelector('zn-chart');
await el.updateComplete;
await new Promise((r) => setTimeout(r, 50));

// Reaching this point means initialising the sankey chart in a zero-width
// container did not throw — previously ECharts' sankey layout crashed with
// "Cannot read properties of null". The render is deferred until the
// container gains a size.
wrapper.style.width = '600px';
await new Promise((r) => setTimeout(r, 150));

const series = (el.chart?.getOption()?.series ?? []) as unknown[];
expect(series.length).to.be.greaterThan(0);
});

it('joins a sync-group when the attribute is set', async () => {
const a: any = await fixture(html`
<zn-chart sync-group="g1" type="bar"
Expand Down