[REF] web,project: stacked line chart generalization

Add stacked option on line chart.
The option already existed in Project for burndown graph, so I exported
and adapted everything concerning stacked lines from Project to Web.

closes odoo/odoo#96833

Task-id: 2929576
Signed-off-by: Simon Genin (ges@odoo) <ges@odoo.com>
This commit is contained in:
dafl-odoo
2022-08-01 14:44:00 +02:00
committed by Dardenne Florent (dafl)
parent 012c84cc62
commit cc207bbe66
7 changed files with 166 additions and 152 deletions
@@ -1,46 +0,0 @@
/** @odoo-module **/
import { hexToRGBA } from "@web/views/graph/colors";
import { GraphRenderer } from "@web/views/graph/graph_renderer";
export class BurndownChartRenderer extends GraphRenderer {
/**
* @override
*/
getLineChartData() {
const data = super.getLineChartData();
const { stacked } = this.model.metaData;
if (stacked) {
for (const dataset of data.datasets) {
dataset.backgroundColor = hexToRGBA(dataset.borderColor, 0.4);
}
}
return data;
}
/**
* @override
*/
getElementOptions() {
const elementOptions = super.getElementOptions();
const { mode, stacked } = this.model.metaData;
if (mode === "line") {
elementOptions.line.fill = stacked;
}
return elementOptions;
}
/**
* @override
*/
getScaleOptions() {
const { xAxes, yAxes } = super.getScaleOptions();
const { mode, stacked } = this.model.metaData;
if (mode === "line") {
for (const y of yAxes) {
y.stacked = stacked;
}
}
return { xAxes, yAxes };
}
}
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { BurndownChartModel } from "./burndown_chart_model";
import { BurndownChartRenderer } from "./burndown_chart_renderer";
import { graphView } from "@web/views/graph/graph_view";
import { registry } from "@web/core/registry";
import { BurndownChartSearchModel } from "./burndown_chart_search_model";
@@ -10,7 +9,6 @@ const viewRegistry = registry.category("views");
const burndownChartGraphView = {
...graphView,
Renderer: BurndownChartRenderer,
buttonTemplate: "project.BurndownChartView.Buttons",
hideCustomGroupBy: true,
Model: BurndownChartModel,
@@ -4,10 +4,7 @@
<t t-name="project.BurndownChartView.Buttons" t-inherit="web.GraphView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//button[@data-mode='pie']" position="replace">
</xpath>
<xpath expr="//div[@role='toolbar'][3]" position="attributes">
<attribute name="t-if">true</attribute>
</xpath>
<xpath expr="//div[@role='toolbar'][4]" position="replace">
<xpath expr="//div[@role='toolbar'][@name='toggleOrderToolbar']" position="replace">
</xpath>
</t>
@@ -3,9 +3,7 @@
import { browser } from "@web/core/browser/browser";
import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { setupControlPanelServiceRegistry, toggleGroupByMenu, toggleMenuItem, toggleMenuItemOption } from "@web/../tests/search/helpers";
import { COLORS, hexToRGBA } from "@web/views/graph/colors";
import { dialogService } from "@web/core/dialog/dialog_service";
import { getGraphRenderer } from "@web/../tests/views/graph_view_tests";
import { makeView } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
@@ -80,91 +78,6 @@ QUnit.module("Project", {}, () => {
QUnit.module("BurndownChart");
QUnit.test("check if default mode is line chart and line chart is stacked for burndown chart", async function (assert) {
assert.expect(5);
const burndownChart = await makeView(makeViewParams);
assert.strictEqual(burndownChart.model.metaData.mode, "line", "should be in line chart mode.");
assert.ok(burndownChart.model.metaData.stacked, "should be stacked by default.");
assert.ok(getGraphRenderer(burndownChart).getScaleOptions().yAxes.every(y => y.stacked), "the stacked property in y axes should be true when the stacked is enabled in line chart");
assert.ok(getGraphRenderer(burndownChart).getElementOptions().line.fill, "The fill property should be true to add backgroundColor in line chart.");
const actualDatasets = [];
const expectedDatasets = [];
const keysToEvaluate = ["backgroundColor", "borderColor", "originIndex", "pointBackgroundColor"];
const datasets = getGraphRenderer(burndownChart).chart.data.datasets;
for (let i = 0; i < datasets.length; i++) {
const dataset = datasets[i];
const actualDataset = {};
keysToEvaluate.forEach(key => {
if (dataset.hasOwnProperty(key)) {
actualDataset[key] = dataset[key];
}
});
actualDatasets.push(actualDataset);
const expectedColor = COLORS[i];
expectedDatasets.push({
backgroundColor: hexToRGBA(expectedColor, 0.4),
borderColor: expectedColor,
originIndex: 0,
pointBackgroundColor: expectedColor,
});
}
assert.deepEqual(actualDatasets, expectedDatasets);
});
QUnit.test("check if the stacked button is visible in the line chart", async function (assert) {
assert.expect(3);
const burndownChart = await makeView(makeViewParams);
assert.ok(burndownChart.model.metaData.stacked, "graph should be a burndown chart.");
assert.containsOnce(target, `button.o_graph_button[data-tooltip="Stacked"]`);
const stackButton = target.querySelector(`button.o_graph_button[data-tooltip="Stacked"]`);
await click(stackButton);
assert.notOk(burndownChart.model.metaData.stacked, "graph should be a classic line chart.");
});
QUnit.test("check if it is classic line chart when stacked prop is false in line chart", async function (assert) {
assert.expect(4);
const burndownChart = await makeView(makeViewParams);
const stackButton = target.querySelector(`button.o_graph_button[data-tooltip="Stacked"]`);
await click(stackButton);
assert.notOk(burndownChart.model.metaData.stacked, "graph should be a classic line chart.");
assert.notOk(getGraphRenderer(burndownChart).getScaleOptions().yAxes.every(y => y.stacked), "the y axes should have a stacked property set to false since the stacked property in line chart is false.");
assert.notOk(getGraphRenderer(burndownChart).getElementOptions().line.fill, "The fill property should be false since the stacked property is false.");
const actualDatasets = [];
const expectedDatasets = [];
const keysToEvaluate = ["backgroundColor", "borderColor", "originIndex", "pointBackgroundColor"];
const datasets = getGraphRenderer(burndownChart).chart.data.datasets;
for (let i = 0; i < datasets.length; i++) {
const dataset = datasets[i];
const actualDataset = {};
keysToEvaluate.forEach(key => {
if (dataset.hasOwnProperty(key)) {
actualDataset[key] = dataset[key];
}
});
actualDatasets.push(actualDataset);
const expectedColor = COLORS[i];
expectedDatasets.push({
borderColor: expectedColor,
originIndex: 0,
pointBackgroundColor: expectedColor,
});
}
assert.deepEqual(actualDatasets, expectedDatasets);
});
QUnit.test("check that the sort buttons are invisible", async function (assert) {
await makeView(makeViewParams);
assert.containsNone(target, '.o_cp_bottom_left:has(.btn-group[role=toolbar][aria-label="Sort graph"])', "The sort buttons are not rendered.");
@@ -18,7 +18,7 @@
t-on-click="() => this.onModeSelected('bar')"
t-att-class="{ active: model.metaData.mode === 'bar' }"
/>
<button class="btn btn-secondary fa fa-area-chart o_graph_button" data-tooltip="Line Chart" aria-label="Line Chart" data-mode="line"
<button class="btn btn-secondary fa fa-line-chart o_graph_button" data-tooltip="Line Chart" aria-label="Line Chart" data-mode="line"
t-on-click="() => this.onModeSelected('line')"
t-att-class="{ active: model.metaData.mode === 'line' }"
/>
@@ -33,7 +33,13 @@
t-att-class="{ active: model.metaData.stacked }"
/>
</div>
<div t-if="model.metaData.mode !== 'pie' and model.metaData.domains.length === 1" class="btn-group" role="toolbar" aria-label="Sort graph">
<div t-if="model.metaData.mode === 'line'" class="btn-group" role="toolbar" aria-label="Change graph">
<button class="btn btn-secondary fa fa-database o_graph_button" data-tooltip="Stacked" aria-label="Stacked"
t-on-click="toggleStacked"
t-att-class="{ active: model.metaData.stacked }"
/>
</div>
<div t-if="model.metaData.mode !== 'pie' and model.metaData.domains.length === 1" class="btn-group" role="toolbar" aria-label="Sort graph" name="toggleOrderToolbar">
<button class="btn btn-secondary fa fa-sort-amount-desc o_graph_button" data-tooltip="Descending" aria-label="Descending"
t-on-click="() => this.toggleOrder('DESC')"
t-att-class="{ active: model.metaData.order === 'DESC' }"
@@ -242,12 +242,12 @@ export class GraphRenderer extends Component {
* @returns {Object}
*/
getElementOptions() {
const { mode } = this.model.metaData;
const { mode, stacked } = this.model.metaData;
const elementOptions = {};
if (mode === "bar") {
elementOptions.rectangle = { borderWidth: 1 };
} else if (mode === "line") {
elementOptions.line = { fill: false, tension: 0 };
elementOptions.line = { fill: stacked, tension: 0 };
}
return elementOptions;
}
@@ -318,7 +318,7 @@ export class GraphRenderer extends Component {
* @returns {Object}
*/
getLineChartData() {
const { groupBy, domains } = this.model.metaData;
const { groupBy, domains, stacked } = this.model.metaData;
const data = this.model.data;
for (let index = 0; index < data.datasets.length; ++index) {
const dataset = data.datasets[index];
@@ -345,6 +345,9 @@ export class GraphRenderer extends Component {
}
dataset.pointBackgroundColor = dataset.borderColor;
dataset.pointBorderColor = "rgba(0,0,0,0.2)";
if (stacked) {
dataset.backgroundColor = hexToRGBA(dataset.borderColor, 0.4);
}
}
if (data.datasets.length === 1 && data.datasets[0].originIndex === 0) {
const dataset = data.datasets[0];
@@ -413,6 +416,7 @@ export class GraphRenderer extends Component {
measure,
measures,
mode,
stacked,
} = this.model.metaData;
if (mode === "pie") {
return {};
@@ -435,6 +439,7 @@ export class GraphRenderer extends Component {
suggestedMax: 0,
suggestedMin: 0,
},
stacked: mode === "line" && stacked,
};
return { xAxes: [xAxe], yAxes: [yAxe] };
}
@@ -26,7 +26,7 @@ import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { BORDER_WHITE, DEFAULT_BG } from "@web/views/graph/colors";
import { BORDER_WHITE, DEFAULT_BG, COLORS, hexToRGBA } from "@web/views/graph/colors";
import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
import { patchWithCleanup } from "../helpers/utils";
@@ -131,6 +131,10 @@ function checkModeIs(assert, graph, mode) {
assert.hasClass(getModeButton(target, mode), "active");
}
function getScaleY(graph) {
return getChart(graph).config.options.scales.yAxes;
}
function getXAxeLabel(graph) {
return getChart(graph).config.options.scales.xAxes[0].scaleLabel.labelString;
}
@@ -831,7 +835,7 @@ QUnit.module("Views", (hooks) => {
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<graph type="line" stacked="0">
<field name="bar"/>
<field name="product_id"/>
</graph>
@@ -883,6 +887,143 @@ QUnit.module("Views", (hooks) => {
);
});
QUnit.test("Stacked button visible in the line chart", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
await selectMode(target, "line");
checkModeIs(assert, graph, "line");
assert.strictEqual(graph.model.metaData.stacked, true, "graph should be stacked.");
assert.strictEqual(
getScaleY(graph).every((y) => y.stacked),
true,
"The y axes should have stacked property set to true"
);
assert.containsOnce(target, `button.o_graph_button[data-tooltip="Stacked"]`);
const stackButton = target.querySelector(`button.o_graph_button[data-tooltip="Stacked"]`);
await click(stackButton);
assert.strictEqual(
graph.model.metaData.stacked,
false,
"graph should be a classic line chart."
);
assert.strictEqual(
getScaleY(graph).every((y) => y.stacked),
false,
"The y axes should have stacked property set to false"
);
});
QUnit.test("Stacked line prop click false", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
const stackButton = target.querySelector(`button.o_graph_button[data-tooltip="Stacked"]`);
await click(stackButton);
assert.strictEqual(
graph.model.metaData.stacked,
false,
"graph should be a classic line chart."
);
assert.strictEqual(
getScaleY(graph).every((y) => y.stacked),
false,
"the y axes should have a stacked property set to false since the stacked property in line chart is false."
);
assert.strictEqual(
getGraphRenderer(graph).getElementOptions().line.fill,
false,
"The fill property should be false since the stacked property is false."
);
const expectedDatasets = [
{
backgroundColor: undefined,
borderColor: "#1f77b4",
originIndex: 0,
pointBackgroundColor: "#1f77b4",
},
{
backgroundColor: undefined,
borderColor: "#ff7f0e",
originIndex: 0,
pointBackgroundColor: "#ff7f0e",
},
];
const keysToEvaluate = [
"backgroundColor",
"borderColor",
"originIndex",
"pointBackgroundColor",
];
checkDatasets(assert, graph, keysToEvaluate, expectedDatasets);
});
QUnit.test("Stacked prop and default line chart", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
assert.strictEqual(graph.model.metaData.mode, "line", "should be in line chart mode.");
assert.strictEqual(graph.model.metaData.stacked, true, "should be stacked by default.");
assert.strictEqual(
getScaleY(graph).every((y) => y.stacked),
true,
"the stacked property in y axes should be true when the stacked is enabled in line chart"
);
assert.strictEqual(
getGraphRenderer(graph).getElementOptions().line.fill,
true,
"The fill property should be true to add backgroundColor in line chart."
);
const expectedDatasets = [];
const keysToEvaluate = [
"backgroundColor",
"borderColor",
"originIndex",
"pointBackgroundColor",
];
const datasets = getChart(graph).data.datasets;
for (let i = 0; i < datasets.length; i++) {
const expectedColor = COLORS[i];
expectedDatasets.push({
backgroundColor: hexToRGBA(expectedColor, 0.4),
borderColor: expectedColor,
originIndex: 0,
pointBackgroundColor: expectedColor,
});
}
checkDatasets(assert, graph, keysToEvaluate, expectedDatasets);
});
QUnit.test("line chart rendering (no groupBy, several domains)", async function (assert) {
assert.expect(7);
const graph = await makeView({
@@ -890,7 +1031,7 @@ QUnit.module("Views", (hooks) => {
resModel: "foo",
type: "graph",
arch: `
<graph type="line">
<graph type="line" stacked="0">
<field name="revenue" type="measure"/>
</graph>
`,
@@ -951,7 +1092,7 @@ QUnit.module("Views", (hooks) => {
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<graph type="line" stacked="0">
<field name="revenue" type="measure"/>
<field name="foo"/>
</graph>
@@ -1052,7 +1193,7 @@ QUnit.module("Views", (hooks) => {
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<graph type="line" stacked="0">
<field name="revenue" type="measure"/>
<field name="date" interval="week"/>
</graph>
@@ -1159,7 +1300,7 @@ QUnit.module("Views", (hooks) => {
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<graph type="line" stacked="0">
<field name="revenue" type="measure"/>
<field name="bar"/>
<field name="date" interval="week"/>
@@ -1263,7 +1404,7 @@ QUnit.module("Views", (hooks) => {
serverData,
type: "graph",
resModel: "foo",
arch: `<graph type="line"/>`,
arch: `<graph type="line" stacked="0"/>`,
});
assert.containsOnce(target, "canvas", "should have a canvas");
});
@@ -3640,7 +3781,7 @@ QUnit.module("Views", (hooks) => {
serverData,
context: { search_default_date_filter: 1 },
arch: `
<graph type="line">
<graph type="line" stacked="0">
<field name="product_id"/>
</graph>
`,