Skip to content

Commit e354859

Browse files
committed
[MOV] charts: extract chart animations from Odoo
Extract the code handling chart animation in dashboard from odoo to o-spreadsheet. Task: 4787283 Part-of: #6363 Signed-off-by: Lucas Lefèvre (lul) <lul@odoo.com>
1 parent e18e777 commit e354859

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

src/components/figures/chart/chartJs/chartjs.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Component, onMounted, onWillUnmount, useEffect, useRef } from "@odoo/owl";
22
import { Chart, ChartConfiguration } from "chart.js/auto";
33
import { ComponentsImportance } from "../../../../constants";
4-
import { deepCopy } from "../../../../helpers";
4+
import { deepCopy, deepEquals } from "../../../../helpers";
5+
import { Store, useStore } from "../../../../store_engine";
56
import { FigureUI, SpreadsheetChildEnv } from "../../../../types";
67
import { ChartJSRuntime } from "../../../../types/chart/chart";
78
import { css } from "../../../helpers";
89
import { chartJsExtensionRegistry } from "./chart_js_extension";
10+
import { ChartAnimationStore } from "./chartjs_animation_store";
911
import {
1012
funnelTooltipPositioner,
1113
getFunnelChartController,
@@ -70,6 +72,7 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
7072
private canvas = useRef("graphContainer");
7173
private chart?: Chart;
7274
private currentRuntime!: ChartJSRuntime;
75+
private animationStore: Store<ChartAnimationStore> | undefined;
7376

7477
private currentDevicePixelRatio = window.devicePixelRatio;
7578

@@ -90,6 +93,9 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
9093
}
9194

9295
setup() {
96+
if (this.env.model.getters.isDashboard()) {
97+
this.animationStore = useStore(ChartAnimationStore);
98+
}
9399
onMounted(() => {
94100
const runtime = this.chartRuntime;
95101
this.currentRuntime = runtime;
@@ -115,12 +121,28 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
115121
}
116122

117123
private createChart(chartData: ChartConfiguration<any>) {
124+
if (this.env.model.getters.isDashboard() && this.animationStore) {
125+
const chartType = this.env.model.getters.getChart(this.props.figureUI.id)?.type;
126+
if (chartType && this.animationStore.animationPlayed[this.props.figureUI.id] !== chartType) {
127+
chartData = this.enableAnimationInChartData(chartData);
128+
this.animationStore.disableAnimationForChart(this.props.figureUI.id, chartType);
129+
}
130+
}
131+
118132
const canvas = this.canvas.el as HTMLCanvasElement;
119133
const ctx = canvas.getContext("2d")!;
120134
this.chart = new window.Chart(ctx, chartData);
121135
}
122136

123137
private updateChartJs(chartData: ChartConfiguration<any>) {
138+
if (this.env.model.getters.isDashboard()) {
139+
const chartType = this.env.model.getters.getChart(this.props.figureUI.id)?.type;
140+
if (chartType && this.hasChartDataChanged() && this.animationStore) {
141+
chartData = this.enableAnimationInChartData(chartData);
142+
this.animationStore.disableAnimationForChart(this.props.figureUI.id, chartType);
143+
}
144+
}
145+
124146
if (chartData.data && chartData.data.datasets) {
125147
this.chart!.data = chartData.data;
126148
if (chartData.options?.plugins?.title) {
@@ -132,4 +154,18 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
132154
this.chart!.config.options = chartData.options;
133155
this.chart!.update();
134156
}
157+
158+
private hasChartDataChanged() {
159+
return !deepEquals(
160+
this.currentRuntime.chartJsConfig.data,
161+
this.chartRuntime.chartJsConfig.data
162+
);
163+
}
164+
165+
private enableAnimationInChartData(chartData: ChartConfiguration<any>) {
166+
return {
167+
...chartData,
168+
options: { ...chartData.options, animation: { animateRotate: true } },
169+
};
170+
}
135171
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { SpreadsheetStore } from "../../../../stores/spreadsheet_store";
2+
import { ChartType, UID } from "../../../../types";
3+
4+
export class ChartAnimationStore extends SpreadsheetStore {
5+
mutators = ["disableAnimationForChart"] as const;
6+
7+
animationPlayed = {};
8+
9+
disableAnimationForChart(chartId: UID, chartType: ChartType) {
10+
this.animationPlayed[chartId] = chartType;
11+
return "noStateChange";
12+
}
13+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Chart } from "chart.js";
2+
import { Model, readonlyAllowedCommands } from "../../../src";
3+
import { createChart, setCellContent, updateChart } from "../../test_helpers/commands_helpers";
4+
import { mockChart, mountSpreadsheet, nextTick } from "../../test_helpers/helpers";
5+
6+
mockChart();
7+
8+
let mockedChart: any;
9+
beforeEach(() => {
10+
jest
11+
.spyOn((window as any).Chart.prototype, "constructorMock")
12+
.mockImplementation(function (this: Chart) {
13+
mockedChart = this;
14+
});
15+
});
16+
17+
describe("Chart animations in dashboard", () => {
18+
test("Charts are animated only at first render", async () => {
19+
const model = new Model();
20+
createChart(model, { type: "bar" });
21+
model.updateMode("dashboard");
22+
23+
await mountSpreadsheet({ model });
24+
expect(".o-figure").toHaveCount(1);
25+
expect(mockedChart.config.options.animation.animateRotate).toBe(true);
26+
27+
// Scroll the figure out of the viewport and back in
28+
model.dispatch("SET_VIEWPORT_OFFSET", { offsetX: 0, offsetY: 500 });
29+
await nextTick();
30+
expect(".o-figure").toHaveCount(0);
31+
32+
model.dispatch("SET_VIEWPORT_OFFSET", { offsetX: 0, offsetY: 0 });
33+
await nextTick();
34+
expect(".o-figure").toHaveCount(1);
35+
expect(mockedChart.config.options.animation).toBe(false);
36+
});
37+
38+
test("Animations are replayed only when chart data changes", async () => {
39+
readonlyAllowedCommands.add("UPDATE_CELL");
40+
41+
const model = new Model();
42+
createChart(model, { type: "bar", dataSets: [{ dataRange: "A1:A6" }] });
43+
model.updateMode("dashboard");
44+
await mountSpreadsheet({ model });
45+
46+
expect(mockedChart.config.options.animation).toEqual({ animateRotate: true });
47+
48+
// Dispatch a command that doesn't change the chart data
49+
setCellContent(model, "A50", "6");
50+
await nextTick();
51+
expect(mockedChart.config.options.animation).toBe(false);
52+
53+
// Change the chart data
54+
setCellContent(model, "A2", "6");
55+
await nextTick();
56+
expect(mockedChart.config.options.animation).toEqual({ animateRotate: true });
57+
58+
readonlyAllowedCommands.delete("UPDATE_CELL");
59+
});
60+
61+
test("Charts are animated when chart type changes", async () => {
62+
const model = new Model();
63+
createChart(model, { type: "bar", dataSets: [{ dataRange: "A1:A6" }] }, "chartId");
64+
model.updateMode("dashboard");
65+
await mountSpreadsheet({ model });
66+
67+
model.dispatch("EVALUATE_CELLS");
68+
await nextTick();
69+
expect(mockedChart.config.options.animation).toBe(false);
70+
71+
updateChart(model, "chartId", { type: "pie" });
72+
await nextTick();
73+
expect(mockedChart.config.options.animation.animateRotate).toBe(true);
74+
});
75+
});

0 commit comments

Comments
 (0)