[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:
committed by
Dardenne Florent (dafl)
parent
012c84cc62
commit
cc207bbe66
@@ -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>
|
||||
`,
|
||||
|
||||
Reference in New Issue
Block a user