Skip to content

Master odoo funnel chart adrm #5838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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
11 changes: 6 additions & 5 deletions demo/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,11 +413,11 @@ export const demoData = {
V31: "Proposition",
V32: "Won",
W27: "Count",
W28: "=RANDBETWEEN(120, 140)",
W29: "=W28 - RANDBETWEEN(10, 40)",
W30: "=W29 - RANDBETWEEN(10, 40)",
W31: "=W30 - RANDBETWEEN(10, 40)",
W32: "=W31 - RANDBETWEEN(10, 40)",
W28: "=RANDBETWEEN(10, 40)",
W29: "=RANDBETWEEN(10, 40)",
W30: "=RANDBETWEEN(10, 40)",
W31: "=RANDBETWEEN(10, 40)",
W32: "=RANDBETWEEN(10, 40)",
},
styles: {},
formats: {},
Expand Down Expand Up @@ -711,6 +711,7 @@ export const demoData = {
labelRange: "V27:V32",
title: { text: "Funnel" },
aggregated: false,
cumulative: true,
},
},
],
Expand Down
11 changes: 3 additions & 8 deletions src/components/figures/chart/chartJs/chartjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,15 @@ import { Component, onMounted, onWillUnmount, useEffect, useRef } from "@odoo/ow
import { Chart, ChartConfiguration } from "chart.js/auto";
import { ComponentsImportance } from "../../../../constants";
import { deepCopy } from "../../../../helpers";
import { getChartJSConstructor } from "../../../../helpers/figures/charts/chart_ui_common";
import { Figure, SpreadsheetChildEnv } from "../../../../types";
import { ChartJSRuntime } from "../../../../types/chart/chart";
import { css } from "../../../helpers";
import { FunnelChartController, FunnelChartElement } from "./chartjs_funnel_chart";
import { chartShowValuesPlugin } from "./chartjs_show_values_plugin";
import { waterfallLinesPlugin } from "./chartjs_waterfall_plugin";

interface Props {
figure: Figure;
}

window.Chart?.register(waterfallLinesPlugin);
window.Chart?.register(chartShowValuesPlugin);
window.Chart?.register(FunnelChartController, FunnelChartElement);

css/* scss */ `
.o-spreadsheet {
.o-chart-custom-tooltip {
Expand Down Expand Up @@ -78,7 +72,8 @@ export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
private createChart(chartData: ChartConfiguration<any>) {
const canvas = this.canvas.el as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;
this.chart = new window.Chart(ctx, chartData);
const Chart = getChartJSConstructor();
this.chart = new Chart(ctx, chartData);
}

private updateChartJs(chartRuntime: ChartJSRuntime) {
Expand Down
156 changes: 79 additions & 77 deletions src/components/figures/chart/chartJs/chartjs_funnel_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,97 +6,101 @@ import {
TooltipPositionerFunction,
} from "chart.js";

export class FunnelChartController extends window.Chart?.BarController {
static id = "funnel";
static defaults = { ...window.Chart?.BarController.defaults, dataElementType: "funnel" };

/** Called at each chart render to update the elements of the chart (FunnelChartElement) with the updated data */
updateElements(rects, start, count, mode) {
super.updateElements(rects, start, count, mode);
for (let i = start; i < start + count; i++) {
const rect = rects[i];
// Add the width of the next element to the element's props
this.updateElement(rect, i, { nextElementWidth: rects[i + 1]?.width || 0 }, mode);
export function getFunnelChartController() {
return class FunnelChartController extends window.Chart?.BarController {
static id = "funnel";
static defaults = { ...window.Chart?.BarController.defaults, dataElementType: "funnel" };

/** Called at each chart render to update the elements of the chart (FunnelChartElement) with the updated data */
updateElements(rects, start, count, mode) {
super.updateElements(rects, start, count, mode);
for (let i = start; i < start + count; i++) {
const rect = rects[i];
// Add the width of the next element to the element's props
this.updateElement(rect, i, { nextElementWidth: rects[i + 1]?.width || 0 }, mode);
}
}
}
};
}

/**
* Similar to a bar chart element, but it's a trapezoid rather than a rectangle. The top is of width
* `width`, and the bottom is of width `nextElementWidth`.
*/
export class FunnelChartElement extends window.Chart?.BarElement {
static id = "funnel";

/** Overwrite this to draw a trapezoid rather then a rectangle */
draw(ctx: CanvasRenderingContext2D) {
ctx.save();

const { x, y, width, height, nextElementWidth, base, options } = this.getProps([
"x",
"y",
"width",
"height",
"nextElementWidth",
"base",
"options",
]) as any;
const offset = (width - nextElementWidth) / 2;

const startX = Math.min(x, base);
const startY = y - height / 2;

ctx.fillStyle = options.backgroundColor;

ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startX + width, startY);
ctx.lineTo(startX + width - offset, startY + height);
ctx.lineTo(startX + offset, startY + height);
ctx.closePath();
ctx.fill();
if (options.borderWidth) {
ctx.strokeStyle = options.borderColor;
ctx.lineWidth = options.borderWidth;
ctx.stroke();
export function getFunnelChartElement() {
/**
* Similar to a bar chart element, but it's a trapezoid rather than a rectangle. The top is of width
* `width`, and the bottom is of width `nextElementWidth`.
*/
return class FunnelChartElement extends window.Chart?.BarElement {
static id = "funnel";

/** Overwrite this to draw a trapezoid rather then a rectangle */
draw(ctx: CanvasRenderingContext2D) {
ctx.save();

const { x, y, width, height, nextElementWidth, base, options } = this.getProps([
"x",
"y",
"width",
"height",
"nextElementWidth",
"base",
"options",
]) as any;
const offset = (width - nextElementWidth) / 2;

const startX = Math.min(x, base);
const startY = y - height / 2;

ctx.fillStyle = options.backgroundColor;

ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startX + width, startY);
ctx.lineTo(startX + width - offset, startY + height);
ctx.lineTo(startX + offset, startY + height);
ctx.closePath();
ctx.fill();
if (options.borderWidth) {
ctx.strokeStyle = options.borderColor;
ctx.lineWidth = options.borderWidth;
ctx.stroke();
}

ctx.restore();
}

ctx.restore();
}

/** Check if the mouse is inside the trapezoid */
inRange(mouseX: number, mouseY: number, useFinalPosition: boolean) {
const { x, y, width, height, nextElementWidth, base } = this.getProps(
["x", "y", "width", "height", "nextElementWidth", "base"],
useFinalPosition
) as any;

const startX = Math.min(x, base);
const startY = y - height / 2;
if (mouseY < startY || mouseY > startY + height) {
return false;
/** Check if the mouse is inside the trapezoid */
inRange(mouseX: number, mouseY: number, useFinalPosition: boolean) {
const { x, y, width, height, nextElementWidth, base } = this.getProps(
["x", "y", "width", "height", "nextElementWidth", "base"],
useFinalPosition
) as any;

const startX = Math.min(x, base);
const startY = y - height / 2;
if (mouseY < startY || mouseY > startY + height) {
return false;
}
const offset = (width - nextElementWidth) / 2;
const left = startX + (offset * (mouseY - startY)) / height;
const right = startX + width - (offset * (mouseY - startY)) / height;
if (mouseX < left || mouseX > right) {
return false;
}

return true;
}
const offset = (width - nextElementWidth) / 2;
const left = startX + (offset * (mouseY - startY)) / height;
const right = startX + width - (offset * (mouseY - startY)) / height;
if (mouseX < left || mouseX > right) {
return false;
}

return true;
}
};
}

/**
* Position the tooltip inside the trapezoid.
* The default position for tooltips of bar elements is at the end of rectangle, which is not ideal for trapezoids.
*/
const funnelTooltipPositioner: TooltipPositionerFunction<"funnel"> = function (elements) {
export const funnelTooltipPositioner: TooltipPositionerFunction<"funnel"> = function (elements) {
if (!elements.length) {
return { x: 0, y: 0 };
}

const element = elements[0].element as FunnelChartElement;
const element = elements[0].element;
const { x, y, base, width, height } = element.getProps(["x", "y", "width", "height", "base"]);
const startX = Math.min(x, base);
const startY = y - height / 2;
Expand All @@ -107,8 +111,6 @@ const funnelTooltipPositioner: TooltipPositionerFunction<"funnel"> = function (e
};
};

window.Chart.Tooltip.positioners.funnelTooltipPositioner = funnelTooltipPositioner;

declare module "chart.js" {
interface ChartTypeRegistry {
funnel: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { FunnelChartDefinition } from "../../../../types/chart";
import { GenericChartConfigPanel } from "../building_blocks/generic_side_panel/config_panel";

export class FunnelChartConfigPanel extends GenericChartConfigPanel {
getLabelRangeOptions() {
const definition = this.props.definition as FunnelChartDefinition;
return [
{
name: "aggregated",
label: this.chartTerms.AggregatedChart,
value: definition.aggregated ?? false,
onChange: this.onUpdateAggregated.bind(this),
},
{
name: "cumulative",
label: this.chartTerms.CumulativeData,
value: definition.cumulative ?? false,
onChange: this.onUpdateCumulative.bind(this),
},
{
name: "dataSetsHaveTitle",
label: this.dataSetsHaveTitleLabel,
value: definition.dataSetsHaveTitle,
onChange: this.onUpdateDataSetsHaveTitle.bind(this),
},
];
}

onUpdateCumulative(cumulative: boolean) {
this.props.updateChart(this.props.figureId, {
cumulative,
});
}
}
3 changes: 2 additions & 1 deletion src/components/side_panel/chart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BarConfigPanel } from "./bar_chart/bar_chart_config_panel";
import { GenericChartConfigPanel } from "./building_blocks/generic_side_panel/config_panel";
import { ChartWithAxisDesignPanel } from "./chart_with_axis/design_panel";
import { ComboChartDesignPanel } from "./combo_chart/combo_chart_design_panel";
import { FunnelChartConfigPanel } from "./funnel_chart_panel/funnel_chart_config_panel";
import { FunnelChartDesignPanel } from "./funnel_chart_panel/funnel_chart_design_panel";
import { GaugeChartConfigPanel } from "./gauge_chart_panel/gauge_chart_config_panel";
import { GaugeChartDesignPanel } from "./gauge_chart_panel/gauge_chart_design_panel";
Expand Down Expand Up @@ -79,6 +80,6 @@ chartSidePanelComponentRegistry
design: GeoChartDesignPanel,
})
.add("funnel", {
configuration: GenericChartConfigPanel,
configuration: FunnelChartConfigPanel,
design: FunnelChartDesignPanel,
});
5 changes: 3 additions & 2 deletions src/components/translations_terms.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { PasteInteractiveContent } from "../helpers/ui/paste_interactive";
import { _t } from "../translation";
import { GeoChartColorScale } from "../types/chart/geo_chart";
import { CommandResult } from "../types/index";
Expand Down Expand Up @@ -131,7 +130,9 @@ export const RemoveDuplicateTerms = {
[CommandResult.EmptyTarget]: _t("Please select a range of cells containing values."),
[CommandResult.NoColumnsProvided]: _t("Please select at latest one column to analyze."),
//TODO: Remove it when accept to copy and paste merge cells
[CommandResult.WillRemoveExistingMerge]: PasteInteractiveContent.willRemoveExistingMerge,
[CommandResult.WillRemoveExistingMerge]: _t(
"This operation is not possible due to a merge. Please remove the merges first than try again."
),
},
};

Expand Down
24 changes: 23 additions & 1 deletion src/helpers/figures/charts/chart_ui_common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { ChartConfiguration, ChartOptions } from "chart.js";
import {
funnelTooltipPositioner,
getFunnelChartController,
getFunnelChartElement,
} from "../../../components/figures/chart/chartJs/chartjs_funnel_chart";
import { chartShowValuesPlugin } from "../../../components/figures/chart/chartJs/chartjs_show_values_plugin";
import { waterfallLinesPlugin } from "../../../components/figures/chart/chartJs/chartjs_waterfall_plugin";
import { Figure } from "../../../types";
import { GaugeChartRuntime, ScorecardChartRuntime } from "../../../types/chart";
import { ChartRuntime } from "../../../types/chart/chart";
Expand Down Expand Up @@ -41,7 +48,8 @@ export function chartToImage(
if ("chartJsConfig" in runtime) {
const config = deepCopy(runtime.chartJsConfig);
config.plugins = [backgroundColorChartJSPlugin];
const chart = new window.Chart(canvas, config as ChartConfiguration);
const Chart = getChartJSConstructor();
const chart = new Chart(canvas, config as ChartConfiguration);
const imgContent = chart.toBase64Image() as string;
chart.destroy();
div.remove();
Expand Down Expand Up @@ -76,3 +84,17 @@ const backgroundColorChartJSPlugin = {
ctx.restore();
},
};

/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
export function getChartJSConstructor() {
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
window.Chart.register(
chartShowValuesPlugin,
waterfallLinesPlugin,
getFunnelChartController(),
getFunnelChartElement()
);
window.Chart.Tooltip.positioners.funnelTooltipPositioner = funnelTooltipPositioner;
}
return window.Chart;
}
Loading