From b092c5d1d3ff8cd7dcee3b5a98dde4d8bd5b708b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Mon, 17 Nov 2025 16:09:59 +0100 Subject: [PATCH 1/9] [REF] chart: explicit default humanize Currently, 'humanize' is not explicitely set when creating a new chart. The default value `true` is later assigned as a fallback in the Chart class. I'd like to simplify those classes. --- .../helpers/figures/charts/abstract_chart.ts | 4 ++-- .../src/migrations/migration_steps.ts | 12 ++++++++++++ .../figures/charts/smart_chart_engine.ts | 17 ++++++++++++++--- .../chart/menu_item_insert_chart.test.ts | 1 + tests/test_helpers/constants.ts | 4 ++++ 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts index 3a1a264ac6..c5152d0115 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts @@ -24,13 +24,13 @@ export abstract class AbstractChart { readonly title: TitleDesign; abstract readonly type: ChartType; protected readonly getters: CoreGetters; - readonly humanize: boolean; + readonly humanize: boolean | undefined; constructor(definition: ChartDefinition, sheetId: UID, getters: CoreGetters) { this.title = definition.title; this.sheetId = sheetId; this.getters = getters; - this.humanize = definition.humanize ?? true; + this.humanize = definition.humanize; } /** diff --git a/packages/o-spreadsheet-engine/src/migrations/migration_steps.ts b/packages/o-spreadsheet-engine/src/migrations/migration_steps.ts index 9736eb8d44..de9a9b7cde 100644 --- a/packages/o-spreadsheet-engine/src/migrations/migration_steps.ts +++ b/packages/o-spreadsheet-engine/src/migrations/migration_steps.ts @@ -568,6 +568,18 @@ migrationStepRegistry } return data; }, + }) + .add("19.1.1", { + migrate(data: WorkbookData): any { + for (const sheet of data.sheets || []) { + for (const figure of sheet.figures || []) { + if (figure.tag === "chart" && !("humanize" in figure.data)) { + figure.data.humanize = true; + } + } + } + return data; + } }); function fixOverlappingFilters(data: any): any { diff --git a/src/helpers/figures/charts/smart_chart_engine.ts b/src/helpers/figures/charts/smart_chart_engine.ts index 804c58d437..f43ef4f537 100644 --- a/src/helpers/figures/charts/smart_chart_engine.ts +++ b/src/helpers/figures/charts/smart_chart_engine.ts @@ -17,6 +17,7 @@ const DEFAULT_BAR_CHART_CONFIG: BarChartDefinition = { legendPosition: "none", dataSetsHaveTitle: false, stacked: false, + humanize: true, }; const DEFAULT_LINE_CHART_CONFIG: LineChartDefinition = { @@ -28,6 +29,7 @@ const DEFAULT_LINE_CHART_CONFIG: LineChartDefinition = { stacked: false, cumulative: false, labelsAsText: false, + humanize: true, }; interface ColumnInfo { @@ -342,10 +344,19 @@ export function getSmartChartDefinition(zones: Zone[], getters: Getters): ChartD const nonEmptyColumns = columns.filter((col) => col.type !== "empty"); switch (nonEmptyColumns.length) { case 1: - return buildSingleColumnChart(nonEmptyColumns[0], getters); + return { + humanize: true, + ...buildSingleColumnChart(nonEmptyColumns[0], getters), + }; case 2: - return buildTwoColumnChart(nonEmptyColumns, getters); + return { + humanize: true, + ...buildTwoColumnChart(nonEmptyColumns, getters), + }; default: - return buildMultiColumnChart(nonEmptyColumns, getters); + return { + humanize: true, + ...buildMultiColumnChart(nonEmptyColumns, getters), + }; } } diff --git a/tests/figures/chart/menu_item_insert_chart.test.ts b/tests/figures/chart/menu_item_insert_chart.test.ts index a11777f5fa..bde7e7430f 100644 --- a/tests/figures/chart/menu_item_insert_chart.test.ts +++ b/tests/figures/chart/menu_item_insert_chart.test.ts @@ -116,6 +116,7 @@ describe("Insert chart menu item", () => { legendPosition: "none", title: {}, type: "bar", + humanize: true, }, }; }); diff --git a/tests/test_helpers/constants.ts b/tests/test_helpers/constants.ts index fd3886b6da..5fe29df0f0 100644 --- a/tests/test_helpers/constants.ts +++ b/tests/test_helpers/constants.ts @@ -21,6 +21,7 @@ export const TEST_CHART_DATA = { background: BACKGROUND_CHART_COLOR, stacked: false, legendPosition: "top" as const, + humanize: true, }, combo: { type: "combo" as const, @@ -34,6 +35,7 @@ export const TEST_CHART_DATA = { title: { text: "hello" }, background: BACKGROUND_CHART_COLOR, legendPosition: "top" as const, + humanize: true, }, scorecard: { type: "scorecard" as const, @@ -42,11 +44,13 @@ export const TEST_CHART_DATA = { title: { text: "hello" }, baselineDescr: { text: "description" }, baselineMode: "difference" as const, + humanize: true, }, gauge: { type: "gauge" as const, dataRange: "B1:B4", title: { text: "hello" }, + humanize: true, sectionRule: { rangeMin: "0", rangeMax: "100", From a37c943aa76debe60cf66141f15041f37a37a592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Thu, 20 Nov 2025 13:26:08 +0100 Subject: [PATCH 2/9] [REF] charts: dataset with { value, format } `data` was typed as `any`. This is not a good thing in itself. Moreover, it was only the values without the format. Also typeof is extremely permissive --- .../src/helpers/cells/cell_evaluation.ts | 42 +++++ .../cell_evaluation/evaluation_plugin.ts | 4 +- .../src/types/chart/chart.ts | 4 +- src/helpers/figures/charts/pyramid_chart.ts | 7 +- .../charts/runtime/chart_data_extractor.ts | 154 ++++++++++-------- .../figures/charts/runtime/chartjs_dataset.ts | 65 +++++--- .../figures/charts/runtime/chartjs_scales.ts | 30 +++- .../charts/runtime/chartjs_show_values.ts | 10 +- tests/figures/chart/chart_plugin.test.ts | 49 +++--- tests/test_helpers/getters_helpers.ts | 4 +- 10 files changed, 240 insertions(+), 129 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/helpers/cells/cell_evaluation.ts b/packages/o-spreadsheet-engine/src/helpers/cells/cell_evaluation.ts index ef258f3399..524e70f55e 100644 --- a/packages/o-spreadsheet-engine/src/helpers/cells/cell_evaluation.ts +++ b/packages/o-spreadsheet-engine/src/helpers/cells/cell_evaluation.ts @@ -129,6 +129,48 @@ function _createEvaluatedCell( return textCell(value, format, formattedValue); } +export function isNumberCell( + result: FunctionResultObject | undefined +): result is { value: number } { + return !!result && getEvaluatedCellType(result) === CellValueType.number; +} + +export function isTextCell(result: FunctionResultObject | undefined): result is { value: string } { + return !!result && getEvaluatedCellType(result) === CellValueType.text; +} + +export function isBooleanCell( + result: FunctionResultObject | undefined +): result is { value: boolean } { + return !!result && getEvaluatedCellType(result) === CellValueType.boolean; +} + +export function isEmptyCell(result: FunctionResultObject | undefined): result is { value: null } { + return !!result && getEvaluatedCellType(result) === CellValueType.empty; +} + +export function isErrorCell(result: FunctionResultObject | undefined): result is { value: string } { + return !!result && getEvaluatedCellType(result) === CellValueType.error; +} + +function getEvaluatedCellType({ value, format }: FunctionResultObject): CellValueType { + if (value === null) { + return CellValueType.empty; + } else if (isEvaluationError(value)) { + return CellValueType.error; + } else if (isTextFormat(format)) { + return CellValueType.text; + } + switch (typeof value) { + case "number": + return CellValueType.number; + case "boolean": + return CellValueType.boolean; + case "string": + return CellValueType.text; + } +} + function textCell( value: string, format: string | undefined, diff --git a/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts b/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts index 7eec2e36ba..c789b3ad66 100644 --- a/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts +++ b/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts @@ -259,10 +259,10 @@ export class EvaluationPlugin extends CoreViewPlugin { /** * Return the value of each cell in the range. */ - getRangeValues(range: Range): CellValue[] { + getRangeValues(range: Range): EvaluatedCell[] { const sheet = this.getters.tryGetSheet(range.sheetId); if (sheet === undefined) return []; - return this.mapVisiblePositions(range, (p) => this.getters.getEvaluatedCell(p).value); + return this.mapVisiblePositions(range, (p) => this.getters.getEvaluatedCell(p)); } /** diff --git a/packages/o-spreadsheet-engine/src/types/chart/chart.ts b/packages/o-spreadsheet-engine/src/types/chart/chart.ts index d5aa66502e..aca319ad21 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/chart.ts @@ -21,7 +21,7 @@ import { } from "./tree_map_chart"; import { WaterfallChartDefinition, WaterfallChartRuntime } from "./waterfall_chart"; -import { Align, Color, VerticalAlign } from "../.."; +import { Align, Color, FunctionResultObject, VerticalAlign } from "../.."; import { COLORSCHEMES } from "../../helpers/color"; import { Format } from "../format"; import { Locale } from "../locale"; @@ -104,7 +104,7 @@ export interface LabelValues { export interface DatasetValues { readonly label?: string; - readonly data: any[]; + readonly data: FunctionResultObject[]; readonly hidden?: boolean; } diff --git a/src/helpers/figures/charts/pyramid_chart.ts b/src/helpers/figures/charts/pyramid_chart.ts index beb742033d..cc0d615f89 100644 --- a/src/helpers/figures/charts/pyramid_chart.ts +++ b/src/helpers/figures/charts/pyramid_chart.ts @@ -1,5 +1,6 @@ import { CoreGetters, Validator } from "@odoo/o-spreadsheet-engine"; import { BACKGROUND_CHART_COLOR } from "@odoo/o-spreadsheet-engine/constants"; +import { isNumberCell } from "@odoo/o-spreadsheet-engine/helpers/cells/cell_evaluation"; import { AbstractChart } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/abstract_chart"; import { chartFontColor, @@ -197,7 +198,11 @@ export class PyramidChart extends AbstractChart { const chartData = getPyramidChartData(definition, this.dataSets, this.labelRange, getters); const { dataSetsValues } = chartData; const maxValue = Math.max( - ...dataSetsValues.map((dataSet) => Math.max(...dataSet.data.map(Math.abs))) + ...dataSetsValues.map((dataSet) => + Math.max( + ...dataSet.data.map((cell) => (isNumberCell(cell) ? Math.abs(cell.value) : -Infinity)) + ) + ) ); return { ...definition, diff --git a/src/helpers/figures/charts/runtime/chart_data_extractor.ts b/src/helpers/figures/charts/runtime/chart_data_extractor.ts index acaef099fd..92ba1f1fe1 100644 --- a/src/helpers/figures/charts/runtime/chart_data_extractor.ts +++ b/src/helpers/figures/charts/runtime/chart_data_extractor.ts @@ -1,4 +1,11 @@ -import { _t, deepCopy, findNextDefinedValue, isNumber, range } from "@odoo/o-spreadsheet-engine"; +import { + _t, + deepCopy, + DEFAULT_LOCALE, + findNextDefinedValue, + isNumber, + range, +} from "@odoo/o-spreadsheet-engine"; import { ChartTerms } from "@odoo/o-spreadsheet-engine/components/translations_terms"; import { evaluatePolynomial, @@ -8,7 +15,12 @@ import { polynomialRegression, predictLinearValues, } from "@odoo/o-spreadsheet-engine/functions/helper_statistical"; -import { isEvaluationError, toNumber } from "@odoo/o-spreadsheet-engine/functions/helpers"; +import { toNumber } from "@odoo/o-spreadsheet-engine/functions/helpers"; +import { + isErrorCell, + isNumberCell, + isTextCell, +} from "@odoo/o-spreadsheet-engine/helpers/cells/cell_evaluation"; import { shouldRemoveFirstLabel } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; import { DAYS, isDateTimeFormat, MONTHS } from "@odoo/o-spreadsheet-engine/helpers/format/format"; import { createDate } from "@odoo/o-spreadsheet-engine/helpers/pivot/spreadsheet_pivot/date_spreadsheet_pivot"; @@ -41,8 +53,9 @@ import { TreeMapChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart/t import { Point } from "chart.js"; import { CellValue, - DEFAULT_LOCALE, + CellValueType, Format, + FunctionResultObject, GenericDefinition, Getters, Locale, @@ -50,6 +63,10 @@ import { } from "../../../../types"; import { timeFormatLuxonCompatible } from "../../../chart_date"; +const EMPTY = Object.freeze({ value: null }); +const ZERO = Object.freeze({ value: 0 }); +const ONE = Object.freeze({ value: 1 }); + export function getBarChartData( definition: GenericDefinition, dataSets: DataSet[], @@ -125,12 +142,12 @@ function getDateTimeLabel(value: number, stamp: CalendarChartGranularity): strin function computeValuesAndLabels( timeValues: CellValue[], - values: CellValue[], + values: FunctionResultObject[], horizontalGroupBy: CalendarChartGranularity, verticalGroupBy: CalendarChartGranularity, locale: Locale ) { - const grouping = {}; + const grouping: Record> = {}; const xValues: number[] = []; const yValues: number[] = []; const previousYValues: number[] = []; @@ -160,9 +177,12 @@ function computeValuesAndLabels( previousYValues.push(yValue); } if (!(yValue in grouping[xValue])) { - grouping[xValue][yValue] = 0; + grouping[xValue][yValue] = { value: 0 }; + } + const cell = values[i]; + if (isNumberCell(cell)) { + grouping[xValue][yValue].value += cell.value; } - grouping[xValue][yValue] += values[i]; } xValues.sort((a, b) => a - b); @@ -227,11 +247,15 @@ export function getPyramidChartData( const pyramidDatasetValues: DatasetValues[] = []; if (barDataset[0]) { - const pyramidData = barDataset[0].data.map((value) => (value > 0 ? value : 0)); + const pyramidData = barDataset[0].data.map((cell) => + isNumberCell(cell) && cell.value > 0 ? cell : ZERO + ); pyramidDatasetValues.push({ ...barDataset[0], data: pyramidData }); } if (barDataset[1]) { - const pyramidData = barDataset[1].data.map((value) => (value > 0 ? -value : 0)); + const pyramidData = barDataset[1].data.map((cell) => + isNumberCell(cell) && cell.value > 0 ? { value: -cell.value } : ZERO + ); pyramidDatasetValues.push({ ...barDataset[1], data: pyramidData }); } @@ -455,13 +479,17 @@ export function getHierarchalChartData( }; } -export function getTrendDatasetForBarChart(config: TrendConfiguration, data: any[]) { +export function getTrendDatasetForBarChart( + config: TrendConfiguration, + data: FunctionResultObject[] +) { const filteredValues: number[] = []; const filteredLabels: number[] = []; const labels: number[] = []; for (let i = 0; i < data.length; i++) { - if (typeof data[i] === "number") { - filteredValues.push(data[i]); + const cell = data[i]; + if (isNumberCell(cell)) { + filteredValues.push(cell.value); filteredLabels.push(i + 1); } labels.push(i + 1); @@ -474,7 +502,7 @@ export function getTrendDatasetForBarChart(config: TrendConfiguration, data: any export function getTrendDatasetForLineChart( config: TrendConfiguration, - data: any[], + data: FunctionResultObject[], labels: string[], axisType: AxisType, locale: Locale @@ -491,8 +519,9 @@ export function getTrendDatasetForLineChart( switch (axisType) { case "category": for (let i = 0; i < datasetLength; i++) { - if (typeof data[i] === "number") { - filteredValues.push(data[i]); + const cell = data[i]; + if (isNumberCell(cell)) { + filteredValues.push(cell.value); filteredLabels.push(i + 1); } trendLabels.push(i + 1); @@ -504,8 +533,9 @@ export function getTrendDatasetForLineChart( if (isNaN(label)) { continue; } - if (typeof data[i] === "number") { - filteredValues.push(data[i]); + const cell = data[i]; + if (isNumberCell(cell)) { + filteredValues.push(cell.value); filteredLabels.push(label); } trendLabels.push(label); @@ -514,8 +544,9 @@ export function getTrendDatasetForLineChart( case "time": for (let i = 0; i < data.length; i++) { const date = toNumber({ value: labels[i] }, locale); - if (data[i] !== null) { - filteredValues.push(data[i]); + const cell = data[i]; + if (isNumberCell(cell)) { + filteredValues.push(cell.value); filteredLabels.push(date); } trendLabels.push(date); @@ -710,10 +741,10 @@ function canBeLinearChart( labels.shift(); } - if (labels.some((label) => isNaN(Number(label)) && label)) { + if (labels.some((label) => label.type !== CellValueType.number && label.value)) { return false; } - if (labels.every((label) => !label)) { + if (labels.every((label) => !label.value)) { return false; } @@ -747,14 +778,14 @@ function keepOnlyPositiveValues( ...datasets.map((dataset) => dataset.data?.length || 0) ); const filteredIndexes = range(0, numberOfDataPoints).filter((i) => - datasets.some((ds) => typeof ds.data[i] === "number" && ds.data[i] > 0) + datasets.some((ds) => isNumberCell(ds.data[i]) && ds.data[i].value > 0) ); return { labels: filteredIndexes.map((i) => labels[i] || ""), dataSetsValues: datasets.map((ds) => ({ ...ds, data: filteredIndexes.map((i) => - typeof ds.data[i] === "number" && ds.data[i] > 0 ? ds.data[i] : null + isNumberCell(ds.data[i]) && ds.data[i].value > 0 ? ds.data[i] : EMPTY ), })), }; @@ -773,7 +804,7 @@ function fixEmptyLabelsForDateCharts( if (!newLabels[i]) { newLabels[i] = findNextDefinedValue(newLabels, i); for (const ds of newDatasets) { - ds.data[i] = undefined; + ds.data[i] = EMPTY; } } } @@ -783,7 +814,7 @@ function fixEmptyLabelsForDateCharts( /** * Get the data from a dataSet */ -export function getData(getters: Getters, ds: DataSet): (CellValue | undefined)[] { +export function getData(getters: Getters, ds: DataSet): FunctionResultObject[] { if (ds.dataRange) { const labelCellZone = ds.labelCell ? [ds.labelCell.zone] : []; const dataZone = recomputeZones([ds.dataRange.zone], labelCellZone)[0]; @@ -791,7 +822,7 @@ export function getData(getters: Getters, ds: DataSet): (CellValue | undefined)[ return []; } const dataRange = getters.getRangeFromZone(ds.dataRange.sheetId, dataZone); - return getters.getRangeValues(dataRange).map((value) => (value === "" ? undefined : value)); + return getters.getRangeValues(dataRange).map((cell) => (cell.value === "" ? EMPTY : cell)); } return []; } @@ -812,15 +843,13 @@ function filterInvalidDataPoints( const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => { const label = labels[dataPointIndex]; const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]); - return label || values.some((value) => typeof value === "number"); + return label || values.some(isNumberCell); }); return { labels: dataPointsIndexes.map((i) => labels[i] || ""), dataSetsValues: datasets.map((dataset) => ({ ...dataset, - data: dataPointsIndexes.map((i) => - typeof dataset.data[i] === "number" ? dataset.data[i] : null - ), + data: dataPointsIndexes.map((i) => (isNumberCell(dataset.data[i]) ? dataset.data[i] : EMPTY)), })), }; } @@ -842,15 +871,13 @@ function filterInvalidCalendarDataPoints( const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => { const label = labels[dataPointIndex]; const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]); - return label && isNumber(label, DEFAULT_LOCALE) && typeof values[0] === "number"; + return label && isNumber(label, DEFAULT_LOCALE) && isNumberCell(values[0]); }); return { labels: dataPointsIndexes.map((i) => labels[i] || ""), dataSetsValues: datasets.map((dataset) => ({ ...dataset, - data: dataPointsIndexes.map((i) => - typeof dataset.data[i] === "number" ? dataset.data[i] : null - ), + data: dataPointsIndexes.map((i) => (isNumberCell(dataset.data[i]) ? dataset.data[i] : EMPTY)), })), }; } @@ -866,17 +893,17 @@ function filterInvalidHierarchicalPoints( values.length, ...hierarchy.map((dataset) => dataset.data?.length || 0) ); - const isEmpty = (value: CellValue) => value === undefined || value === null || value === ""; + const isEmpty = (value: CellValue) => value === null || value === ""; const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => { const groups = hierarchy.map((dataset) => dataset.data?.[dataPointIndex]); - if (isEmpty(groups[0])) { + if (isEmpty(groups[0]?.value)) { return false; } // Filter points with empty group in the middle let hasFoundEmptyGroup = false; for (const group of groups) { - hasFoundEmptyGroup ||= isEmpty(group); - if (hasFoundEmptyGroup && !isEmpty(group)) { + hasFoundEmptyGroup ||= isEmpty(group?.value); + if (hasFoundEmptyGroup && !isEmpty(group?.value)) { return false; } } @@ -925,7 +952,7 @@ function aggregateDataForLabels( labels: string[], datasets: DatasetValues[] ): { labels: string[]; dataSetsValues: DatasetValues[] } { - const parseNumber = (value) => (typeof value === "number" ? value : 0); + const parseNumber = (value: CellValue) => (typeof value === "number" ? value : 0); const labelSet = new Set(labels); const labelMap: { [key: string]: number[] } = {}; labelSet.forEach((label) => { @@ -935,7 +962,9 @@ function aggregateDataForLabels( for (const indexOfLabel of range(0, labels.length)) { const label = labels[indexOfLabel]; for (const indexOfDataset of range(0, datasets.length)) { - labelMap[label][indexOfDataset] += parseNumber(datasets[indexOfDataset].data[indexOfLabel]); + labelMap[label][indexOfDataset] += parseNumber( + datasets[indexOfDataset].data[indexOfLabel]?.value + ); } } @@ -943,7 +972,7 @@ function aggregateDataForLabels( labels: Array.from(labelSet), dataSetsValues: datasets.map((dataset, indexOfDataset) => ({ ...dataset, - data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]), + data: Array.from(labelSet).map((label) => ({ value: labelMap[label][indexOfDataset] })), })), }; } @@ -982,7 +1011,7 @@ function getChartLabelValues( ) { labels = { formattedValues: getters.getRangeFormattedValues(labelRange), - values: getters.getRangeValues(labelRange).map((val) => String(val ?? "")), + values: getters.getRangeValues(labelRange).map(({ value }) => String(value ?? "")), }; } else if (dataSets[0]) { const ranges = getData(getters, dataSets[0]); @@ -1042,16 +1071,12 @@ function getChartDatasetValues(getters: Getters, dataSets: DataSet[]): DatasetVa let data = ds.dataRange ? getData(getters, ds) : []; if ( - data.every((e) => !e || (typeof e === "string" && !isEvaluationError(e))) && - data.filter((e) => typeof e === "string").length > 1 + data.every((cell) => !cell.value || isTextCell(cell)) && + data.filter(isTextCell).length > 1 ) { // Convert categorical data into counts - data = data.map((e) => (e && !isEvaluationError(e) ? 1 : null)); - } else if ( - data.every( - (cell) => cell === undefined || cell === null || !isNumber(cell.toString(), DEFAULT_LOCALE) - ) - ) { + data = data.map((cell) => (!isErrorCell(cell) ? ONE : EMPTY)); + } else if (data.every((cell) => !isNumberCell(cell))) { hidden = true; } datasetValues.push({ data, label, hidden }); @@ -1083,24 +1108,24 @@ function getHierarchicalDatasetValues(getters: Getters, dataSets: DataSet[]): Da } const minLength = Math.min(...dataSetsData.map((ds) => ds.length)); - let currentValues: (CellValue | undefined)[] = []; + let currentValues: FunctionResultObject[] = []; const leafDatasetIndex = dataSets.length - 1; for (let i = 0; i < minLength; i++) { for (let dsIndex = 0; dsIndex < dataSetsData.length; dsIndex++) { - let value = dataSetsData[dsIndex][i]; - if ((value === undefined || value === null) && dsIndex !== leafDatasetIndex) { - value = currentValues[dsIndex]; + let cell = dataSetsData[dsIndex][i]; + if ((cell === undefined || cell.value === null) && dsIndex !== leafDatasetIndex) { + cell = currentValues[dsIndex]; } - if (value !== currentValues[dsIndex]) { + if (cell?.value !== currentValues[dsIndex]?.value) { currentValues = currentValues.slice(0, dsIndex); - currentValues[dsIndex] = value; + currentValues[dsIndex] = cell; } - datasetValues[dsIndex].data.push(value ?? null); + datasetValues[dsIndex].data.push(cell ?? EMPTY); } } - return datasetValues.filter((ds) => ds.data.some((d) => d !== null)); + return datasetValues.filter((ds) => ds.data.some((d) => d.value !== null)); } export function makeDatasetsCumulative( @@ -1108,16 +1133,17 @@ export function makeDatasetsCumulative( order: "asc" | "desc" ): DatasetValues[] { return datasets.map((dataset) => { - const data: number[] = []; + const data: { value: number | null; format?: Format }[] = []; let accumulator = 0; const indexes = - order === "asc" ? Object.keys(dataset.data) : Object.keys(dataset.data).reverse(); + order === "asc" ? range(0, dataset.data.length) : range(0, dataset.data.length).reverse(); for (const i of indexes) { - if (!isNaN(parseFloat(dataset.data[i]))) { - accumulator += parseFloat(dataset.data[i]); - data[i] = accumulator; + const cell = dataset.data[i]; + if (isNumberCell(cell)) { + accumulator += cell.value; + data[i] = { ...cell, value: accumulator }; } else { - data[i] = dataset.data[i]; + data[i] = EMPTY; } } return { ...dataset, data }; diff --git a/src/helpers/figures/charts/runtime/chartjs_dataset.ts b/src/helpers/figures/charts/runtime/chartjs_dataset.ts index 434ba44c0b..4bf540b775 100644 --- a/src/helpers/figures/charts/runtime/chartjs_dataset.ts +++ b/src/helpers/figures/charts/runtime/chartjs_dataset.ts @@ -8,6 +8,8 @@ import { LINE_DATA_POINT_RADIUS, LINE_FILL_TRANSPARENCY, } from "@odoo/o-spreadsheet-engine/constants"; +import { tryToNumber } from "@odoo/o-spreadsheet-engine/functions/helpers"; +import { isNumberCell } from "@odoo/o-spreadsheet-engine/helpers/cells/cell_evaluation"; import { ColorGenerator, colorToRGBA, @@ -81,7 +83,7 @@ export function getBarChartDatasets( const backgroundColor = colors.next(); const dataset: ChartDataset<"bar"> = { label, - data, + data: data.map((cell) => (isNumberCell(cell) ? cell.value : NaN)), hidden, borderColor: definition.background || BACKGROUND_CHART_COLOR, borderWidth: definition.stacked ? 1 : 0, @@ -119,7 +121,8 @@ export function getCalendarChartDatasetAndLabels( const values = dataSetsValues .map((ds) => ds.data) .flat() - .filter(isDefined); + .filter(isNumberCell) + .map((cell) => cell.value); const maxValue = Math.max(...values); const minValue = Math.min(...values); @@ -135,14 +138,14 @@ export function getCalendarChartDatasetAndLabels( label: dataSetValues.label, data: dataSetValues.data.map((v) => 1), backgroundColor: dataSetValues.data.map((v) => - v !== undefined ? colorMap(v) : definition.missingValueColor || COLOR_TRANSPARENT + isNumberCell(v) ? colorMap(v.value) : definition.missingValueColor || COLOR_TRANSPARENT ), borderColor: definition.background || BACKGROUND_CHART_COLOR, borderSkipped: false, borderWidth: 1, barPercentage: 1, categoryPercentage: 1, - values: dataSetValues.data, + values: dataSetValues.data.map((cell) => (isNumberCell(cell) ? cell.value : NaN)), }); } @@ -179,20 +182,20 @@ export function getWaterfallDatasetAndLabels( continue; } for (let i = 0; i < dataSetsValue.data.length; i++) { - const data = dataSetsValue.data[i]; + const cell = dataSetsValue.data[i]; labelsWithSubTotals.push(labels[i]); - if (isNaN(Number(data))) { + if (!isNumberCell(cell)) { datasetValues.push([lastValue, lastValue]); backgroundColor.push(""); continue; } - datasetValues.push([lastValue, data + lastValue]); - let color = data >= 0 ? positiveColor : negativeColor; + datasetValues.push([lastValue, cell.value + lastValue]); + let color = cell.value >= 0 ? positiveColor : negativeColor; if (i === 0 && dataSetsValue === dataSetsValues[0] && definition.firstValueAsSubtotal) { color = subTotalColor; } backgroundColor.push(color); - lastValue += data; + lastValue += cell.value; } if (definition.showSubTotals) { labelsWithSubTotals.push(_t("Subtotal")); @@ -223,16 +226,22 @@ export function getLineChartDatasets( for (let index = 0; index < dataSetsValues.length; index++) { let { label, data, hidden } = dataSetsValues[index]; label = definition.dataSets?.[index].label || label; + let dataValues: (number | { x: number; y: number })[] = []; const color = colors.next(); if (axisType && ["linear", "time"].includes(axisType)) { // Replace empty string labels by undefined to make sure chartJS doesn't decide that "" is the same as 0 - data = data.map((y, index) => ({ x: labels[index] || undefined, y })); + dataValues = data.map((y, index) => ({ + x: labels[index] === "" ? NaN : tryToNumber(labels[index], args.locale) ?? NaN, + y: isNumberCell(y) ? y.value : NaN, + })); + } else { + dataValues = data.map((cell) => (isNumberCell(cell) ? cell.value : NaN)); } const dataset: ChartDataset<"line"> = { label, - data, + data: dataValues, hidden, tension: 0, // 0 -> render straight lines, which is much faster borderColor: color, @@ -282,12 +291,12 @@ export function getPieChartDatasets( if (hidden) continue; const dataset: ChartDataset<"pie"> = { label, - data, + data: data.map((cell) => (isNumberCell(cell) ? cell.value : NaN)), borderColor: definition.background || "#FFFFFF", backgroundColor, hoverOffset: 10, }; - dataSets!.push(dataset); + dataSets.push(dataset); } return dataSets; } @@ -315,7 +324,7 @@ export function getComboChartDatasets( const type = design?.type ?? "line"; const dataset: ChartDataset<"bar" | "line"> = { label: label, - data, + data: data.map((cell) => (isNumberCell(cell) ? cell.value : null)), hidden, borderColor: color, backgroundColor: color, @@ -363,7 +372,7 @@ export function getRadarChartDatasets( const borderColor = colors.next(); const dataset: ChartDataset<"radar"> = { label, - data, + data: data.map((cell) => (isNumberCell(cell) ? cell.value : null)), hidden, borderColor, backgroundColor: borderColor, @@ -397,12 +406,16 @@ export function getGeoChartDatasets( const labelsAndValues: { [featureId: string]: { value: number; label: string } } = {}; if (dataSetsValues[0]) { for (let i = 0; i < dataSetsValues[0].data.length; i++) { - if (!labels[i] || dataSetsValues[0].data[i] === undefined) { + const cell = dataSetsValues[0].data[i]; + if (!labels[i] || cell === undefined) { continue; } const featureId = args.geoFeatureNameToId(regionName, labels[i]); if (featureId) { - labelsAndValues[featureId] = { value: dataSetsValues[0].data[i], label: labels[i] }; + labelsAndValues[featureId] = { + value: isNumberCell(cell) ? cell.value : 0, + label: labels[i], + }; } } } @@ -439,7 +452,13 @@ export function getFunnelChartDatasets( const dataset: ChartDataset<"bar"> = { label: datasetLabel, - data: data.map((value) => (value <= 0 ? [0, 0] : [-value, value])), + data: data.map((cell) => { + if (!isNumberCell(cell)) { + return 0; + } + const value = cell.value; + return value <= 0 ? [0, 0] : [-value, value]; + }), backgroundColor: getFunnelLabelColors(labels, definition.funnelColors), yAxisID: "y", xAxisID: "x", @@ -507,10 +526,8 @@ function getDataEntriesFromDatasets(hierarchicalDatasetValues: DatasetValues[], for (let i = 0; i < maxDatasetLength; i++) { entries[i] = {}; for (let j = 0; j < hierarchicalDatasetValues.length; j++) { - const groupBy = - hierarchicalDatasetValues[j].data[i] === null - ? GHOST_SUNBURST_VALUE - : String(hierarchicalDatasetValues[j].data[i]); + const value = hierarchicalDatasetValues[j].data[i]?.value; + const groupBy = value === null ? GHOST_SUNBURST_VALUE : String(value); entries[i][j] = groupBy; } entries[i].value = Number(values[i]); @@ -602,8 +619,8 @@ export function getTreeMapChartDatasets( for (let i = 0; i < maxDatasetLength; i++) { datasetEntries[i] = {}; for (let j = 0; j < dataSetsValues.length; j++) { - datasetEntries[i][j] = dataSetsValues[j].data[i] - ? String(dataSetsValues[j].data[i]) + datasetEntries[i][j] = dataSetsValues[j].data[i].value + ? String(dataSetsValues[j].data[i].value) : undefined; } datasetEntries[i].value = Number(labels[i]); diff --git a/src/helpers/figures/charts/runtime/chartjs_scales.ts b/src/helpers/figures/charts/runtime/chartjs_scales.ts index a7d28fc5b1..c2a4dc0585 100644 --- a/src/helpers/figures/charts/runtime/chartjs_scales.ts +++ b/src/helpers/figures/charts/runtime/chartjs_scales.ts @@ -6,6 +6,7 @@ import { DEFAULT_CHART_COLOR_SCALE, GRAY_300, } from "@odoo/o-spreadsheet-engine/constants"; +import { isNumberCell } from "@odoo/o-spreadsheet-engine/helpers/cells/cell_evaluation"; import { COLORSCHEMES, getColorScale } from "@odoo/o-spreadsheet-engine/helpers/color"; import { MOVING_AVERAGE_TREND_LINE_XAXIS_ID, @@ -141,7 +142,10 @@ export function getCalendarColorScale( if (!dataSetsValues.length || definition.legendPosition === "none") { return undefined; } - const allValues = dataSetsValues.flatMap((ds) => ds.data).filter(isDefined); + const allValues = dataSetsValues + .flatMap((ds) => ds.data) + .filter(isNumberCell) + .map((cell) => cell.value); const minValue = Math.min(...allValues); const maxValue = Math.max(...allValues); let colorScale: Color[] = []; @@ -283,7 +287,9 @@ export function getPyramidChartScales( scales!.x!.ticks!.callback = (value: number) => scalesXCallback(Math.abs(value)); const maxValue = Math.max( - ...dataSetsValues.map((dataSet) => Math.max(...dataSet.data.map(Math.abs))) + ...dataSetsValues.map((dataSet) => + Math.max(...dataSet.data.filter(isNumberCell).map((x) => Math.abs(x.value))) + ) ); scales!.x!.suggestedMin = -maxValue; scales!.x!.suggestedMax = maxValue; @@ -297,7 +303,9 @@ export function getRadarChartScales( ): ChartScales { const { locale, axisFormats, dataSetsValues } = args; const minValue = Math.min( - ...dataSetsValues.map((ds) => Math.min(...ds.data.filter((x) => !isNaN(x)))) + ...dataSetsValues.map((ds) => + Math.min(...ds.data.filter(isNumberCell).map((x) => x.value as number)) + ) ); return { r: { @@ -376,12 +384,20 @@ export function getFunnelChartScales( border: { display: false }, ticks: { callback: function (tickValue, index, ticks) { - const value = dataSet.data?.[index]; - const baseValue = dataSet.data?.[0]; - if (!baseValue || value === undefined) { + const valueCell = dataSet.data?.[index]; + const baseValueCell = dataSet.data?.[0]; + if ( + !baseValueCell?.value || + valueCell?.value === null || + !isNumberCell(valueCell) || + !isNumberCell(baseValueCell) + ) { return ""; } - return formatValue(value / baseValue, { format: "0%", locale: args.locale }); + return formatValue(valueCell.value / baseValueCell.value, { + format: "0%", + locale: args.locale, + }); }, }, grid: { display: false }, diff --git a/src/helpers/figures/charts/runtime/chartjs_show_values.ts b/src/helpers/figures/charts/runtime/chartjs_show_values.ts index 44fdf80de7..32348639c1 100644 --- a/src/helpers/figures/charts/runtime/chartjs_show_values.ts +++ b/src/helpers/figures/charts/runtime/chartjs_show_values.ts @@ -1,3 +1,4 @@ +import { isNumberCell } from "@odoo/o-spreadsheet-engine/helpers/cells/cell_evaluation"; import { chartFontColor, formatChartDatasetValue, @@ -40,11 +41,10 @@ export function getCalendarChartShowValues( ): ChartShowValuesPluginOptions { const { locale, axisFormats } = args; let background = (_value, dataset, index) => definition.background; - const values = - args.dataSetsValues - .flat() - .map((dsv) => dsv?.data.filter((v) => v !== null && v !== undefined)) - .flat() || []; + const values = args.dataSetsValues + .flat() + .flatMap((dsv) => dsv?.data.filter(isNumberCell)) + .map((cell) => cell.value); if (values.length) { const min = Math.min(...values); const max = Math.max(...values); diff --git a/tests/figures/chart/chart_plugin.test.ts b/tests/figures/chart/chart_plugin.test.ts index 7cb39666c7..fc912993dd 100644 --- a/tests/figures/chart/chart_plugin.test.ts +++ b/tests/figures/chart/chart_plugin.test.ts @@ -439,7 +439,7 @@ describe("datasource tests", function () { const config = getChartConfiguration(model, "43"); // In line/bars charts we want to keep invalid data that have a label to have a discontinuous line/empty space between bars - expect(config.data?.datasets![0].data).toEqual([null, 12]); + expect(config.data?.datasets![0].data).toEqual([NaN, 12]); } ); @@ -1943,8 +1943,8 @@ describe("Chart design configuration", () => { ); const data = getChartConfiguration(model, "1").data; expect(data.labels).toEqual(["P1", "", ""]); - expect(data.datasets![0].data).toEqual([null, 10, null]); - expect(data.datasets![1].data).toEqual([null, null, 20]); + expect(data.datasets![0].data).toEqual([NaN, 10, NaN]); + expect(data.datasets![1].data).toEqual([NaN, NaN, 20]); }); test("value without matching index in the label set", () => { @@ -2836,7 +2836,10 @@ describe("Linear/Time charts", () => { setCellContent(model, "C3", ""); const data = getChartConfiguration(model, chartId).data; expect(data.labels![1]).toEqual("1/17/1900"); - expect(data.datasets![0].data![1]).toEqual({ y: undefined, x: "1/17/1900" }); + expect(data.datasets![0].data![1]).toEqual({ + y: NaN, + x: toNumber("1/17/1900", model.getters.getLocale()), + }); }); test("date chart: rows datasets/labels are supported", () => { @@ -2856,8 +2859,8 @@ describe("Linear/Time charts", () => { const chart = (model.getters.getChartRuntime(chartId) as LineChartRuntime).chartJsConfig; expect(chart.data!.datasets![0].data).toEqual([ - { y: 1, x: "2" }, - { y: 10, x: "01/02/1900" }, + { y: 1, x: 2 }, + { y: 10, x: toNumber("01/02/1900", model.getters.getLocale()) }, ]); expect(chart.options?.scales?.x?.type).toEqual("time"); }); @@ -2881,8 +2884,8 @@ describe("Linear/Time charts", () => { const data = getChartConfiguration(model, chartId).data; expect(data.labels).toEqual(["0", "1"]); expect(data.datasets![0].data).toEqual([ - { y: 0, x: "0" }, - { y: 1, x: "1" }, + { y: 0, x: 0 }, + { y: 1, x: 1 }, ]); }); @@ -2901,7 +2904,7 @@ describe("Linear/Time charts", () => { setCellContent(model, "C3", ""); const data = getChartConfiguration(model, chartId).data; expect(data.labels![1]).toEqual(""); - expect(data.datasets![0].data![1]).toEqual({ y: 11, x: undefined }); + expect(data.datasets![0].data![1]).toEqual({ y: 11, x: NaN }); }); test("can create linear chart with non-number header in the label range", () => { @@ -2920,7 +2923,7 @@ describe("Linear/Time charts", () => { const chart = (model.getters.getChartRuntime(chartId) as LineChartRuntime).chartJsConfig; expect(chart.options?.scales?.x?.type).toEqual("linear"); expect(chart.data!.labels).toEqual(["1"]); - expect(chart.data!.datasets![0].data).toEqual([{ y: 10, x: "1" }]); + expect(chart.data!.datasets![0].data).toEqual([{ y: 10, x: 1 }]); }); test("ChartJS configuration for linear chart", () => { @@ -2942,10 +2945,10 @@ describe("Linear/Time charts", () => { datasets: [ { data: [ - { x: "20", y: 10 }, - { x: "19", y: 11 }, - { x: "18", y: 12 }, - { x: "17", y: 13 }, + { x: 20, y: 10 }, + { x: 19, y: 11 }, + { x: 18, y: 12 }, + { x: 17, y: 13 }, ], }, ], @@ -2980,10 +2983,10 @@ describe("Linear/Time charts", () => { datasets: [ { data: [ - { x: "1/19/1900", y: 10 }, - { x: "1/18/1900", y: 11 }, - { x: "1/17/1900", y: 12 }, - { x: "1/16/1900", y: 13 }, + { x: toNumber("1/19/1900", model.getters.getLocale()), y: 10 }, + { x: toNumber("1/18/1900", model.getters.getLocale()), y: 11 }, + { x: toNumber("1/17/1900", model.getters.getLocale()), y: 12 }, + { x: toNumber("1/16/1900", model.getters.getLocale()), y: 13 }, ], }, ], @@ -3267,8 +3270,8 @@ describe("Cumulative Data line chart", () => { ); const chartData = getChartConfiguration(model, "1").data!.datasets![0].data; - const initialData = [11, 12, 13, null, 30]; // null if for the non-number value with a label - const expectedCumulativeData = [11, 23, 36, null, 66]; + const initialData = [11, 12, 13, NaN, 30]; // NaN if for the non-number value with a label + const expectedCumulativeData = [11, 23, 36, NaN, 66]; expect(chartData).toEqual(initialData); @@ -3296,8 +3299,8 @@ describe("Cumulative Data line chart", () => { const runtime = model.getters.getChartRuntime("chartId") as LineChartRuntime; expect(runtime.chartJsConfig.data!.datasets![0].data).toEqual([ - { x: "1", y: 10 }, - { x: "2", y: 30 }, + { x: 1, y: 10 }, + { x: 2, y: 30 }, ]); }); }); @@ -3335,7 +3338,7 @@ describe("Pie chart invalid values", () => { }, "1" ); - const expectedData = [null, null, null, 42]; // negative & non-number values are replaced by null + const expectedData = [NaN, NaN, NaN, 42]; // negative & non-number values are replaced by NaN const expectedLabels = ["P2", "P3", "P4", ""]; const data = getChartConfiguration(model, "1").data; diff --git a/tests/test_helpers/getters_helpers.ts b/tests/test_helpers/getters_helpers.ts index caf59219d5..443bf2df35 100644 --- a/tests/test_helpers/getters_helpers.ts +++ b/tests/test_helpers/getters_helpers.ts @@ -166,7 +166,9 @@ export function getRangeValues( xc: string, sheetId: UID = model.getters.getActiveSheetId() ): (CellValue | undefined)[] { - return model.getters.getRangeValues(model.getters.getRangeFromSheetXC(sheetId, xc)); + return model.getters + .getRangeValues(model.getters.getRangeFromSheetXC(sheetId, xc)) + .map((cell) => cell.value); } /** From 8c4f3dd7bcfa7ef3a6510cabe59a99920c1d1638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Mon, 24 Nov 2025 16:54:58 +0100 Subject: [PATCH 3/9] [REF] charts: format doesn't depend on ranges The format of chart axis no longer depends on ranges, but now on the chart data. --- .../cell_evaluation/evaluation_plugin.ts | 10 ---- .../src/types/chart/chart.ts | 2 +- .../charts/runtime/chart_data_extractor.ts | 56 ++++++++++--------- tests/figures/chart/bar_chart_plugin.test.ts | 1 + tests/figures/chart/chart_plugin.test.ts | 5 +- .../figures/chart/combo_chart_plugin.test.ts | 3 +- .../pyramid_chart_plugin.test.ts | 6 +- 7 files changed, 44 insertions(+), 39 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts b/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts index c789b3ad66..e9d612f425 100644 --- a/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts +++ b/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts @@ -150,7 +150,6 @@ export class EvaluationPlugin extends CoreViewPlugin { "getCorrespondingFormulaCell", "getRangeFormattedValues", "getRangeValues", - "getRangeFormats", "getEvaluatedCell", "getEvaluatedCells", "getEvaluatedCellsInZone", @@ -265,15 +264,6 @@ export class EvaluationPlugin extends CoreViewPlugin { return this.mapVisiblePositions(range, (p) => this.getters.getEvaluatedCell(p)); } - /** - * Return the format of each cell in the range. - */ - getRangeFormats(range: Range): (Format | undefined)[] { - const sheet = this.getters.tryGetSheet(range.sheetId); - if (sheet === undefined) return []; - return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.format); - } - getEvaluatedCell(position: CellPosition): EvaluatedCell { return this.evaluator.getEvaluatedCell(position); } diff --git a/packages/o-spreadsheet-engine/src/types/chart/chart.ts b/packages/o-spreadsheet-engine/src/types/chart/chart.ts index aca319ad21..226756d9d0 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/chart.ts @@ -253,7 +253,7 @@ export interface ChartRuntimeGenerationArgs { export type GenericDefinition = Partial< Omit > & { - dataSets?: Omit[]; + dataSets: Omit[]; }; export interface ChartColorScale { diff --git a/src/helpers/figures/charts/runtime/chart_data_extractor.ts b/src/helpers/figures/charts/runtime/chart_data_extractor.ts index 92ba1f1fe1..53d436eb97 100644 --- a/src/helpers/figures/charts/runtime/chart_data_extractor.ts +++ b/src/helpers/figures/charts/runtime/chart_data_extractor.ts @@ -30,6 +30,7 @@ import { AxisType, BarChartDefinition, ChartRuntimeGenerationArgs, + CustomizedDataSet, DataSet, DatasetValues, FunnelChartDefinition, @@ -85,8 +86,8 @@ export function getBarChartData( ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues)); } - const leftAxisFormat = getChartDatasetFormat(getters, dataSets, "left"); - const rightAxisFormat = getChartDatasetFormat(getters, dataSets, "right"); + const leftAxisFormat = getChartDatasetFormat(definition.dataSets, dataSetsValues, "left"); + const rightAxisFormat = getChartDatasetFormat(definition.dataSets, dataSetsValues, "right"); const axisFormats = definition.horizontal ? { x: leftAxisFormat || rightAxisFormat } : { y: leftAxisFormat, y1: rightAxisFormat }; @@ -216,6 +217,7 @@ export function getCalendarChartData( const locale = getters.getLocale() || DEFAULT_LOCALE; ({ labels, dataSetsValues } = filterInvalidCalendarDataPoints(labels, dataSetsValues, locale)); + const axisFormats = { y: getChartDatasetFormat(definition.dataSets, dataSetsValues, "left") }; ({ labels, dataSetsValues } = computeValuesAndLabels( labels, @@ -225,8 +227,6 @@ export function getCalendarChartData( locale )); - const axisFormats = { y: getChartDatasetFormat(getters, dataSets, "left") }; - return { dataSetsValues, axisFormats, @@ -295,8 +295,8 @@ export function getLineChartData( dataSetsValues = makeDatasetsCumulative(dataSetsValues, "asc"); } - const leftAxisFormat = getChartDatasetFormat(getters, dataSets, "left"); - const rightAxisFormat = getChartDatasetFormat(getters, dataSets, "right"); + const leftAxisFormat = getChartDatasetFormat(definition.dataSets, dataSetsValues, "left"); + const rightAxisFormat = getChartDatasetFormat(definition.dataSets, dataSetsValues, "right"); const labelsFormat = getChartLabelFormat(getters, labelRange, removeFirstLabel); const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat, x: labelsFormat }; @@ -346,7 +346,7 @@ export function getPieChartData( ({ dataSetsValues, labels } = keepOnlyPositiveValues(labels, dataSetsValues)); - const dataSetFormat = getChartDatasetFormat(getters, dataSets, "left"); + const dataSetFormat = getChartDatasetFormat(definition.dataSets, dataSetsValues, "left"); return { dataSetsValues, @@ -376,8 +376,8 @@ export function getRadarChartData( } const dataSetFormat = - getChartDatasetFormat(getters, dataSets, "left") || - getChartDatasetFormat(getters, dataSets, "right"); + getChartDatasetFormat(definition.dataSets, dataSetsValues, "left") || + getChartDatasetFormat(definition.dataSets, dataSetsValues, "right"); const axisFormats = { r: dataSetFormat }; return { @@ -404,8 +404,8 @@ export function getGeoChartData( ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues)); const format = - getChartDatasetFormat(getters, dataSets, "left") || - getChartDatasetFormat(getters, dataSets, "right"); + getChartDatasetFormat(definition.dataSets, dataSetsValues, "left") || + getChartDatasetFormat(definition.dataSets, dataSetsValues, "right"); return { dataSetsValues, @@ -440,8 +440,8 @@ export function getFunnelChartData( } const format = - getChartDatasetFormat(getters, dataSets, "left") || - getChartDatasetFormat(getters, dataSets, "right"); + getChartDatasetFormat(definition.dataSets, dataSetsValues, "left") || + getChartDatasetFormat(definition.dataSets, dataSetsValues, "right"); return { dataSetsValues, @@ -954,17 +954,20 @@ function aggregateDataForLabels( ): { labels: string[]; dataSetsValues: DatasetValues[] } { const parseNumber = (value: CellValue) => (typeof value === "number" ? value : 0); const labelSet = new Set(labels); - const labelMap: { [key: string]: number[] } = {}; + const labelMap: { [key: string]: { value: number; format?: Format }[] } = {}; labelSet.forEach((label) => { - labelMap[label] = new Array(datasets.length).fill(0); + labelMap[label] = new Array(datasets.length); }); for (const indexOfLabel of range(0, labels.length)) { const label = labels[indexOfLabel]; for (const indexOfDataset of range(0, datasets.length)) { - labelMap[label][indexOfDataset] += parseNumber( - datasets[indexOfDataset].data[indexOfLabel]?.value - ); + const cell = datasets[indexOfDataset].data[indexOfLabel]; + if (!labelMap[label][indexOfDataset]) { + labelMap[label][indexOfDataset] = { ...cell, value: parseNumber(cell?.value) }; + } else { + labelMap[label][indexOfDataset].value += parseNumber(cell?.value); + } } } @@ -972,7 +975,7 @@ function aggregateDataForLabels( labels: Array.from(labelSet), dataSetsValues: datasets.map((dataset, indexOfDataset) => ({ ...dataset, - data: Array.from(labelSet).map((label) => ({ value: labelMap[label][indexOfDataset] })), + data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]), })), }; } @@ -1043,15 +1046,18 @@ function getChartLabelValues( * found in the dataset ranges that isn't a date format. */ function getChartDatasetFormat( - getters: Getters, - allDataSets: DataSet[], + dataSetDefinitions: Pick[], // TODO simplify once CustomizedDataSet no longer contains ranges + dataSetValues: DatasetValues[], axis: "left" | "right" ): Format | undefined { - const dataSets = allDataSets.filter((ds) => (axis === "right") === !!ds.rightYAxis); + const dataSets = dataSetValues.filter( + (ds, i) => (axis === "right") === (dataSetDefinitions[i].yAxisId === "y1") + ); for (const ds of dataSets) { - const formatsInDataset = getters.getRangeFormats(ds.dataRange); - const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f)); - if (format) return format; + const cell = ds.data.find(({ format }) => format !== undefined && !isDateTimeFormat(format)); + if (cell) { + return cell.format; + } } return undefined; } diff --git a/tests/figures/chart/bar_chart_plugin.test.ts b/tests/figures/chart/bar_chart_plugin.test.ts index 959e6dcea9..c8575cbb5d 100644 --- a/tests/figures/chart/bar_chart_plugin.test.ts +++ b/tests/figures/chart/bar_chart_plugin.test.ts @@ -76,6 +76,7 @@ describe("bar chart", () => { type: "bar", dataSets: [{ dataRange: "A1", yAxisId: "y" }], axesDesign: { x: { title: { text: "xAxis" } }, y: { title: { text: "yAxis" } } }, + dataSetsHaveTitle: false, }, "id" ); diff --git a/tests/figures/chart/chart_plugin.test.ts b/tests/figures/chart/chart_plugin.test.ts index fc912993dd..3953f4693c 100644 --- a/tests/figures/chart/chart_plugin.test.ts +++ b/tests/figures/chart/chart_plugin.test.ts @@ -2106,6 +2106,7 @@ describe("Chart design configuration", () => { test.each(["bar", "line", "scatter", "waterfall"])( "Bar/Line chart Y axis, cell with format", (chartType) => { + setCellContent(model, "A2", "10"); setCellFormat(model, "A2", "[$$]#,##0.00"); createChart(model, { ...defaultChart, type: chartType as "bar" | "line" }, "42"); expect( @@ -2134,8 +2135,10 @@ describe("Chart design configuration", () => { }, "42" ); + setCellContent(model, "A2", "10"); + setCellContent(model, "B2", "20"); setCellFormat(model, "A2", "[$$]#,#"); - setCellFormat(model, "B1", "0%"); + setCellFormat(model, "B2", "0%"); const config = model.getters.getChartRuntime("42") as any; const scales = config.chartJsConfig?.options?.scales; diff --git a/tests/figures/chart/combo_chart_plugin.test.ts b/tests/figures/chart/combo_chart_plugin.test.ts index 3b96955350..6dd36d1155 100644 --- a/tests/figures/chart/combo_chart_plugin.test.ts +++ b/tests/figures/chart/combo_chart_plugin.test.ts @@ -45,7 +45,8 @@ describe("combo chart", () => { test("both axis and tooltips formats are based on their data set", () => { const model = new Model(); - + setCellContent(model, "B1", "1000"); + setCellContent(model, "C1", "2000"); setCellFormat(model, "B1", "0.00%"); // first data set setCellFormat(model, "C1", "0.00[$$]"); // second data set diff --git a/tests/figures/chart/pyramid_chart/pyramid_chart_plugin.test.ts b/tests/figures/chart/pyramid_chart/pyramid_chart_plugin.test.ts index 8dd75711c3..baa393bf0a 100644 --- a/tests/figures/chart/pyramid_chart/pyramid_chart_plugin.test.ts +++ b/tests/figures/chart/pyramid_chart/pyramid_chart_plugin.test.ts @@ -78,7 +78,11 @@ describe("population pyramid chart", () => { createChart( model, - { type: "pyramid", dataSets: [{ dataRange: "A1" }, { dataRange: "A2" }] }, + { + type: "pyramid", + dataSets: [{ dataRange: "A1" }, { dataRange: "A2" }], + dataSetsHaveTitle: false, + }, "id" ); const runtime = model.getters.getChartRuntime("id") as any; From 2bd96d12736a188a3cf6d378367ce0c1a460dc9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Mon, 24 Nov 2025 17:00:30 +0100 Subject: [PATCH 4/9] [REF] charts: remove useless getter This getter is only used once and the formatted values can be taken from the evaluated cells without the need to iterate over the entire range again. --- .../cell_evaluation/evaluation_plugin.ts | 12 +----------- .../figures/charts/runtime/chart_data_extractor.ts | 5 +++-- tests/test_helpers/getters_helpers.ts | 4 +++- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts b/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts index e9d612f425..7e7a75be61 100644 --- a/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts +++ b/packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts @@ -10,7 +10,7 @@ import { invalidateDependenciesCommands, invalidateEvaluationCommands, } from "../../../types/commands"; -import { Format, FormattedValue } from "../../../types/format"; +import { Format } from "../../../types/format"; import { CellPosition, FunctionResultObject, @@ -148,7 +148,6 @@ export class EvaluationPlugin extends CoreViewPlugin { "evaluateFormulaResult", "evaluateCompiledFormula", "getCorrespondingFormulaCell", - "getRangeFormattedValues", "getRangeValues", "getEvaluatedCell", "getEvaluatedCells", @@ -246,15 +245,6 @@ export class EvaluationPlugin extends CoreViewPlugin { return this.evaluator.evaluateCompiledFormula(sheetId, compiledFormula, getSymbolValue); } - /** - * Return the value of each cell in the range as they are displayed in the grid. - */ - getRangeFormattedValues(range: Range): FormattedValue[] { - const sheet = this.getters.tryGetSheet(range.sheetId); - if (sheet === undefined) return []; - return this.mapVisiblePositions(range, (p) => this.getters.getEvaluatedCell(p).formattedValue); - } - /** * Return the value of each cell in the range. */ diff --git a/src/helpers/figures/charts/runtime/chart_data_extractor.ts b/src/helpers/figures/charts/runtime/chart_data_extractor.ts index 53d436eb97..c0c1ce8d40 100644 --- a/src/helpers/figures/charts/runtime/chart_data_extractor.ts +++ b/src/helpers/figures/charts/runtime/chart_data_extractor.ts @@ -1012,9 +1012,10 @@ function getChartLabelValues( !labelRange.invalidSheetName && !getters.isColHidden(labelRange.sheetId, left) ) { + const cells = getters.getRangeValues(labelRange); labels = { - formattedValues: getters.getRangeFormattedValues(labelRange), - values: getters.getRangeValues(labelRange).map(({ value }) => String(value ?? "")), + formattedValues: cells.map(({ formattedValue }) => formattedValue), + values: cells.map(({ value }) => String(value ?? "")), }; } else if (dataSets[0]) { const ranges = getData(getters, dataSets[0]); diff --git a/tests/test_helpers/getters_helpers.ts b/tests/test_helpers/getters_helpers.ts index 443bf2df35..4feeba51c9 100644 --- a/tests/test_helpers/getters_helpers.ts +++ b/tests/test_helpers/getters_helpers.ts @@ -158,7 +158,9 @@ export function getRangeFormattedValues( xc: string, sheetId: UID = model.getters.getActiveSheetId() ): FormattedValue[] { - return model.getters.getRangeFormattedValues(model.getters.getRangeFromSheetXC(sheetId, xc)); + return model.getters + .getRangeValues(model.getters.getRangeFromSheetXC(sheetId, xc)) + .map((cell) => cell.formattedValue); } export function getRangeValues( From dd90f0856ed478c5d4ddd0173ef07158e51d9aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Tue, 18 Nov 2025 14:55:48 +0100 Subject: [PATCH 5/9] [IMP] charts: validate definition --- .../helpers/figures/charts/abstract_chart.ts | 13 +++ .../helpers/figures/charts/chart_factory.ts | 5 + .../helpers/figures/charts/scorecard_chart.ts | 13 ++- .../src/migrations/data.ts | 1 + .../src/migrations/migration_steps.ts | 24 ++++- .../src/plugins/core/chart.ts | 4 +- .../src/registries/chart_registry.ts | 1 + .../src/types/chart/scatter_chart.ts | 2 +- .../src/types/validator.ts | 5 +- src/helpers/figures/charts/bar_chart.ts | 74 ++++++--------- src/helpers/figures/charts/calendar_chart.ts | 58 +++++------- src/helpers/figures/charts/combo_chart.ts | 69 +++++--------- src/helpers/figures/charts/funnel_chart.ts | 77 +++++---------- src/helpers/figures/charts/gauge_chart.ts | 8 +- src/helpers/figures/charts/geo_chart.ts | 57 ++++------- src/helpers/figures/charts/line_chart.ts | 89 ++++++------------ src/helpers/figures/charts/pie_chart.ts | 66 +++++-------- src/helpers/figures/charts/pyramid_chart.ts | 70 +++++--------- src/helpers/figures/charts/radar_chart.ts | 72 +++++--------- src/helpers/figures/charts/scatter_chart.ts | 73 +++++--------- src/helpers/figures/charts/sunburst_chart.ts | 64 +++++-------- src/helpers/figures/charts/tree_map_chart.ts | 67 +++++-------- src/helpers/figures/charts/waterfall_chart.ts | 94 ++++++------------- src/registries/chart_types.ts | 15 +++ .../__snapshots__/chart_plugin.test.ts.snap | 2 +- .../sunburst/sunburst_chart_plugin.test.ts | 1 + tests/model/model_import_export.test.ts | 33 +++++++ tests/test_helpers/commands_helpers.ts | 32 ++++++- 28 files changed, 461 insertions(+), 628 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts index c5152d0115..832564f12c 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts @@ -2,6 +2,7 @@ import { ChartCreationContext, ChartDefinition, ChartType, + ChartWithDataSetDefinition, DataSet, ExcelChartDataset, ExcelChartDefinition, @@ -26,6 +27,18 @@ export abstract class AbstractChart { protected readonly getters: CoreGetters; readonly humanize: boolean | undefined; + static commonKeys: readonly (keyof ChartDefinition)[] = [ + "type", + "title", + "background", + "humanize", + ]; + static dataSetKeys: readonly (keyof ChartWithDataSetDefinition)[] = [ + "dataSets", + "dataSetsHaveTitle", + "labelRange", + ]; + constructor(definition: ChartDefinition, sheetId: UID, getters: CoreGetters) { this.title = definition.title; this.sheetId = sheetId; diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_factory.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_factory.ts index 53f61ce5fc..77388420e6 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_factory.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_factory.ts @@ -58,6 +58,11 @@ export function validateChartDefinition( if (!validators) { throw new Error("Unknown chart type."); } + const allowedKeys = new Set(validators.allowedDefinitionKeys); + const hasExtraKeys = !new Set(Object.keys(definition)).isSubsetOf(allowedKeys); + if (hasExtraKeys) { + return CommandResult.InvalidChartDefinition; + } return validators.validateChartDefinition(validator, definition); } diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts index 7b451b6dab..6ee6119c22 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts @@ -171,6 +171,17 @@ export class ScorecardChart extends AbstractChart { readonly humanize: boolean; readonly type = "scorecard"; + static allowedDefinitionKeys: readonly (keyof ScorecardChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "keyValue", + "keyDescr", + "baseline", + "baselineMode", + "baselineDescr", + "baselineColorUp", + "baselineColorDown", + ] as const; + constructor(definition: ScorecardChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.keyValue = createValidRange(getters, sheetId, definition.keyValue); @@ -249,7 +260,7 @@ export class ScorecardChart extends AbstractChart { getContextCreation(): ChartCreationContext { return { - ...this, + ...this.getDefinition(), range: this.keyValue ? [{ dataRange: this.getters.getRangeString(this.keyValue, this.sheetId) }] : undefined, diff --git a/packages/o-spreadsheet-engine/src/migrations/data.ts b/packages/o-spreadsheet-engine/src/migrations/data.ts index c6c433191e..aa182556d4 100644 --- a/packages/o-spreadsheet-engine/src/migrations/data.ts +++ b/packages/o-spreadsheet-engine/src/migrations/data.ts @@ -308,6 +308,7 @@ function fixChartDefinitions(data: Partial, initialMessages: State const definition = map[cmd.chartId]; const newDefinition = { ...definition, ...cmd.definition }; command = { ...cmd, definition: newDefinition }; + delete newDefinition.chartId; map[cmd.chartId] = newDefinition; break; } diff --git a/packages/o-spreadsheet-engine/src/migrations/migration_steps.ts b/packages/o-spreadsheet-engine/src/migrations/migration_steps.ts index de9a9b7cde..53485445e2 100644 --- a/packages/o-spreadsheet-engine/src/migrations/migration_steps.ts +++ b/packages/o-spreadsheet-engine/src/migrations/migration_steps.ts @@ -6,6 +6,7 @@ import { getUniqueText, sanitizeSheetName } from "../helpers/misc"; import { getMaxObjectId } from "../helpers/pivot/pivot_helpers"; import { DEFAULT_TABLE_CONFIG } from "../helpers/table_presets"; import { overlap, toZone, zoneToXc } from "../helpers/zones"; +import { chartRegistry } from "../registries/chart_registry"; import { Registry } from "../registry"; import { CustomizedDataSet } from "../types/chart"; import { Format } from "../types/format"; @@ -579,7 +580,28 @@ migrationStepRegistry } } return data; - } + }, + }) + .add("19.1.2", { + migrate(data: WorkbookData): any { + for (const sheet of data.sheets || []) { + for (const figure of sheet.figures || []) { + if (figure.tag === "chart") { + const definition = figure.data; + const allowedDefinitionKeys = new Set( + chartRegistry.get(definition.type).allowedDefinitionKeys + ); + allowedDefinitionKeys.add("chartId"); + for (const key in definition) { + if (!allowedDefinitionKeys.has(key)) { + delete definition[key]; + } + } + } + } + } + return data; + }, }); function fixOverlappingFilters(data: any): any { diff --git a/packages/o-spreadsheet-engine/src/plugins/core/chart.ts b/packages/o-spreadsheet-engine/src/plugins/core/chart.ts index 83d54244dd..68825c85b2 100644 --- a/packages/o-spreadsheet-engine/src/plugins/core/chart.ts +++ b/packages/o-spreadsheet-engine/src/plugins/core/chart.ts @@ -220,7 +220,9 @@ export class ChartPlugin extends CorePlugin implements ChartState { // instead of in figure.data if (figure.tag === "chart") { const chartId = figure.data.chartId; - const chart = this.createChart(figure.id, figure.data, sheet.id); + const definition = { ...figure.data }; + delete definition.chartId; + const chart = this.createChart(figure.id, definition, sheet.id); this.charts[chartId] = { chart, figureId: figure.id }; } else if (figure.tag === "carousel") { for (const chartId in figure.data.chartDefinitions || {}) { diff --git a/packages/o-spreadsheet-engine/src/registries/chart_registry.ts b/packages/o-spreadsheet-engine/src/registries/chart_registry.ts index d980a51af9..5e3ddc70bc 100644 --- a/packages/o-spreadsheet-engine/src/registries/chart_registry.ts +++ b/packages/o-spreadsheet-engine/src/registries/chart_registry.ts @@ -27,6 +27,7 @@ export interface ChartBuilder { applyRange: RangeAdapter ): ChartDefinition; getChartDefinitionFromContextCreation(context: ChartCreationContext): ChartDefinition; + allowedDefinitionKeys: readonly string[]; sequence: number; dataSeriesLimit?: number; } diff --git a/packages/o-spreadsheet-engine/src/types/chart/scatter_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/scatter_chart.ts index 6b508e901d..ec1216ca17 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/scatter_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/scatter_chart.ts @@ -1,7 +1,7 @@ import { LineChartDefinition, LineChartRuntime } from "./line_chart"; export interface ScatterChartDefinition - extends Omit { + extends Omit { readonly type: "scatter"; } diff --git a/packages/o-spreadsheet-engine/src/types/validator.ts b/packages/o-spreadsheet-engine/src/types/validator.ts index 4921eb161e..297361af75 100644 --- a/packages/o-spreadsheet-engine/src/types/validator.ts +++ b/packages/o-spreadsheet-engine/src/types/validator.ts @@ -15,5 +15,8 @@ export interface Validator { */ chainValidations(...validations: Validation[]): Validation; - checkValidations(command: T, ...validations: Validation[]): CommandResult | CommandResult[]; + checkValidations( + command: T, + ...validations: Validation>[] + ): CommandResult | CommandResult[]; } diff --git a/src/helpers/figures/charts/bar_chart.ts b/src/helpers/figures/charts/bar_chart.ts index e1d83dbf36..91a2304739 100644 --- a/src/helpers/figures/charts/bar_chart.ts +++ b/src/helpers/figures/charts/bar_chart.ts @@ -20,17 +20,14 @@ import { BarChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/bar_chart"; import { - AxesDesign, ChartCreationContext, CustomizedDataSet, DataSet, - DatasetDesign, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; -import { LegendPosition } from "@odoo/o-spreadsheet-engine/types/chart/common_chart"; import { CommandResult } from "@odoo/o-spreadsheet-engine/types/commands"; import { Getters } from "@odoo/o-spreadsheet-engine/types/getters"; -import { ApplyRangeChange, Color, RangeAdapter, UID } from "@odoo/o-spreadsheet-engine/types/misc"; +import { ApplyRangeChange, RangeAdapter, UID } from "@odoo/o-spreadsheet-engine/types/misc"; import { Range } from "@odoo/o-spreadsheet-engine/types/range"; import { toXlsxHexColor } from "@odoo/o-spreadsheet-engine/xlsx/helpers/colors"; import type { ChartConfiguration } from "chart.js"; @@ -48,19 +45,23 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class BarChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; - readonly stacked: boolean; - readonly aggregated?: boolean; readonly type = "bar"; - readonly dataSetsHaveTitle: boolean; - readonly dataSetDesign?: DatasetDesign[]; - readonly axesDesign?: AxesDesign; - readonly horizontal?: boolean; - readonly showValues?: boolean; - readonly zoomable?: boolean; - - constructor(definition: BarChartDefinition, sheetId: UID, getters: CoreGetters) { + + static allowedDefinitionKeys: readonly (keyof BarChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "horizontal", + "axesDesign", + "stacked", + "aggregated", + "showValues", + "zoomable", + ] as const; + + constructor(private definition: BarChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -69,16 +70,6 @@ export class BarChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.stacked = definition.stacked; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.dataSetDesign = definition.dataSets; - this.axesDesign = definition.axesDesign; - this.horizontal = definition.horizontal; - this.showValues = definition.showValues; - this.zoomable = definition.zoomable; } static transformDefinition( @@ -119,12 +110,12 @@ export class BarChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -164,41 +155,32 @@ export class BarChart extends AbstractChart { const ranges: CustomizedDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), }); } return { - type: "bar", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - stacked: this.stacked, - aggregated: this.aggregated, - axesDesign: this.axesDesign, - horizontal: this.horizontal, - showValues: this.showValues, - zoomable: this.horizontal ? undefined : this.zoomable, - humanize: this.humanize, + zoomable: this.definition.horizontal ? undefined : this.definition.zoomable, }; } getDefinitionForExcel(): ExcelChartDefinition | undefined { + const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle) + shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) ); - const definition = this.getDefinition(); return { ...definition, - backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR), - fontColor: toXlsxHexColor(chartFontColor(this.background)), + backgroundColor: toXlsxHexColor(definition.background || BACKGROUND_CHART_COLOR), + fontColor: toXlsxHexColor(chartFontColor(definition.background)), dataSets, labelRange, verticalAxis: getDefinedAxis(definition), @@ -232,7 +214,7 @@ export function createBarChartRuntime(chart: BarChart, getters: Getters): BarCha }, options: { ...CHART_COMMON_OPTIONS, - indexAxis: chart.horizontal ? "y" : "x", + indexAxis: definition.horizontal ? "y" : "x", layout: getChartLayout(definition, chartData), scales: getBarChartScales(definition, chartData), plugins: { @@ -244,5 +226,5 @@ export function createBarChartRuntime(chart: BarChart, getters: Getters): BarCha }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/calendar_chart.ts b/src/helpers/figures/charts/calendar_chart.ts index c6398087cf..2d6b4ddad4 100644 --- a/src/helpers/figures/charts/calendar_chart.ts +++ b/src/helpers/figures/charts/calendar_chart.ts @@ -13,7 +13,6 @@ import { import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { - BarChartDefinition, BarChartRuntime, ChartCreationContext, CustomizedDataSet, @@ -23,14 +22,10 @@ import { import { CALENDAR_CHART_GRANULARITIES, CalendarChartDefinition, - CalendarChartGranularity, } from "@odoo/o-spreadsheet-engine/types/chart/calendar_chart"; import type { ChartConfiguration } from "chart.js"; import { ApplyRangeChange, - AxesDesign, - ChartColorScale, - Color, CommandResult, DataSet, Getters, @@ -62,17 +57,23 @@ function checkDateGranularity(definition: CalendarChartDefinition): CommandResul export class CalendarChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; readonly type = "calendar"; - readonly showValues?: boolean; - readonly colorScale?: ChartColorScale; - readonly axesDesign?: AxesDesign; - readonly horizontalGroupBy: CalendarChartGranularity; - readonly verticalGroupBy: CalendarChartGranularity; - readonly legendPosition: LegendPosition; - readonly missingValueColor?: Color; - - constructor(definition: CalendarChartDefinition, sheetId: UID, getters: CoreGetters) { + + static allowedDefinitionKeys: readonly (keyof CalendarChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "dataSets", + "labelRange", + "dataSetsHaveTitle", + "showValues", + "colorScale", + "missingValueColor", + "axesDesign", + "horizontalGroupBy", + "verticalGroupBy", + "legendPosition", + ] as const; + + constructor(private definition: CalendarChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -81,27 +82,19 @@ export class CalendarChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.showValues = definition.showValues; - this.colorScale = definition.colorScale; - this.axesDesign = definition.axesDesign; - this.horizontalGroupBy = definition.horizontalGroupBy; - this.verticalGroupBy = definition.verticalGroupBy; - this.legendPosition = definition.legendPosition; - this.missingValueColor = definition.missingValueColor; } static transformDefinition( chartSheetId: UID, - definition: BarChartDefinition, + definition: CalendarChartDefinition, applyChange: RangeAdapter - ): BarChartDefinition { + ): CalendarChartDefinition { return transformChartDefinitionWithDataSetsWithZone(chartSheetId, definition, applyChange); } static validateChartDefinition( validator: Validator, - definition: BarChartDefinition + definition: CalendarChartDefinition ): CommandResult | CommandResult[] { return validator.checkValidations( definition, @@ -177,21 +170,12 @@ export class CalendarChart extends AbstractChart { dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), })); return { - type: "calendar", - background: this.background, + ...this.definition, dataSets: ranges, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - showValues: this.showValues, - colorScale: this.colorScale, - axesDesign: this.axesDesign, - horizontalGroupBy: this.horizontalGroupBy, - verticalGroupBy: this.verticalGroupBy, - legendPosition: this.legendPosition, - missingValueColor: this.missingValueColor, }; } @@ -243,5 +227,5 @@ export function createCalendarChartRuntime( }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/combo_chart.ts b/src/helpers/figures/charts/combo_chart.ts index c26a267b51..26c910f355 100644 --- a/src/helpers/figures/charts/combo_chart.ts +++ b/src/helpers/figures/charts/combo_chart.ts @@ -15,11 +15,6 @@ import { } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; -import { - AxesDesign, - CustomizedDataSet, - LegendPosition, -} from "@odoo/o-spreadsheet-engine/types/chart"; import { ComboChartDataSet, ComboChartDefinition, @@ -30,8 +25,8 @@ import { ChartConfiguration } from "chart.js"; import { ApplyRangeChange, ChartCreationContext, - Color, CommandResult, + CustomizedDataSet, DataSet, ExcelChartDefinition, Getters, @@ -53,18 +48,22 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class ComboChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range; - readonly background?: Color; - readonly legendPosition: LegendPosition; - readonly aggregated?: boolean; - readonly dataSetsHaveTitle: boolean; - readonly dataSetDesign?: ComboChartDataSet[]; - readonly axesDesign?: AxesDesign; readonly type = "combo"; - readonly showValues?: boolean; - readonly hideDataMarkers?: boolean; - readonly zoomable?: boolean; - constructor(definition: ComboChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof ComboChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "aggregated", + "axesDesign", + "showValues", + "hideDataMarkers", + "zoomable", + ] as const; + + constructor(private definition: ComboChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -73,15 +72,6 @@ export class ComboChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.dataSetDesign = definition.dataSets; - this.axesDesign = definition.axesDesign; - this.showValues = definition.showValues; - this.hideDataMarkers = definition.hideDataMarkers; - this.zoomable = definition.zoomable; } static transformDefinition( @@ -103,12 +93,12 @@ export class ComboChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -128,41 +118,32 @@ export class ComboChart extends AbstractChart { const ranges: ComboChartDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"), + type: this.definition.dataSets?.[i]?.type ?? (i ? "line" : "bar"), }); } return { - type: "combo", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - aggregated: this.aggregated, - axesDesign: this.axesDesign, - showValues: this.showValues, - hideDataMarkers: this.hideDataMarkers, - zoomable: this.zoomable, - humanize: this.humanize, }; } getDefinitionForExcel(): ExcelChartDefinition | undefined { + const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle) + shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) ); - const definition = this.getDefinition(); return { ...definition, - backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR), - fontColor: toXlsxHexColor(chartFontColor(this.background)), + backgroundColor: toXlsxHexColor(definition.background || BACKGROUND_CHART_COLOR), + fontColor: toXlsxHexColor(chartFontColor(definition.background)), dataSets, labelRange, verticalAxis: getDefinedAxis(definition), @@ -249,5 +230,5 @@ export function createComboChartRuntime(chart: ComboChart, getters: Getters): Co }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/funnel_chart.ts b/src/helpers/figures/charts/funnel_chart.ts index e21028bb66..41cb234b63 100644 --- a/src/helpers/figures/charts/funnel_chart.ts +++ b/src/helpers/figures/charts/funnel_chart.ts @@ -12,30 +12,15 @@ import { } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; +import { FunnelChartDefinition, FunnelChartRuntime } from "@odoo/o-spreadsheet-engine/types/chart"; import { - FunnelChartColors, - FunnelChartDefinition, - FunnelChartRuntime, - LegendPosition, -} from "@odoo/o-spreadsheet-engine/types/chart"; -import { - AxesDesign, ChartCreationContext, CustomizedDataSet, DataSet, - DatasetDesign, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; import { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getChartShowValues, getChartTitle, @@ -49,19 +34,23 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class FunnelChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; - readonly aggregated?: boolean; readonly type = "funnel"; - readonly dataSetsHaveTitle: boolean; - readonly dataSetDesign?: DatasetDesign[]; - readonly axesDesign?: AxesDesign; - readonly horizontal = true; - readonly showValues?: boolean; - readonly funnelColors?: FunnelChartColors; - readonly cumulative?: boolean; - - constructor(definition: FunnelChartDefinition, sheetId: UID, getters: CoreGetters) { + + static allowedDefinitionKeys: readonly (keyof FunnelChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "axesDesign", + "legendPosition", + "horizontal", + "aggregated", + "showValues", + "funnelColors", + "cumulative", + ] as const; + + constructor(private definition: FunnelChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -70,16 +59,6 @@ export class FunnelChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.dataSetDesign = definition.dataSets; - this.axesDesign = definition.axesDesign; - this.showValues = definition.showValues; - this.horizontal = true; - this.funnelColors = definition.funnelColors; - this.cumulative = definition.cumulative; } static transformDefinition( @@ -120,12 +99,12 @@ export class FunnelChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -165,27 +144,17 @@ export class FunnelChart extends AbstractChart { const ranges: CustomizedDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), }); } return { - type: "funnel", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - aggregated: this.aggregated, - horizontal: this.horizontal, - axesDesign: this.axesDesign, - showValues: this.showValues, - funnelColors: this.funnelColors, - cumulative: this.cumulative, - humanize: this.humanize, }; } @@ -232,5 +201,5 @@ export function createFunnelChartRuntime(chart: FunnelChart, getters: Getters): }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/gauge_chart.ts b/src/helpers/figures/charts/gauge_chart.ts index f109172144..fd4b99c696 100644 --- a/src/helpers/figures/charts/gauge_chart.ts +++ b/src/helpers/figures/charts/gauge_chart.ts @@ -143,6 +143,12 @@ export class GaugeChart extends AbstractChart { readonly background?: Color; readonly type = "gauge"; + static allowedDefinitionKeys: readonly (keyof GaugeChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "dataRange", + "sectionRule", + ] as const; + constructor(definition: GaugeChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataRange = createValidRange(this.getters, this.sheetId, definition.dataRange); @@ -267,7 +273,7 @@ export class GaugeChart extends AbstractChart { getContextCreation(): ChartCreationContext { return { - ...this, + ...this.getDefinition(), range: this.dataRange ? [{ dataRange: this.getters.getRangeString(this.dataRange, this.sheetId) }] : undefined, diff --git a/src/helpers/figures/charts/geo_chart.ts b/src/helpers/figures/charts/geo_chart.ts index a91462f770..0d8e8a897e 100644 --- a/src/helpers/figures/charts/geo_chart.ts +++ b/src/helpers/figures/charts/geo_chart.ts @@ -12,13 +12,10 @@ import { } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; -import { LegendPosition } from "@odoo/o-spreadsheet-engine/types/chart"; import { - ChartColorScale, ChartCreationContext, CustomizedDataSet, DataSet, - DatasetDesign, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; import { @@ -26,15 +23,7 @@ import { GeoChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/geo_chart"; import { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getChartTitle, getGeoChartData, @@ -47,16 +36,20 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class GeoChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; readonly type = "geo"; - readonly dataSetsHaveTitle: boolean; - readonly dataSetDesign?: DatasetDesign[]; - readonly colorScale?: ChartColorScale; - readonly missingValueColor?: Color; - readonly region?: string; - constructor(definition: GeoChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof GeoChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "colorScale", + "missingValueColor", + "region", + ] as const; + + constructor(private definition: GeoChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -65,13 +58,6 @@ export class GeoChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.dataSetDesign = definition.dataSets; - this.colorScale = definition.colorScale; - this.missingValueColor = definition.missingValueColor; - this.region = definition.region; } static transformDefinition( @@ -106,12 +92,12 @@ export class GeoChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -151,24 +137,17 @@ export class GeoChart extends AbstractChart { const ranges: CustomizedDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), }); } return { - type: "geo", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - colorScale: this.colorScale, - missingValueColor: this.missingValueColor, - region: this.region, - humanize: this.humanize, }; } @@ -212,5 +191,5 @@ export function createGeoChartRuntime(chart: GeoChart, getters: Getters): GeoCha }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/line_chart.ts b/src/helpers/figures/charts/line_chart.ts index 47111ac58d..ab4725e4f0 100644 --- a/src/helpers/figures/charts/line_chart.ts +++ b/src/helpers/figures/charts/line_chart.ts @@ -16,27 +16,16 @@ import { import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { - AxesDesign, ChartCreationContext, ChartJSRuntime, CustomizedDataSet, DataSet, - DatasetDesign, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; -import { LegendPosition } from "@odoo/o-spreadsheet-engine/types/chart/common_chart"; import { LineChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart/line_chart"; import { toXlsxHexColor } from "@odoo/o-spreadsheet-engine/xlsx/helpers/colors"; import { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getChartShowValues, getChartTitle, @@ -51,22 +40,26 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class LineChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; - readonly labelsAsText: boolean; - readonly stacked: boolean; - readonly aggregated?: boolean; readonly type = "line"; - readonly dataSetsHaveTitle: boolean; - readonly cumulative: boolean; - readonly dataSetDesign?: DatasetDesign[]; - readonly axesDesign?: AxesDesign; - readonly fillArea?: boolean; - readonly showValues?: boolean; - readonly hideDataMarkers?: boolean; - readonly zoomable?: boolean; - constructor(definition: LineChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof LineChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "labelsAsText", + "stacked", + "aggregated", + "cumulative", + "axesDesign", + "fillArea", + "showValues", + "hideDataMarkers", + "zoomable", + ] as const; + + constructor(private definition: LineChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( this.getters, @@ -75,19 +68,6 @@ export class LineChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(this.getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.labelsAsText = definition.labelsAsText; - this.stacked = definition.stacked; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.cumulative = definition.cumulative; - this.dataSetDesign = definition.dataSets; - this.axesDesign = definition.axesDesign; - this.fillArea = definition.fillArea; - this.showValues = definition.showValues; - this.hideDataMarkers = definition.hideDataMarkers; - this.zoomable = definition.zoomable; } static validateChartDefinition( @@ -139,30 +119,17 @@ export class LineChart extends AbstractChart { const ranges: CustomizedDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), }); } return { - type: "line", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - labelsAsText: this.labelsAsText, - stacked: this.stacked, - aggregated: this.aggregated, - cumulative: this.cumulative, - axesDesign: this.axesDesign, - fillArea: this.fillArea, - showValues: this.showValues, - hideDataMarkers: this.hideDataMarkers, - zoomable: this.zoomable, - humanize: this.humanize, }; } @@ -170,12 +137,12 @@ export class LineChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -198,16 +165,16 @@ export class LineChart extends AbstractChart { } getDefinitionForExcel(): ExcelChartDefinition | undefined { + const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle) + shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) ); - const definition = this.getDefinition(); return { ...definition, - backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR), - fontColor: toXlsxHexColor(chartFontColor(this.background)), + backgroundColor: toXlsxHexColor(definition.background || BACKGROUND_CHART_COLOR), + fontColor: toXlsxHexColor(chartFontColor(definition.background)), dataSets, labelRange, verticalAxis: getDefinedAxis(definition), @@ -260,6 +227,6 @@ export function createLineChartRuntime(chart: LineChart, getters: Getters): Char return { chartJsConfig: config, - background: chart.background || BACKGROUND_CHART_COLOR, + background: definition.background || BACKGROUND_CHART_COLOR, }; } diff --git a/src/helpers/figures/charts/pie_chart.ts b/src/helpers/figures/charts/pie_chart.ts index 164ad4116c..8e01d37e9d 100644 --- a/src/helpers/figures/charts/pie_chart.ts +++ b/src/helpers/figures/charts/pie_chart.ts @@ -19,22 +19,13 @@ import { DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; -import { LegendPosition } from "@odoo/o-spreadsheet-engine/types/chart/common_chart"; import { PieChartDefinition, PieChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/pie_chart"; import { toXlsxHexColor } from "@odoo/o-spreadsheet-engine/xlsx/helpers/colors"; import type { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getChartShowValues, getChartTitle, @@ -48,16 +39,21 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class PieChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; readonly type = "pie"; - readonly aggregated?: boolean; - readonly dataSetsHaveTitle: boolean; - readonly isDoughnut?: boolean; - readonly showValues?: boolean; - readonly pieHolePercentage?: number; - constructor(definition: PieChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof PieChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "aggregated", + "isDoughnut", + "pieHolePercentage", + "showValues", + ] as const; + + constructor(private definition: PieChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -66,13 +62,6 @@ export class PieChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.isDoughnut = definition.isDoughnut; - this.showValues = definition.showValues; - this.pieHolePercentage = definition.pieHolePercentage; } static transformDefinition( @@ -113,7 +102,7 @@ export class PieChart extends AbstractChart { getContextCreation(): ChartCreationContext { return { - ...this, + ...this.getDefinition(), range: this.dataSets.map((ds: DataSet) => ({ dataRange: this.getters.getRangeString(ds.dataRange, this.sheetId), })), @@ -129,22 +118,14 @@ export class PieChart extends AbstractChart { targetSheetId?: UID ): PieChartDefinition { return { - type: "pie", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: dataSets.map((ds: DataSet) => ({ dataRange: this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId), })), - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - aggregated: this.aggregated, - isDoughnut: this.isDoughnut, - showValues: this.showValues, - pieHolePercentage: this.pieHolePercentage, - humanize: this.humanize, }; } @@ -169,15 +150,16 @@ export class PieChart extends AbstractChart { } getDefinitionForExcel(): ExcelChartDefinition | undefined { + const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle) + shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) ); return { - ...this.getDefinition(), - backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR), - fontColor: toXlsxHexColor(chartFontColor(this.background)), + ...definition, + backgroundColor: toXlsxHexColor(definition.background || BACKGROUND_CHART_COLOR), + fontColor: toXlsxHexColor(chartFontColor(definition.background)), dataSets, labelRange, }; @@ -203,7 +185,7 @@ export function createPieChartRuntime(chart: PieChart, getters: Getters): PieCha const chartData = getPieChartData(definition, chart.dataSets, chart.labelRange, getters); const config: ChartConfiguration<"doughnut" | "pie"> = { - type: chart.isDoughnut ? "doughnut" : "pie", + type: definition.isDoughnut ? "doughnut" : "pie", data: { labels: chartData.labels, datasets: getPieChartDatasets(definition, chartData), @@ -211,7 +193,7 @@ export function createPieChartRuntime(chart: PieChart, getters: Getters): PieCha options: { ...CHART_COMMON_OPTIONS, cutout: - chart.isDoughnut && definition.pieHolePercentage !== undefined + definition.isDoughnut && definition.pieHolePercentage !== undefined ? definition.pieHolePercentage + "%" : undefined, layout: getChartLayout(definition, chartData), @@ -224,5 +206,5 @@ export function createPieChartRuntime(chart: PieChart, getters: Getters): PieCha }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/pyramid_chart.ts b/src/helpers/figures/charts/pyramid_chart.ts index cc0d615f89..87a49eed1a 100644 --- a/src/helpers/figures/charts/pyramid_chart.ts +++ b/src/helpers/figures/charts/pyramid_chart.ts @@ -17,29 +17,18 @@ import { import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { - AxesDesign, ChartCreationContext, CustomizedDataSet, DataSet, - DatasetDesign, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; -import { LegendPosition } from "@odoo/o-spreadsheet-engine/types/chart/common_chart"; import { PyramidChartDefinition, PyramidChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/pyramid_chart"; import { toXlsxHexColor } from "@odoo/o-spreadsheet-engine/xlsx/helpers/colors"; import { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getBarChartDatasets, getBarChartLegend, @@ -54,18 +43,23 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class PyramidChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; - readonly aggregated?: boolean; readonly type = "pyramid"; - readonly dataSetsHaveTitle: boolean; - readonly dataSetDesign?: DatasetDesign[]; - readonly axesDesign?: AxesDesign; - readonly horizontal = true; - readonly stacked = true; - readonly showValues?: boolean; - constructor(definition: PyramidChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof PyramidChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "showValues", + "aggregated", + "axesDesign", + "stacked", + "horizontal", + "zoomable", + ] as const; + + constructor(private definition: PyramidChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -74,13 +68,6 @@ export class PyramidChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.dataSetDesign = definition.dataSets; - this.axesDesign = definition.axesDesign; - this.showValues = definition.showValues; } static transformDefinition( @@ -120,12 +107,12 @@ export class PyramidChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -165,36 +152,29 @@ export class PyramidChart extends AbstractChart { const ranges: CustomizedDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), }); } return { - type: "pyramid", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - aggregated: this.aggregated, - axesDesign: this.axesDesign, horizontal: true, stacked: true, - showValues: this.showValues, - humanize: this.humanize, }; } getDefinitionForExcel(getters: Getters): ExcelChartDefinition | undefined { + const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle) + shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) ); - const definition = this.getDefinition(); const chartData = getPyramidChartData(definition, this.dataSets, this.labelRange, getters); const { dataSetsValues } = chartData; const maxValue = Math.max( @@ -207,8 +187,8 @@ export class PyramidChart extends AbstractChart { return { ...definition, horizontal: true, - backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR), - fontColor: toXlsxHexColor(chartFontColor(this.background)), + backgroundColor: toXlsxHexColor(definition.background || BACKGROUND_CHART_COLOR), + fontColor: toXlsxHexColor(chartFontColor(definition.background)), dataSets, labelRange, verticalAxis: getDefinedAxis(definition), @@ -258,5 +238,5 @@ export function createPyramidChartRuntime( }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/radar_chart.ts b/src/helpers/figures/charts/radar_chart.ts index 0207878cda..59366950b4 100644 --- a/src/helpers/figures/charts/radar_chart.ts +++ b/src/helpers/figures/charts/radar_chart.ts @@ -19,7 +19,6 @@ import { CustomizedDataSet, DataSet, ExcelChartDefinition, - LegendPosition, } from "@odoo/o-spreadsheet-engine/types/chart"; import { RadarChartDefinition, @@ -27,16 +26,7 @@ import { } from "@odoo/o-spreadsheet-engine/types/chart/radar_chart"; import { toXlsxHexColor } from "@odoo/o-spreadsheet-engine/xlsx/helpers/colors"; import { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - DatasetDesign, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getChartShowValues, getChartTitle, @@ -51,18 +41,22 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class RadarChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; - readonly stacked: boolean; - readonly aggregated?: boolean; readonly type = "radar"; - readonly dataSetsHaveTitle: boolean; - readonly dataSetDesign?: DatasetDesign[]; - readonly fillArea?: boolean; - readonly showValues?: boolean; - readonly hideDataMarkers?: boolean; - constructor(definition: RadarChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof RadarChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "showValues", + "aggregated", + "stacked", + "fillArea", + "hideDataMarkers", + ] as const; + + constructor(private definition: RadarChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -71,15 +65,6 @@ export class RadarChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.stacked = definition.stacked; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.dataSetDesign = definition.dataSets; - this.fillArea = definition.fillArea; - this.showValues = definition.showValues; - this.hideDataMarkers = definition.hideDataMarkers; } static transformDefinition( @@ -119,12 +104,12 @@ export class RadarChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -164,40 +149,31 @@ export class RadarChart extends AbstractChart { const ranges: CustomizedDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), }); } return { - type: "radar", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - stacked: this.stacked, - aggregated: this.aggregated, - fillArea: this.fillArea, - showValues: this.showValues, - hideDataMarkers: this.hideDataMarkers, - humanize: this.humanize, }; } getDefinitionForExcel(): ExcelChartDefinition | undefined { + const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle) + shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) ); - const definition = this.getDefinition(); return { ...definition, - backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR), - fontColor: toXlsxHexColor(chartFontColor(this.background)), + backgroundColor: toXlsxHexColor(definition.background || BACKGROUND_CHART_COLOR), + fontColor: toXlsxHexColor(chartFontColor(definition.background)), dataSets, labelRange, }; @@ -241,5 +217,5 @@ export function createRadarChartRuntime(chart: RadarChart, getters: Getters): Ra }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/scatter_chart.ts b/src/helpers/figures/charts/scatter_chart.ts index 565f3490fd..6115101fb9 100644 --- a/src/helpers/figures/charts/scatter_chart.ts +++ b/src/helpers/figures/charts/scatter_chart.ts @@ -18,30 +18,19 @@ import { import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { - AxesDesign, ChartCreationContext, CustomizedDataSet, DataSet, - DatasetDesign, ExcelChartDataset, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; -import { LegendPosition } from "@odoo/o-spreadsheet-engine/types/chart/common_chart"; import { ScatterChartDefinition, ScatterChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/scatter_chart"; import { toXlsxHexColor } from "@odoo/o-spreadsheet-engine/xlsx/helpers/colors"; import { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getChartShowValues, getChartTitle, @@ -56,18 +45,22 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class ScatterChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; - readonly labelsAsText: boolean; - readonly aggregated?: boolean; readonly type = "scatter"; - readonly dataSetsHaveTitle: boolean; - readonly dataSetDesign?: DatasetDesign[]; - readonly axesDesign?: AxesDesign; - readonly showValues?: boolean; - readonly zoomable?: boolean; - constructor(definition: ScatterChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof ScatterChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "showValues", + "labelsAsText", + "aggregated", + "axesDesign", + "zoomable", + ] as const; + + constructor(private definition: ScatterChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( this.getters, @@ -76,15 +69,6 @@ export class ScatterChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(this.getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.labelsAsText = definition.labelsAsText; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.dataSetDesign = definition.dataSets; - this.axesDesign = definition.axesDesign; - this.showValues = definition.showValues; - this.zoomable = definition.zoomable; } static validateChartDefinition( @@ -132,26 +116,17 @@ export class ScatterChart extends AbstractChart { const ranges: CustomizedDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), }); } return { - type: "scatter", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - labelsAsText: this.labelsAsText, - aggregated: this.aggregated, - axesDesign: this.axesDesign, - showValues: this.showValues, - zoomable: this.zoomable, - humanize: this.humanize, }; } @@ -159,12 +134,12 @@ export class ScatterChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -187,19 +162,19 @@ export class ScatterChart extends AbstractChart { } getDefinitionForExcel(): ExcelChartDefinition | undefined { + const definition = this.getDefinition(); const dataSets: ExcelChartDataset[] = this.dataSets .map((ds: DataSet) => toExcelDataset(this.getters, ds)) .filter((ds) => ds.range !== ""); const labelRange = toExcelLabelRange( this.getters, this.labelRange, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle) + shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) ); - const definition = this.getDefinition(); return { ...definition, - backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR), - fontColor: toXlsxHexColor(chartFontColor(this.background)), + backgroundColor: toXlsxHexColor(definition.background || BACKGROUND_CHART_COLOR), + fontColor: toXlsxHexColor(chartFontColor(definition.background)), dataSets, labelRange, verticalAxis: getDefinedAxis(definition), @@ -257,6 +232,6 @@ export function createScatterChartRuntime( return { chartJsConfig: config, - background: chart.background || BACKGROUND_CHART_COLOR, + background: definition.background || BACKGROUND_CHART_COLOR, }; } diff --git a/src/helpers/figures/charts/sunburst_chart.ts b/src/helpers/figures/charts/sunburst_chart.ts index b1374d71f7..19c574408a 100644 --- a/src/helpers/figures/charts/sunburst_chart.ts +++ b/src/helpers/figures/charts/sunburst_chart.ts @@ -18,22 +18,12 @@ import { } from "@odoo/o-spreadsheet-engine/types/chart"; import { ChartCreationContext, - ChartStyle, CustomizedDataSet, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; -import { LegendPosition } from "@odoo/o-spreadsheet-engine/types/chart/common_chart"; import type { ChartConfiguration, ChartOptions } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getChartTitle, getHierarchalChartData, @@ -47,17 +37,22 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class SunburstChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; readonly type = "sunburst"; - readonly dataSetsHaveTitle: boolean; - readonly showValues?: boolean; - readonly showLabels?: boolean; - readonly valuesDesign?: ChartStyle; - readonly groupColors?: (Color | undefined | null)[]; - readonly pieHolePercentage?: number; - - constructor(definition: SunburstChartDefinition, sheetId: UID, getters: CoreGetters) { + + static allowedDefinitionKeys: readonly (keyof SunburstChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "showValues", + "showLabels", + "valuesDesign", + "groupColors", + "pieHolePercentage", + ] as const; + + constructor(private definition: SunburstChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -66,14 +61,6 @@ export class SunburstChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.showValues = definition.showValues; - this.showLabels = definition.showLabels; - this.valuesDesign = definition.valuesDesign; - this.groupColors = definition.groupColors; - this.pieHolePercentage = definition.pieHolePercentage; } static transformDefinition( @@ -111,6 +98,7 @@ export class SunburstChart extends AbstractChart { valuesDesign: context.valuesDesign, groupColors: context.groupColors, humanize: context.humanize, + pieHolePercentage: context.pieHolePercentage, }; } @@ -121,7 +109,7 @@ export class SunburstChart extends AbstractChart { getContextCreation(): ChartCreationContext { const leafRange = this.dataSets.at(-1)?.dataRange; return { - ...this, + ...this.getDefinition(), range: this.labelRange ? [{ dataRange: this.getters.getRangeString(this.labelRange, this.sheetId) }] : [], @@ -138,23 +126,14 @@ export class SunburstChart extends AbstractChart { targetSheetId?: UID ): SunburstChartDefinition { return { - type: "sunburst", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: dataSets.map((ds: DataSet) => ({ dataRange: this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId), })), - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - showValues: this.showValues, - showLabels: this.showLabels, - valuesDesign: this.valuesDesign, - groupColors: this.groupColors, - pieHolePercentage: this.pieHolePercentage, - humanize: this.humanize, }; } @@ -210,7 +189,8 @@ export function createSunburstChartRuntime( datasets: getSunburstChartDatasets(definition, chartData), }, options: { - cutout: chart.pieHolePercentage === undefined ? "25%" : `${chart.pieHolePercentage}%`, + cutout: + definition.pieHolePercentage === undefined ? "25%" : `${definition.pieHolePercentage}%`, ...(CHART_COMMON_OPTIONS as ChartOptions<"doughnut">), layout: getChartLayout(definition, chartData), plugins: { @@ -223,5 +203,5 @@ export function createSunburstChartRuntime( }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; } diff --git a/src/helpers/figures/charts/tree_map_chart.ts b/src/helpers/figures/charts/tree_map_chart.ts index 48e0763774..6d544fc96f 100644 --- a/src/helpers/figures/charts/tree_map_chart.ts +++ b/src/helpers/figures/charts/tree_map_chart.ts @@ -17,24 +17,13 @@ import { CustomizedDataSet, DataSet, ExcelChartDefinition, - TitleDesign, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; -import { LegendPosition } from "@odoo/o-spreadsheet-engine/types/chart/common_chart"; import { TreeMapChartDefinition, TreeMapChartRuntime, - TreeMapColoringOptions, } from "@odoo/o-spreadsheet-engine/types/chart/tree_map_chart"; import { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getChartTitle, getHierarchalChartData, @@ -53,18 +42,23 @@ export class TreeMapChart extends AbstractChart { }; readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly legendPosition: LegendPosition; readonly type = "treemap"; - readonly dataSetsHaveTitle: boolean; - readonly showHeaders?: boolean; - readonly headerDesign?: TitleDesign; - readonly showValues?: boolean; - readonly showLabels?: boolean; - readonly valuesDesign?: TitleDesign; - readonly coloringOptions?: TreeMapColoringOptions; - constructor(definition: TreeMapChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof TreeMapChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "showHeaders", + "headerDesign", + "showLabels", + "valuesDesign", + "coloringOptions", + "showValues", + ] as const; + + constructor(private definition: TreeMapChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -73,15 +67,6 @@ export class TreeMapChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.legendPosition = definition.legendPosition; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.showHeaders = definition.showHeaders; - this.headerDesign = definition.headerDesign; - this.showValues = definition.showValues; - this.showLabels = definition.showLabels; - this.valuesDesign = definition.valuesDesign; - this.coloringOptions = definition.coloringOptions; } static transformDefinition( @@ -127,7 +112,8 @@ export class TreeMapChart extends AbstractChart { getContextCreation(): ChartCreationContext { const leafRange = this.dataSets.at(-1)?.dataRange; return { - ...this, + ...this.getDefinition(), + treemapColoringOptions: this.definition.coloringOptions, range: this.labelRange ? [{ dataRange: this.getters.getRangeString(this.labelRange, this.sheetId) }] : [], @@ -170,22 +156,12 @@ export class TreeMapChart extends AbstractChart { dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), })); return { - type: "treemap", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - showValues: this.showValues, - showHeaders: this.showHeaders, - headerDesign: this.headerDesign, - showLabels: this.showLabels, - valuesDesign: this.valuesDesign, - coloringOptions: this.coloringOptions, - humanize: this.humanize, }; } @@ -232,5 +208,8 @@ export function createTreeMapChartRuntime( }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { + chartJsConfig: config, + background: definition.background || BACKGROUND_CHART_COLOR, + }; } diff --git a/src/helpers/figures/charts/waterfall_chart.ts b/src/helpers/figures/charts/waterfall_chart.ts index 204a81d914..e9ba0ecc18 100644 --- a/src/helpers/figures/charts/waterfall_chart.ts +++ b/src/helpers/figures/charts/waterfall_chart.ts @@ -13,30 +13,17 @@ import { import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { - AxesDesign, ChartCreationContext, CustomizedDataSet, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; -import { - LegendPosition, - VerticalAxisPosition, -} from "@odoo/o-spreadsheet-engine/types/chart/common_chart"; import { WaterfallChartDefinition, WaterfallChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/waterfall_chart"; import type { ChartConfiguration } from "chart.js"; -import { - ApplyRangeChange, - Color, - CommandResult, - Getters, - Range, - RangeAdapter, - UID, -} from "../../../types"; +import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } from "../../../types"; import { getBarChartData, getChartTitle, @@ -51,24 +38,28 @@ import { getChartLayout } from "./runtime/chartjs_layout"; export class WaterfallChart extends AbstractChart { readonly dataSets: DataSet[]; readonly labelRange?: Range | undefined; - readonly background?: Color; - readonly verticalAxisPosition: VerticalAxisPosition; - readonly legendPosition: LegendPosition; - readonly aggregated?: boolean; readonly type = "waterfall"; - readonly dataSetsHaveTitle: boolean; - readonly showSubTotals: boolean; - readonly firstValueAsSubtotal?: boolean; - readonly showConnectorLines: boolean; - readonly positiveValuesColor?: Color; - readonly negativeValuesColor?: Color; - readonly subTotalValuesColor?: Color; - readonly dataSetDesign: CustomizedDataSet[]; - readonly axesDesign?: AxesDesign; - readonly showValues?: boolean; - readonly zoomable?: boolean; - constructor(definition: WaterfallChartDefinition, sheetId: UID, getters: CoreGetters) { + static allowedDefinitionKeys: readonly (keyof WaterfallChartDefinition)[] = [ + ...AbstractChart.commonKeys, + "legendPosition", + "dataSets", + "dataSetsHaveTitle", + "labelRange", + "verticalAxisPosition", + "aggregated", + "showSubTotals", + "showConnectorLines", + "firstValueAsSubtotal", + "positiveValuesColor", + "negativeValuesColor", + "subTotalValuesColor", + "zoomable", + "axesDesign", + "showValues", + ] as const; + + constructor(private definition: WaterfallChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); this.dataSets = createDataSets( getters, @@ -77,21 +68,6 @@ export class WaterfallChart extends AbstractChart { definition.dataSetsHaveTitle ); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); - this.background = definition.background; - this.verticalAxisPosition = definition.verticalAxisPosition; - this.legendPosition = definition.legendPosition; - this.aggregated = definition.aggregated; - this.dataSetsHaveTitle = definition.dataSetsHaveTitle; - this.showSubTotals = definition.showSubTotals; - this.showConnectorLines = definition.showConnectorLines; - this.positiveValuesColor = definition.positiveValuesColor; - this.negativeValuesColor = definition.negativeValuesColor; - this.subTotalValuesColor = definition.subTotalValuesColor; - this.firstValueAsSubtotal = definition.firstValueAsSubtotal; - this.dataSetDesign = definition.dataSets; - this.axesDesign = definition.axesDesign; - this.showValues = definition.showValues; - this.zoomable = definition.zoomable; } static transformDefinition( @@ -134,12 +110,12 @@ export class WaterfallChart extends AbstractChart { const range: CustomizedDataSet[] = []; for (const [i, dataSet] of this.dataSets.entries()) { range.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), }); } return { - ...this, + ...this.getDefinition(), range, auxiliaryRange: this.labelRange ? this.getters.getRangeString(this.labelRange, this.sheetId) @@ -179,32 +155,17 @@ export class WaterfallChart extends AbstractChart { const ranges: CustomizedDataSet[] = []; for (const [i, dataSet] of dataSets.entries()) { ranges.push({ - ...this.dataSetDesign?.[i], + ...this.definition.dataSets?.[i], dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), }); } return { - type: "waterfall", + ...this.definition, dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - background: this.background, dataSets: ranges, - legendPosition: this.legendPosition, - verticalAxisPosition: this.verticalAxisPosition, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, - title: this.title, - aggregated: this.aggregated, - showSubTotals: this.showSubTotals, - showConnectorLines: this.showConnectorLines, - positiveValuesColor: this.positiveValuesColor, - negativeValuesColor: this.negativeValuesColor, - subTotalValuesColor: this.subTotalValuesColor, - firstValueAsSubtotal: this.firstValueAsSubtotal, - axesDesign: this.axesDesign, - showValues: this.showValues, - zoomable: this.zoomable, - humanize: this.humanize, }; } @@ -256,5 +217,8 @@ export function createWaterfallChartRuntime( }, }; - return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR }; + return { + chartJsConfig: config, + background: definition.background || BACKGROUND_CHART_COLOR, + }; } diff --git a/src/registries/chart_types.ts b/src/registries/chart_types.ts index da73140f55..ddc70ee442 100644 --- a/src/registries/chart_types.ts +++ b/src/registries/chart_types.ts @@ -60,6 +60,7 @@ chartRegistry.add("bar", { validateChartDefinition: BarChart.validateChartDefinition, transformDefinition: BarChart.transformDefinition, getChartDefinitionFromContextCreation: BarChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: BarChart.allowedDefinitionKeys, sequence: 10, }); chartRegistry.add("combo", { @@ -70,6 +71,7 @@ chartRegistry.add("combo", { validateChartDefinition: ComboChart.validateChartDefinition, transformDefinition: ComboChart.transformDefinition, getChartDefinitionFromContextCreation: ComboChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: ComboChart.allowedDefinitionKeys, sequence: 15, }); chartRegistry.add("line", { @@ -80,6 +82,7 @@ chartRegistry.add("line", { validateChartDefinition: LineChart.validateChartDefinition, transformDefinition: LineChart.transformDefinition, getChartDefinitionFromContextCreation: LineChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: LineChart.allowedDefinitionKeys, sequence: 20, }); chartRegistry.add("pie", { @@ -90,6 +93,7 @@ chartRegistry.add("pie", { validateChartDefinition: PieChart.validateChartDefinition, transformDefinition: PieChart.transformDefinition, getChartDefinitionFromContextCreation: PieChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: PieChart.allowedDefinitionKeys, sequence: 30, }); chartRegistry.add("scorecard", { @@ -100,6 +104,7 @@ chartRegistry.add("scorecard", { validateChartDefinition: ScorecardChart.validateChartDefinition, transformDefinition: ScorecardChart.transformDefinition, getChartDefinitionFromContextCreation: ScorecardChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: ScorecardChart.allowedDefinitionKeys, sequence: 40, }); chartRegistry.add("gauge", { @@ -110,6 +115,7 @@ chartRegistry.add("gauge", { validateChartDefinition: GaugeChart.validateChartDefinition, transformDefinition: GaugeChart.transformDefinition, getChartDefinitionFromContextCreation: GaugeChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: GaugeChart.allowedDefinitionKeys, sequence: 50, }); chartRegistry.add("scatter", { @@ -120,6 +126,7 @@ chartRegistry.add("scatter", { validateChartDefinition: ScatterChart.validateChartDefinition, transformDefinition: ScatterChart.transformDefinition, getChartDefinitionFromContextCreation: ScatterChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: ScatterChart.allowedDefinitionKeys, sequence: 60, }); chartRegistry.add("waterfall", { @@ -130,6 +137,7 @@ chartRegistry.add("waterfall", { validateChartDefinition: WaterfallChart.validateChartDefinition, transformDefinition: WaterfallChart.transformDefinition, getChartDefinitionFromContextCreation: WaterfallChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: WaterfallChart.allowedDefinitionKeys, sequence: 70, }); chartRegistry.add("pyramid", { @@ -140,6 +148,7 @@ chartRegistry.add("pyramid", { validateChartDefinition: PyramidChart.validateChartDefinition, transformDefinition: PyramidChart.transformDefinition, getChartDefinitionFromContextCreation: PyramidChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: PyramidChart.allowedDefinitionKeys, sequence: 80, dataSeriesLimit: 2, }); @@ -151,6 +160,7 @@ chartRegistry.add("radar", { validateChartDefinition: RadarChart.validateChartDefinition, transformDefinition: RadarChart.transformDefinition, getChartDefinitionFromContextCreation: RadarChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: RadarChart.allowedDefinitionKeys, sequence: 80, }); chartRegistry.add("geo", { @@ -161,6 +171,7 @@ chartRegistry.add("geo", { validateChartDefinition: GeoChart.validateChartDefinition, transformDefinition: GeoChart.transformDefinition, getChartDefinitionFromContextCreation: GeoChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: GeoChart.allowedDefinitionKeys, sequence: 90, dataSeriesLimit: 1, }); @@ -172,6 +183,7 @@ chartRegistry.add("funnel", { validateChartDefinition: FunnelChart.validateChartDefinition, transformDefinition: FunnelChart.transformDefinition, getChartDefinitionFromContextCreation: FunnelChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: FunnelChart.allowedDefinitionKeys, sequence: 100, dataSeriesLimit: 1, }); @@ -183,6 +195,7 @@ chartRegistry.add("sunburst", { validateChartDefinition: SunburstChart.validateChartDefinition, transformDefinition: SunburstChart.transformDefinition, getChartDefinitionFromContextCreation: SunburstChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: SunburstChart.allowedDefinitionKeys, sequence: 30, }); chartRegistry.add("treemap", { @@ -193,6 +206,7 @@ chartRegistry.add("treemap", { validateChartDefinition: TreeMapChart.validateChartDefinition, transformDefinition: TreeMapChart.transformDefinition, getChartDefinitionFromContextCreation: TreeMapChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: TreeMapChart.allowedDefinitionKeys, sequence: 100, }); chartRegistry.add("calendar", { @@ -205,6 +219,7 @@ chartRegistry.add("calendar", { validateChartDefinition: CalendarChart.validateChartDefinition, transformDefinition: CalendarChart.transformDefinition, getChartDefinitionFromContextCreation: CalendarChart.getDefinitionFromContextCreation, + allowedDefinitionKeys: CalendarChart.allowedDefinitionKeys, sequence: 110, dataSeriesLimit: 1, }); diff --git a/tests/figures/chart/__snapshots__/chart_plugin.test.ts.snap b/tests/figures/chart/__snapshots__/chart_plugin.test.ts.snap index 0f9e5a8f5a..42a1755a76 100644 --- a/tests/figures/chart/__snapshots__/chart_plugin.test.ts.snap +++ b/tests/figures/chart/__snapshots__/chart_plugin.test.ts.snap @@ -58,7 +58,7 @@ exports[`datasource tests create a chart with stacked bar 1`] = ` "chartShowValuesPlugin": { "background": [Function], "callback": [Function], - "horizontal": undefined, + "horizontal": false, "showValues": false, "type": "bar", }, diff --git a/tests/figures/chart/sunburst/sunburst_chart_plugin.test.ts b/tests/figures/chart/sunburst/sunburst_chart_plugin.test.ts index 1c1e5cf58e..f2fbd76d05 100644 --- a/tests/figures/chart/sunburst/sunburst_chart_plugin.test.ts +++ b/tests/figures/chart/sunburst/sunburst_chart_plugin.test.ts @@ -67,6 +67,7 @@ describe("Sunburst chart chart", () => { valuesDesign: { italic: true }, groupColors: ["#123456", "#654321"], humanize: false, + pieHolePercentage: 0, }); }); diff --git a/tests/model/model_import_export.test.ts b/tests/model/model_import_export.test.ts index 2bc2b414b9..879589f32f 100644 --- a/tests/model/model_import_export.test.ts +++ b/tests/model/model_import_export.test.ts @@ -801,6 +801,39 @@ test("migrate version 19.1.0: colorScale is changed to a trio of color", () => { expect(model.exportData().sheets[0].figures[0].data.colorScale).toEqual(COLORSCHEMES.reds); }); +test("migrate version 19.1.1: remove extra keys from chart definition", () => { + const definition = { + type: "line", + title: "demo chart", + labelRange: "A1:A4", + humanize: true, + dataSets: [], + dataSetsHaveTitle: false, + }; + const data = { + version: "18.5.1", + sheets: [ + { + id: "sh1", + figures: [ + { + id: "someuuid", + tag: "chart", + data: { + ...definition, + chartId: "someuuid", + extraKey1: "extraValue1", + extraKey2: "extraValue2", + }, + }, + ], + }, + ], + }; + const model = new Model(data); + expect(model.getters.getChartDefinition("someuuid")).toEqual(definition); +}); + describe("Import", () => { test("Import sheet with rows/cols size defined.", () => { const model = new Model({ diff --git a/tests/test_helpers/commands_helpers.ts b/tests/test_helpers/commands_helpers.ts index ca94afe200..20d007ac24 100644 --- a/tests/test_helpers/commands_helpers.ts +++ b/tests/test_helpers/commands_helpers.ts @@ -44,6 +44,9 @@ import { import { createEqualCF, target, toRangeData, toRangesData } from "./helpers"; import { ICON_SETS } from "@odoo/o-spreadsheet-engine/components/icons/icons"; +// import { chartFactory } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_factory"; +// import { chartRegistry } from "@odoo/o-spreadsheet-engine/registries/chart_registry"; +import { chartRegistry } from "@odoo/o-spreadsheet-engine/registries/chart_registry"; import { SunburstChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart"; import { CalendarChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart/calendar_chart"; import { ComboChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart/combo_chart"; @@ -223,6 +226,8 @@ export function createChart( ) { const id = chartId || model.uuidGenerator.uuidv4(); sheetId = sheetId || model.getters.getActiveSheetId(); + + // definition with all possible fields filled const definition = { ...data, title: data.title || { text: "test" }, @@ -244,6 +249,13 @@ export function createChart( horizontalGroupBy: ("horizontalGroupBy" in data && data.horizontalGroupBy) || "day_of_week", verticalGroupBy: ("verticalGroupBy" in data && data.verticalGroupBy) || "month_number", }; + + const keys = new Set(chartRegistry.get(data.type).allowedDefinitionKeys); + for (const key of Object.keys(definition)) { + if (!keys.has(key)) { + delete definition[key]; + } + } return model.dispatch("CREATE_CHART", { figureId: figureData.figureId || model.uuidGenerator.smallUuid(), chartId: id, @@ -508,15 +520,25 @@ export function updateChart( definition: Partial, sheetId: UID = model.getters.getActiveSheetId() ): DispatchResult { - const def: ChartDefinition = { - ...model.getters.getChartDefinition(chartId), - ...definition, - } as ChartDefinition; + const currentDefinition = model.getters.getChartDefinition(chartId); + let updatedDef: ChartDefinition; + if (definition.type && definition.type !== currentDefinition.type) { + const context = model.getters.getContextCreationChart(chartId); + const converted = chartRegistry + .get(definition.type!) + .getChartDefinitionFromContextCreation(context ?? {}); + updatedDef = { ...converted, ...definition } as ChartDefinition; + } else { + updatedDef = { + ...currentDefinition, + ...definition, + } as ChartDefinition; + } return model.dispatch("UPDATE_CHART", { figureId: model.getters.getFigureIdFromChartId(chartId), chartId, sheetId, - definition: def, + definition: updatedDef, }); } From d4dc479fa1879c8c17531ce93967218c5f2daf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Tue, 25 Nov 2025 14:11:30 +0100 Subject: [PATCH 6/9] [REF] chart: labels are { value, format } The goal is ultimately to get the format from there instead of using the label range and reading again the cells (see next commit) --- .../src/types/chart/chart.ts | 5 +- .../charts/runtime/chart_data_extractor.ts | 101 ++++++++++-------- 2 files changed, 55 insertions(+), 51 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/types/chart/chart.ts b/packages/o-spreadsheet-engine/src/types/chart/chart.ts index 226756d9d0..e5ad50c463 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/chart.ts @@ -97,10 +97,7 @@ export type ChartJSRuntime = export type ChartRuntime = ChartJSRuntime | ScorecardChartRuntime | GaugeChartRuntime; -export interface LabelValues { - readonly values: string[]; - readonly formattedValues: string[]; -} +export type LabelValues = FunctionResultObject[]; export interface DatasetValues { readonly label?: string; diff --git a/src/helpers/figures/charts/runtime/chart_data_extractor.ts b/src/helpers/figures/charts/runtime/chart_data_extractor.ts index c0c1ce8d40..1bf3444a4c 100644 --- a/src/helpers/figures/charts/runtime/chart_data_extractor.ts +++ b/src/helpers/figures/charts/runtime/chart_data_extractor.ts @@ -3,7 +3,6 @@ import { deepCopy, DEFAULT_LOCALE, findNextDefinedValue, - isNumber, range, } from "@odoo/o-spreadsheet-engine"; import { ChartTerms } from "@odoo/o-spreadsheet-engine/components/translations_terms"; @@ -22,7 +21,12 @@ import { isTextCell, } from "@odoo/o-spreadsheet-engine/helpers/cells/cell_evaluation"; import { shouldRemoveFirstLabel } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; -import { DAYS, isDateTimeFormat, MONTHS } from "@odoo/o-spreadsheet-engine/helpers/format/format"; +import { + DAYS, + formatValue, + isDateTimeFormat, + MONTHS, +} from "@odoo/o-spreadsheet-engine/helpers/format/format"; import { createDate } from "@odoo/o-spreadsheet-engine/helpers/pivot/spreadsheet_pivot/date_spreadsheet_pivot"; import { recomputeZones } from "@odoo/o-spreadsheet-engine/helpers/recompute_zones"; import { positions } from "@odoo/o-spreadsheet-engine/helpers/zones"; @@ -74,8 +78,10 @@ export function getBarChartData( labelRange: Range | undefined, getters: Getters ): ChartRuntimeGenerationArgs { - const labelValues = getChartLabelValues(getters, dataSets, labelRange); - let labels = labelValues.formattedValues; + const locale = getters.getLocale(); + let labels = getChartLabelValues(getters, dataSets, labelRange).map(({ value, format }) => + formatValue(value, { format, locale }) + ); let dataSetsValues = getChartDatasetValues(getters, dataSets); if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { labels.shift(); @@ -142,7 +148,7 @@ function getDateTimeLabel(value: number, stamp: CalendarChartGranularity): strin } function computeValuesAndLabels( - timeValues: CellValue[], + timeValues: FunctionResultObject[], values: FunctionResultObject[], horizontalGroupBy: CalendarChartGranularity, verticalGroupBy: CalendarChartGranularity, @@ -156,8 +162,8 @@ function computeValuesAndLabels( const xValue = toNumber( createDate( { granularity: horizontalGroupBy, type: "date", displayName: "date" }, - timeValues[i], - DEFAULT_LOCALE + timeValues[i].value, + locale ), locale ); @@ -168,8 +174,8 @@ function computeValuesAndLabels( const yValue = toNumber( createDate( { granularity: verticalGroupBy, type: "date", displayName: "date" }, - timeValues[i], - DEFAULT_LOCALE + timeValues[i].value, + locale ), locale ); @@ -197,7 +203,7 @@ function computeValuesAndLabels( return { dataSetsValues, - labels: xValues.map((v) => getDateTimeLabel(v, horizontalGroupBy)), + labels: xValues.map((v) => ({ value: getDateTimeLabel(v, horizontalGroupBy) })), }; } @@ -208,7 +214,7 @@ export function getCalendarChartData( getters: Getters ): ChartRuntimeGenerationArgs { const labelValues = getChartLabelValues(getters, dataSets, labelRange); - let labels = labelValues.values; + let labels = labelValues; let dataSetsValues = getChartDatasetValues(getters, dataSets); if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { labels.shift(); @@ -216,7 +222,7 @@ export function getCalendarChartData( const locale = getters.getLocale() || DEFAULT_LOCALE; - ({ labels, dataSetsValues } = filterInvalidCalendarDataPoints(labels, dataSetsValues, locale)); + ({ labels, dataSetsValues } = filterInvalidCalendarDataPoints(labels, dataSetsValues)); const axisFormats = { y: getChartDatasetFormat(definition.dataSets, dataSetsValues, "left") }; ({ labels, dataSetsValues } = computeValuesAndLabels( @@ -230,7 +236,7 @@ export function getCalendarChartData( return { dataSetsValues, axisFormats, - labels, + labels: labels.map(({ value }) => String(value ?? "")), locale: getters.getLocale(), topPadding: getTopPaddingForDashboard(definition, getters), }; @@ -273,7 +279,12 @@ export function getLineChartData( ): ChartRuntimeGenerationArgs { const axisType = getChartAxisType(definition, dataSets, labelRange, getters); const labelValues = getChartLabelValues(getters, dataSets, labelRange); - let labels = axisType === "linear" ? labelValues.values : labelValues.formattedValues; + let labels = + axisType === "linear" + ? labelValues.map(({ value }) => String(value ?? "")) + : labelValues.map(({ value, format }) => + formatValue(value, { format, locale: getters.getLocale() }) + ); let dataSetsValues = getChartDatasetValues(getters, dataSets); const removeFirstLabel = shouldRemoveFirstLabel( labelRange, @@ -332,7 +343,9 @@ export function getPieChartData( getters: Getters ): ChartRuntimeGenerationArgs { const labelValues = getChartLabelValues(getters, dataSets, labelRange); - let labels = labelValues.formattedValues; + let labels = labelValues.map(({ value, format }) => + formatValue(value, { format, locale: getters.getLocale() }) + ); let dataSetsValues = getChartDatasetValues(getters, dataSets); if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { labels.shift(); @@ -364,7 +377,9 @@ export function getRadarChartData( getters: Getters ): ChartRuntimeGenerationArgs { const labelValues = getChartLabelValues(getters, dataSets, labelRange); - let labels = labelValues.formattedValues; + let labels = labelValues.map(({ value, format }) => + formatValue(value, { format, locale: getters.getLocale() }) + ); let dataSetsValues = getChartDatasetValues(getters, dataSets); if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { labels.shift(); @@ -396,7 +411,9 @@ export function getGeoChartData( ): GeoChartRuntimeGenerationArgs { const dataSets = fullDataSets.slice(0, 1); const labelValues = getChartLabelValues(getters, dataSets, labelRange); - let labels = labelValues.formattedValues; + let labels = labelValues.map(({ value, format }) => + formatValue(value, { format, locale: getters.getLocale() }) + ); if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { labels.shift(); } @@ -425,7 +442,9 @@ export function getFunnelChartData( getters: Getters ): ChartRuntimeGenerationArgs { const labelValues = getChartLabelValues(getters, dataSets, labelRange); - let labels = labelValues.formattedValues; + let labels = labelValues.map(({ value, format }) => + formatValue(value, { format, locale: getters.getLocale() }) + ); let dataSetsValues = getChartDatasetValues(getters, dataSets); if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { labels.shift(); @@ -458,7 +477,7 @@ export function getHierarchalChartData( getters: Getters ): ChartRuntimeGenerationArgs { // In hierarchical charts, labels are the leaf values (numbers), and the hierarchy is defined in the dataSets (strings) - let labels = getChartLabelValues(getters, dataSets, labelRange).values; + let labels = getChartLabelValues(getters, dataSets, labelRange); let dataSetsValues = getHierarchicalDatasetValues(getters, dataSets); const removeFirstLabel = shouldRemoveFirstLabel( labelRange, @@ -474,7 +493,7 @@ export function getHierarchalChartData( return { dataSetsValues, axisFormats: { y: getChartLabelFormat(getters, labelRange, removeFirstLabel) }, - labels, + labels: labels.map(({ value }) => String(value ?? "")), locale: getters.getLocale(), }; } @@ -860,10 +879,9 @@ function filterInvalidDataPoints( * - have no label and a non-numeric value */ function filterInvalidCalendarDataPoints( - labels: string[], - datasets: DatasetValues[], - locale: Locale -): { labels: string[]; dataSetsValues: DatasetValues[] } { + labels: LabelValues, + datasets: DatasetValues[] +): { labels: LabelValues; dataSetsValues: DatasetValues[] } { const numberOfDataPoints = Math.max( labels.length, ...datasets.map((dataset) => dataset.data?.length || 0) @@ -871,7 +889,7 @@ function filterInvalidCalendarDataPoints( const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => { const label = labels[dataPointIndex]; const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]); - return label && isNumber(label, DEFAULT_LOCALE) && isNumberCell(values[0]); + return isNumberCell(label) && isNumberCell(values[0]); }); return { labels: dataPointsIndexes.map((i) => labels[i] || ""), @@ -886,9 +904,9 @@ function filterInvalidCalendarDataPoints( * Filter the data points that have either no value, a negative value, no root group or null group values in the middle */ function filterInvalidHierarchicalPoints( - values: string[], + values: LabelValues, hierarchy: DatasetValues[] -): { labels: string[]; dataSetsValues: DatasetValues[] } { +): { labels: LabelValues; dataSetsValues: DatasetValues[] } { const numberOfDataPoints = Math.max( values.length, ...hierarchy.map((dataset) => dataset.data?.length || 0) @@ -907,7 +925,7 @@ function filterInvalidHierarchicalPoints( return false; } } - return values[dataPointIndex] && !isNaN(Number(values[dataPointIndex])); + return values[dataPointIndex] && isNumberCell(values[dataPointIndex]); }); return { labels: dataPointsIndexes.map((i) => values[i]), @@ -921,14 +939,14 @@ function filterInvalidHierarchicalPoints( /** * If the values are a mix of positive and negative values, keep only the positive ones */ -function filterValuesWithDifferentSigns(values: string[], hierarchy: DatasetValues[]) { +function filterValuesWithDifferentSigns(values: LabelValues, hierarchy: DatasetValues[]) { const positivePointsIndexes: number[] = []; const negativePointsIndexes: number[] = []; for (let i = 0; i < values.length; i++) { - if (Number(values[i]) <= 0) { + if (Number(values[i].value) <= 0) { negativePointsIndexes.push(i); - } else if (Number(values[i]) > 0) { + } else if (Number(values[i].value) > 0) { positivePointsIndexes.push(i); } } @@ -1004,7 +1022,7 @@ function getChartLabelValues( dataSets: DataSet[], labelRange?: Range ): LabelValues { - let labels: LabelValues = { values: [], formattedValues: [] }; + let labels: LabelValues = []; if (labelRange) { const { left } = labelRange.zone; if ( @@ -1012,31 +1030,20 @@ function getChartLabelValues( !labelRange.invalidSheetName && !getters.isColHidden(labelRange.sheetId, left) ) { - const cells = getters.getRangeValues(labelRange); - labels = { - formattedValues: cells.map(({ formattedValue }) => formattedValue), - values: cells.map(({ value }) => String(value ?? "")), - }; + labels = getters.getRangeValues(labelRange); } else if (dataSets[0]) { const ranges = getData(getters, dataSets[0]); - labels = { - formattedValues: range(0, ranges.length).map((r) => r.toString()), - values: labels.formattedValues, - }; + labels = range(0, ranges.length).map((r) => ({ value: r.toString() })); } } else if (dataSets.length === 1) { const dataLength = getData(getters, dataSets[0]).length; for (let i = 0; i < dataLength; i++) { - labels.formattedValues.push(""); - labels.values.push(""); + labels.push({ value: "" }); } } else { if (dataSets[0]) { const ranges = getData(getters, dataSets[0]); - labels = { - formattedValues: range(0, ranges.length).map((r) => r.toString()), - values: labels.formattedValues, - }; + labels = range(0, ranges.length).map((r) => ({ value: r.toString() })); } } return labels; From d3be80b1d31cd7f9f0a3379647f650f8f2cb97ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Tue, 25 Nov 2025 11:37:13 +0100 Subject: [PATCH 7/9] [REF] chart: decouple extracting the data Extract chart data from ranges is now decoupled from everything else --- .../helpers/figures/charts/abstract_chart.ts | 19 +- .../helpers/figures/charts/chart_common.ts | 16 +- .../helpers/figures/charts/chart_factory.ts | 3 +- .../helpers/figures/charts/scorecard_chart.ts | 4 +- .../src/registries/chart_registry.ts | 35 ++- .../src/types/chart/chart.ts | 5 + .../calendar_chart_config_panel.ts | 22 +- .../line_chart/line_chart_config_panel.ts | 7 +- .../scatter_chart_config_panel.ts | 7 +- src/helpers/figures/charts/bar_chart.ts | 15 +- src/helpers/figures/charts/calendar_chart.ts | 6 +- src/helpers/figures/charts/combo_chart.ts | 15 +- src/helpers/figures/charts/funnel_chart.ts | 9 +- src/helpers/figures/charts/gauge_chart.ts | 2 +- src/helpers/figures/charts/geo_chart.ts | 9 +- src/helpers/figures/charts/line_chart.ts | 15 +- src/helpers/figures/charts/pie_chart.ts | 15 +- src/helpers/figures/charts/pyramid_chart.ts | 14 +- src/helpers/figures/charts/radar_chart.ts | 15 +- .../charts/runtime/chart_data_extractor.ts | 250 +++++++----------- src/helpers/figures/charts/scatter_chart.ts | 14 +- src/helpers/figures/charts/sunburst_chart.ts | 6 +- src/helpers/figures/charts/tree_map_chart.ts | 6 +- src/helpers/figures/charts/waterfall_chart.ts | 6 +- src/registries/chart_types.ts | 20 +- 25 files changed, 257 insertions(+), 278 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts index 832564f12c..b51119d8b4 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/abstract_chart.ts @@ -14,7 +14,8 @@ import { CellErrorType } from "../../../types/errors"; import { AdaptSheetName, ApplyRangeChange, RangeAdapter, UID } from "../../../types/misc"; import { Range } from "../../../types/range"; import { Validator } from "../../../types/validator"; -import { toExcelDataset, toExcelLabelRange } from "./chart_common"; +import { getZoneArea } from "../../zones"; +import { shouldRemoveFirstLabel, toExcelDataset, toExcelLabelRange } from "./chart_common"; /** * AbstractChart is the class from which every Chart should inherit. @@ -114,15 +115,19 @@ export abstract class AbstractChart { */ abstract getContextCreation(): ChartCreationContext; - protected getCommonDataSetAttributesForExcel( - labelRange: Range | undefined, - dataSets: DataSet[], - shouldRemoveFirstLabel: boolean - ) { + protected getCommonDataSetAttributesForExcel(labelRange: Range | undefined, dataSets: DataSet[]) { const excelDataSets: ExcelChartDataset[] = dataSets .map((ds: DataSet) => toExcelDataset(this.getters, ds)) .filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference); - const excelLabelRange = toExcelLabelRange(this.getters, labelRange, shouldRemoveFirstLabel); + const definition = this.getDefinition(); + const datasetLength = dataSets[0] ? getZoneArea(dataSets[0].dataRange.zone) : undefined; + const labelLength = labelRange ? getZoneArea(labelRange.zone) : 0; + const _shouldRemoveFirstLabel = shouldRemoveFirstLabel( + labelLength, + datasetLength, + "dataSetsHaveTitle" in definition ? definition.dataSetsHaveTitle : false + ); + const excelLabelRange = toExcelLabelRange(this.getters, labelRange, _shouldRemoveFirstLabel); return { dataSets: excelDataSets, labelRange: excelLabelRange, diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_common.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_common.ts index e1a43c2aa4..7fdf905cf1 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_common.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_common.ts @@ -33,7 +33,7 @@ import { adaptStringRange } from "../../formulas"; import { isDefined, largeMax } from "../../misc"; import { createRange, duplicateRangeInDuplicatedSheet } from "../../range"; import { rangeReference } from "../../references"; -import { getZoneArea, isFullRow, toUnboundedZone, zoneToDimension, zoneToXc } from "../../zones"; +import { isFullRow, toUnboundedZone, zoneToDimension, zoneToXc } from "../../zones"; export const TREND_LINE_XAXIS_ID = "x1"; export const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage"; @@ -398,19 +398,11 @@ export function checkLabelRange(definition: ChartWithDataSetDefinition): Command } export function shouldRemoveFirstLabel( - labelRange: Range | undefined, - dataset: DataSet | undefined, + numberOfLabels: number, + numberOfDataPoints: number | undefined, dataSetsHaveTitle: boolean ) { - if (!dataSetsHaveTitle) return false; - if (!labelRange) return false; - if (!dataset) return true; - const datasetLength = getZoneArea(dataset.dataRange.zone); - const labelLength = getZoneArea(labelRange.zone); - if (labelLength < datasetLength) { - return false; - } - return true; + return dataSetsHaveTitle && (!numberOfDataPoints || numberOfLabels >= numberOfDataPoints); } export function getChartPositionAtCenterOfViewport( diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_factory.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_factory.ts index 77388420e6..5824ca944d 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_factory.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_factory.ts @@ -36,8 +36,9 @@ export function chartRuntimeFactory(getters: Getters) { if (!builder) { throw new Error("No runtime builder for this chart."); } - const runtime = builder.getChartRuntime(chart, getters); const definition = chart.getDefinition(); + const data = builder.extractData(definition, chart.sheetId, getters); + const runtime = builder.getChartRuntime(getters, chart, data); if ("chartJsConfig" in runtime && /line|combo|bar|scatter|waterfall/.test(definition.type)) { const chartJsConfig = runtime.chartJsConfig as ChartConfiguration; runtime["masterChartConfig"] = generateMasterChartConfig(chartJsConfig); diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts index 6ee6119c22..79c035dddb 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts @@ -448,8 +448,8 @@ export function drawScoreChart( } export function createScorecardChartRuntime( - chart: ScorecardChart, - getters: Getters + getters: Getters, + chart: ScorecardChart ): ScorecardChartRuntime { let formattedKeyValue = ""; let keyValueCell: EvaluatedCell | undefined; diff --git a/packages/o-spreadsheet-engine/src/registries/chart_registry.ts b/packages/o-spreadsheet-engine/src/registries/chart_registry.ts index 5e3ddc70bc..5db5122802 100644 --- a/packages/o-spreadsheet-engine/src/registries/chart_registry.ts +++ b/packages/o-spreadsheet-engine/src/registries/chart_registry.ts @@ -10,30 +10,27 @@ import { Registry } from "./registry"; /** * Instantiate a chart object based on a definition */ -export interface ChartBuilder { +export interface ChartBuilder { /** * Check if this factory should be used */ - match: (type: ChartType) => boolean; - createChart: (definition: ChartDefinition, sheetId: UID, getters: CoreGetters) => AbstractChart; - getChartRuntime: (chart: AbstractChart, getters: Getters) => ChartRuntime; - validateChartDefinition( - validator: Validator, - definition: ChartDefinition - ): CommandResult | CommandResult[]; - transformDefinition( - chartSheetId: UID, - definition: ChartDefinition, - applyRange: RangeAdapter - ): ChartDefinition; - getChartDefinitionFromContextCreation(context: ChartCreationContext): ChartDefinition; + match: (type: T["type"]) => boolean; + createChart: (definition: T, sheetId: UID, getters: CoreGetters) => AbstractChart; + extractData: (definition: T, sheetId: UID, getters: Getters) => D; + getChartRuntime: (getters: Getters, chart: AbstractChart, data: NoInfer) => ChartRuntime; + validateChartDefinition(validator: Validator, definition: T): CommandResult | CommandResult[]; + transformDefinition(chartSheetId: UID, definition: T, applyRange: RangeAdapter): T; + getChartDefinitionFromContextCreation(context: ChartCreationContext): T; allowedDefinitionKeys: readonly string[]; sequence: number; dataSeriesLimit?: number; } -/** - * This registry is intended to map a cell content (raw string) to - * an instance of a cell. - */ -export const chartRegistry = new Registry(); +interface ChartRegistry extends Registry> { + add( + type: T, + builder: ChartBuilder }>, D> + ): this; +} + +export const chartRegistry: ChartRegistry = new Registry(); diff --git a/packages/o-spreadsheet-engine/src/types/chart/chart.ts b/packages/o-spreadsheet-engine/src/types/chart/chart.ts index e5ad50c463..be0c233c7d 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/chart.ts @@ -236,6 +236,11 @@ export interface ChartCreationContext { export type ChartAxisFormats = { [axisId: string]: Format | undefined } | undefined; +export interface ChartData { + dataSetsValues: DatasetValues[]; + labelValues: LabelValues; +} + export interface ChartRuntimeGenerationArgs { dataSetsValues: DatasetValues[]; axisFormats: ChartAxisFormats; diff --git a/src/components/side_panel/chart/calendar_chart/calendar_chart_config_panel.ts b/src/components/side_panel/chart/calendar_chart/calendar_chart_config_panel.ts index f209f58746..b6ddb2df4c 100644 --- a/src/components/side_panel/chart/calendar_chart/calendar_chart_config_panel.ts +++ b/src/components/side_panel/chart/calendar_chart/calendar_chart_config_panel.ts @@ -5,9 +5,8 @@ import { CalendarChartDefinition, CalendarChartGranularity, } from "@odoo/o-spreadsheet-engine/types/chart/calendar_chart"; -import { createValidRange, isDateTime } from "../../../../helpers"; -import { createDataSets } from "../../../../helpers/figures/charts"; -import { getBarChartData } from "../../../../helpers/figures/charts/runtime"; +import { isDateTime } from "../../../../helpers"; +import { getBarChartData, getChartData } from "../../../../helpers/figures/charts/runtime"; import { DEFAULT_LOCALE } from "../../../../types"; import { GenericChartConfigPanel } from "../building_blocks/generic_side_panel/config_panel"; import { ChartSidePanelProps } from "../common"; @@ -37,24 +36,9 @@ export class CalendarChartConfigPanel extends GenericChartConfigPanel< const sheetId = this.env.model.getters.getFigureSheetId( this.env.model.getters.getFigureIdFromChartId(this.props.chartId) )!; - const dataSets = createDataSets( - this.env.model.getters, - this.props.definition.dataSets, - sheetId, - this.props.definition.dataSetsHaveTitle - ); - if (dataSets.length === 0) { - return []; - } - const labelRange = createValidRange( - this.env.model.getters, - sheetId, - this.props.definition.labelRange - ); const data = getBarChartData( this.props.definition, - dataSets, - labelRange, + getChartData(this.env.model.getters, sheetId, this.props.definition), this.env.model.getters ); const labels = data.labels.filter((l) => isDateTime(l, DEFAULT_LOCALE)); diff --git a/src/components/side_panel/chart/line_chart/line_chart_config_panel.ts b/src/components/side_panel/chart/line_chart/line_chart_config_panel.ts index 8f87f8d91b..7553994a2e 100644 --- a/src/components/side_panel/chart/line_chart/line_chart_config_panel.ts +++ b/src/components/side_panel/chart/line_chart/line_chart_config_panel.ts @@ -9,12 +9,7 @@ export class LineConfigPanel extends GenericChartConfigPanel { get canTreatLabelsAsText() { const chart = this.env.model.getters.getChart(this.props.chartId); if (chart && chart instanceof LineChart) { - return canChartParseLabels( - chart.getDefinition(), - chart.dataSets, - chart.labelRange, - this.env.model.getters - ); + return canChartParseLabels(this.env.model.getters, chart.sheetId, chart.getDefinition()); } return false; } diff --git a/src/components/side_panel/chart/scatter_chart/scatter_chart_config_panel.ts b/src/components/side_panel/chart/scatter_chart/scatter_chart_config_panel.ts index 2284b762c4..1f96e0fb5d 100644 --- a/src/components/side_panel/chart/scatter_chart/scatter_chart_config_panel.ts +++ b/src/components/side_panel/chart/scatter_chart/scatter_chart_config_panel.ts @@ -9,12 +9,7 @@ export class ScatterConfigPanel extends GenericChartConfigPanel { get canTreatLabelsAsText() { const chart = this.env.model.getters.getChart(this.props.chartId); if (chart && chart instanceof ScatterChart) { - return canChartParseLabels( - chart.getDefinition(), - chart.dataSets, - chart.labelRange, - this.env.model.getters - ); + return canChartParseLabels(this.env.model.getters, chart.sheetId, chart.getDefinition()); } return false; } diff --git a/src/helpers/figures/charts/bar_chart.ts b/src/helpers/figures/charts/bar_chart.ts index 91a2304739..a3f99b7bb9 100644 --- a/src/helpers/figures/charts/bar_chart.ts +++ b/src/helpers/figures/charts/bar_chart.ts @@ -9,7 +9,6 @@ import { duplicateDataSetsInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, - shouldRemoveFirstLabel, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; @@ -21,6 +20,7 @@ import { } from "@odoo/o-spreadsheet-engine/types/chart/bar_chart"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDefinition, @@ -170,12 +170,11 @@ export class BarChart extends AbstractChart { }; } - getDefinitionForExcel(): ExcelChartDefinition | undefined { + getDefinitionForExcel(getters: Getters): ExcelChartDefinition | undefined { const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, - this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) + this.dataSets ); return { ...definition, @@ -202,9 +201,13 @@ export class BarChart extends AbstractChart { } } -export function createBarChartRuntime(chart: BarChart, getters: Getters): BarChartRuntime { +export function createBarChartRuntime( + getters: Getters, + chart: BarChart, + data: ChartData +): BarChartRuntime { const definition = chart.getDefinition(); - const chartData = getBarChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getBarChartData(definition, data, getters); const config: ChartConfiguration<"bar" | "line"> = { type: "bar", diff --git a/src/helpers/figures/charts/calendar_chart.ts b/src/helpers/figures/charts/calendar_chart.ts index 2d6b4ddad4..63d694b9c8 100644 --- a/src/helpers/figures/charts/calendar_chart.ts +++ b/src/helpers/figures/charts/calendar_chart.ts @@ -15,6 +15,7 @@ import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { BarChartRuntime, ChartCreationContext, + ChartData, CustomizedDataSet, ExcelChartDefinition, LegendPosition, @@ -199,11 +200,12 @@ export class CalendarChart extends AbstractChart { } export function createCalendarChartRuntime( + getters: Getters, chart: CalendarChart, - getters: Getters + data: ChartData ): BarChartRuntime { const definition = chart.getDefinition(); - const chartData = getCalendarChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getCalendarChartData(definition, data, getters); const { labels, datasets } = getCalendarChartDatasetAndLabels(definition, chartData); const config: ChartConfiguration<"bar"> = { diff --git a/src/helpers/figures/charts/combo_chart.ts b/src/helpers/figures/charts/combo_chart.ts index 26c910f355..d95b32fe79 100644 --- a/src/helpers/figures/charts/combo_chart.ts +++ b/src/helpers/figures/charts/combo_chart.ts @@ -9,7 +9,6 @@ import { duplicateDataSetsInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, - shouldRemoveFirstLabel, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; @@ -25,6 +24,7 @@ import { ChartConfiguration } from "chart.js"; import { ApplyRangeChange, ChartCreationContext, + ChartData, CommandResult, CustomizedDataSet, DataSet, @@ -133,12 +133,11 @@ export class ComboChart extends AbstractChart { }; } - getDefinitionForExcel(): ExcelChartDefinition | undefined { + getDefinitionForExcel(getters: Getters): ExcelChartDefinition | undefined { const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, - this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) + this.dataSets ); return { ...definition, @@ -207,9 +206,13 @@ export class ComboChart extends AbstractChart { } } -export function createComboChartRuntime(chart: ComboChart, getters: Getters): ComboChartRuntime { +export function createComboChartRuntime( + getters: Getters, + chart: ComboChart, + data: ChartData +): ComboChartRuntime { const definition = chart.getDefinition(); - const chartData = getBarChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getBarChartData(definition, data, getters); const config: ChartConfiguration = { type: "bar", diff --git a/src/helpers/figures/charts/funnel_chart.ts b/src/helpers/figures/charts/funnel_chart.ts index 41cb234b63..40ea58fcea 100644 --- a/src/helpers/figures/charts/funnel_chart.ts +++ b/src/helpers/figures/charts/funnel_chart.ts @@ -15,6 +15,7 @@ import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { FunnelChartDefinition, FunnelChartRuntime } from "@odoo/o-spreadsheet-engine/types/chart"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDefinition, @@ -177,9 +178,13 @@ export class FunnelChart extends AbstractChart { } } -export function createFunnelChartRuntime(chart: FunnelChart, getters: Getters): FunnelChartRuntime { +export function createFunnelChartRuntime( + getters: Getters, + chart: FunnelChart, + data: ChartData +): FunnelChartRuntime { const definition = chart.getDefinition(); - const chartData = getFunnelChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getFunnelChartData(definition, data, getters); const config: ChartConfiguration = { type: "funnel", diff --git a/src/helpers/figures/charts/gauge_chart.ts b/src/helpers/figures/charts/gauge_chart.ts index fd4b99c696..332fbfbd88 100644 --- a/src/helpers/figures/charts/gauge_chart.ts +++ b/src/helpers/figures/charts/gauge_chart.ts @@ -299,7 +299,7 @@ export class GaugeChart extends AbstractChart { } } -export function createGaugeChartRuntime(chart: GaugeChart, getters: Getters): GaugeChartRuntime { +export function createGaugeChartRuntime(getters: Getters, chart: GaugeChart): GaugeChartRuntime { const locale = getters.getLocale(); const chartColors = chart.sectionRule.colors; diff --git a/src/helpers/figures/charts/geo_chart.ts b/src/helpers/figures/charts/geo_chart.ts index 0d8e8a897e..04c4c615ec 100644 --- a/src/helpers/figures/charts/geo_chart.ts +++ b/src/helpers/figures/charts/geo_chart.ts @@ -14,6 +14,7 @@ import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDefinition, @@ -170,9 +171,13 @@ export class GeoChart extends AbstractChart { } } -export function createGeoChartRuntime(chart: GeoChart, getters: Getters): GeoChartRuntime { +export function createGeoChartRuntime( + getters: Getters, + chart: GeoChart, + data: ChartData +): GeoChartRuntime { const definition = chart.getDefinition(); - const chartData = getGeoChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getGeoChartData(definition, data, getters); const config: ChartConfiguration = { type: "choropleth", diff --git a/src/helpers/figures/charts/line_chart.ts b/src/helpers/figures/charts/line_chart.ts index ab4725e4f0..f60ca0cda2 100644 --- a/src/helpers/figures/charts/line_chart.ts +++ b/src/helpers/figures/charts/line_chart.ts @@ -9,7 +9,6 @@ import { duplicateDataSetsInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, - shouldRemoveFirstLabel, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; @@ -17,6 +16,7 @@ import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, + ChartData, ChartJSRuntime, CustomizedDataSet, DataSet, @@ -164,12 +164,11 @@ export class LineChart extends AbstractChart { return new LineChart(definition, this.sheetId, this.getters); } - getDefinitionForExcel(): ExcelChartDefinition | undefined { + getDefinitionForExcel(getters: Getters): ExcelChartDefinition | undefined { const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, - this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) + this.dataSets ); return { ...definition, @@ -202,9 +201,13 @@ export class LineChart extends AbstractChart { } } -export function createLineChartRuntime(chart: LineChart, getters: Getters): ChartJSRuntime { +export function createLineChartRuntime( + getters: Getters, + chart: LineChart, + data: ChartData +): ChartJSRuntime { const definition = chart.getDefinition(); - const chartData = getLineChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getLineChartData(definition, data, getters); const config: ChartConfiguration = { type: "line", diff --git a/src/helpers/figures/charts/pie_chart.ts b/src/helpers/figures/charts/pie_chart.ts index 8e01d37e9d..ca1c576b5f 100644 --- a/src/helpers/figures/charts/pie_chart.ts +++ b/src/helpers/figures/charts/pie_chart.ts @@ -8,7 +8,6 @@ import { createDataSets, duplicateDataSetsInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, - shouldRemoveFirstLabel, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; @@ -16,6 +15,7 @@ import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, + ChartData, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -149,12 +149,11 @@ export class PieChart extends AbstractChart { return new PieChart(definition, sheetId, this.getters); } - getDefinitionForExcel(): ExcelChartDefinition | undefined { + getDefinitionForExcel(getters: Getters): ExcelChartDefinition | undefined { const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, - this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) + this.dataSets ); return { ...definition, @@ -180,9 +179,13 @@ export class PieChart extends AbstractChart { } } -export function createPieChartRuntime(chart: PieChart, getters: Getters): PieChartRuntime { +export function createPieChartRuntime( + getters: Getters, + chart: PieChart, + data: ChartData +): PieChartRuntime { const definition = chart.getDefinition(); - const chartData = getPieChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getPieChartData(definition, data, getters); const config: ChartConfiguration<"doughnut" | "pie"> = { type: definition.isDoughnut ? "doughnut" : "pie", diff --git a/src/helpers/figures/charts/pyramid_chart.ts b/src/helpers/figures/charts/pyramid_chart.ts index 87a49eed1a..5c9d8c5b37 100644 --- a/src/helpers/figures/charts/pyramid_chart.ts +++ b/src/helpers/figures/charts/pyramid_chart.ts @@ -10,7 +10,6 @@ import { duplicateDataSetsInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, - shouldRemoveFirstLabel, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; @@ -18,6 +17,7 @@ import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDefinition, @@ -32,6 +32,7 @@ import { ApplyRangeChange, CommandResult, Getters, Range, RangeAdapter, UID } fr import { getBarChartDatasets, getBarChartLegend, + getChartData, getChartTitle, getPyramidChartData, getPyramidChartScales, @@ -172,10 +173,10 @@ export class PyramidChart extends AbstractChart { const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, - this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) + this.dataSets ); - const chartData = getPyramidChartData(definition, this.dataSets, this.labelRange, getters); + const data = getChartData(getters, this.sheetId, definition); + const chartData = getPyramidChartData(definition, data, getters); const { dataSetsValues } = chartData; const maxValue = Math.max( ...dataSetsValues.map((dataSet) => @@ -212,11 +213,12 @@ export class PyramidChart extends AbstractChart { } export function createPyramidChartRuntime( + getters: Getters, chart: PyramidChart, - getters: Getters + data: ChartData ): PyramidChartRuntime { const definition = chart.getDefinition(); - const chartData = getPyramidChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getPyramidChartData(definition, data, getters); const config: ChartConfiguration = { type: "bar", diff --git a/src/helpers/figures/charts/radar_chart.ts b/src/helpers/figures/charts/radar_chart.ts index 59366950b4..2ae726a2a4 100644 --- a/src/helpers/figures/charts/radar_chart.ts +++ b/src/helpers/figures/charts/radar_chart.ts @@ -8,7 +8,6 @@ import { createDataSets, duplicateDataSetsInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, - shouldRemoveFirstLabel, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; @@ -16,6 +15,7 @@ import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDefinition, @@ -163,12 +163,11 @@ export class RadarChart extends AbstractChart { }; } - getDefinitionForExcel(): ExcelChartDefinition | undefined { + getDefinitionForExcel(getters: Getters): ExcelChartDefinition | undefined { const definition = this.getDefinition(); const { dataSets, labelRange } = this.getCommonDataSetAttributesForExcel( this.labelRange, - this.dataSets, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) + this.dataSets ); return { ...definition, @@ -194,9 +193,13 @@ export class RadarChart extends AbstractChart { } } -export function createRadarChartRuntime(chart: RadarChart, getters: Getters): RadarChartRuntime { +export function createRadarChartRuntime( + getters: Getters, + chart: RadarChart, + data: ChartData +): RadarChartRuntime { const definition = chart.getDefinition(); - const chartData = getRadarChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getRadarChartData(definition, data, getters); const config: ChartConfiguration = { type: "radar", diff --git a/src/helpers/figures/charts/runtime/chart_data_extractor.ts b/src/helpers/figures/charts/runtime/chart_data_extractor.ts index 1bf3444a4c..2d72872570 100644 --- a/src/helpers/figures/charts/runtime/chart_data_extractor.ts +++ b/src/helpers/figures/charts/runtime/chart_data_extractor.ts @@ -1,10 +1,4 @@ -import { - _t, - deepCopy, - DEFAULT_LOCALE, - findNextDefinedValue, - range, -} from "@odoo/o-spreadsheet-engine"; +import { _t, deepCopy, findNextDefinedValue, range, UID } from "@odoo/o-spreadsheet-engine"; import { ChartTerms } from "@odoo/o-spreadsheet-engine/components/translations_terms"; import { evaluatePolynomial, @@ -20,7 +14,10 @@ import { isNumberCell, isTextCell, } from "@odoo/o-spreadsheet-engine/helpers/cells/cell_evaluation"; -import { shouldRemoveFirstLabel } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; +import { + createDataSets, + shouldRemoveFirstLabel, +} from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; import { DAYS, formatValue, @@ -28,12 +25,14 @@ import { MONTHS, } from "@odoo/o-spreadsheet-engine/helpers/format/format"; import { createDate } from "@odoo/o-spreadsheet-engine/helpers/pivot/spreadsheet_pivot/date_spreadsheet_pivot"; +import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { recomputeZones } from "@odoo/o-spreadsheet-engine/helpers/recompute_zones"; -import { positions } from "@odoo/o-spreadsheet-engine/helpers/zones"; import { AxisType, BarChartDefinition, + ChartData, ChartRuntimeGenerationArgs, + ChartWithDataSetDefinition, CustomizedDataSet, DataSet, DatasetValues, @@ -58,7 +57,6 @@ import { TreeMapChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart/t import { Point } from "chart.js"; import { CellValue, - CellValueType, Format, FunctionResultObject, GenericDefinition, @@ -74,18 +72,11 @@ const ONE = Object.freeze({ value: 1 }); export function getBarChartData( definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): ChartRuntimeGenerationArgs { const locale = getters.getLocale(); - let labels = getChartLabelValues(getters, dataSets, labelRange).map(({ value, format }) => - formatValue(value, { format, locale }) - ); - let dataSetsValues = getChartDatasetValues(getters, dataSets); - if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { - labels.shift(); - } + let labels = labelValues.map(({ value, format }) => formatValue(value, { format, locale })); ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues)); if (definition.aggregated) { @@ -209,18 +200,12 @@ function computeValuesAndLabels( export function getCalendarChartData( definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): ChartRuntimeGenerationArgs { - const labelValues = getChartLabelValues(getters, dataSets, labelRange); let labels = labelValues; - let dataSetsValues = getChartDatasetValues(getters, dataSets); - if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { - labels.shift(); - } - const locale = getters.getLocale() || DEFAULT_LOCALE; + const locale = getters.getLocale(); ({ labels, dataSetsValues } = filterInvalidCalendarDataPoints(labels, dataSetsValues)); const axisFormats = { y: getChartDatasetFormat(definition.dataSets, dataSetsValues, "left") }; @@ -244,11 +229,14 @@ export function getCalendarChartData( export function getPyramidChartData( definition: PyramidChartDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): ChartRuntimeGenerationArgs { - const barChartData = getBarChartData(definition, dataSets.slice(0, 2), labelRange, getters); + const barChartData = getBarChartData( + definition, + { labelValues, dataSetsValues: dataSetsValues.slice(0, 2) }, + getters + ); const barDataset = barChartData.dataSetsValues.filter((ds) => !ds.hidden); const pyramidDatasetValues: DatasetValues[] = []; @@ -273,27 +261,16 @@ export function getPyramidChartData( export function getLineChartData( definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): ChartRuntimeGenerationArgs { - const axisType = getChartAxisType(definition, dataSets, labelRange, getters); - const labelValues = getChartLabelValues(getters, dataSets, labelRange); + const axisType = getChartAxisType(definition, { labelValues, dataSetsValues }); let labels = axisType === "linear" ? labelValues.map(({ value }) => String(value ?? "")) : labelValues.map(({ value, format }) => formatValue(value, { format, locale: getters.getLocale() }) ); - let dataSetsValues = getChartDatasetValues(getters, dataSets); - const removeFirstLabel = shouldRemoveFirstLabel( - labelRange, - dataSets[0], - definition.dataSetsHaveTitle || false - ); - if (removeFirstLabel) { - labels.shift(); - } ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues)); if (axisType === "time") { @@ -308,7 +285,7 @@ export function getLineChartData( const leftAxisFormat = getChartDatasetFormat(definition.dataSets, dataSetsValues, "left"); const rightAxisFormat = getChartDatasetFormat(definition.dataSets, dataSetsValues, "right"); - const labelsFormat = getChartLabelFormat(getters, labelRange, removeFirstLabel); + const labelsFormat = getChartLabelFormat(labelValues); const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat, x: labelsFormat }; const trendDataSetsValues: (Point[] | undefined)[] = []; @@ -338,18 +315,12 @@ export function getLineChartData( export function getPieChartData( definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): ChartRuntimeGenerationArgs { - const labelValues = getChartLabelValues(getters, dataSets, labelRange); let labels = labelValues.map(({ value, format }) => formatValue(value, { format, locale: getters.getLocale() }) ); - let dataSetsValues = getChartDatasetValues(getters, dataSets); - if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { - labels.shift(); - } ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues)); @@ -372,18 +343,12 @@ export function getPieChartData( export function getRadarChartData( definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): ChartRuntimeGenerationArgs { - const labelValues = getChartLabelValues(getters, dataSets, labelRange); let labels = labelValues.map(({ value, format }) => formatValue(value, { format, locale: getters.getLocale() }) ); - let dataSetsValues = getChartDatasetValues(getters, dataSets); - if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { - labels.shift(); - } ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues)); if (definition.aggregated) { @@ -405,19 +370,13 @@ export function getRadarChartData( export function getGeoChartData( definition: GeoChartDefinition, - fullDataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): GeoChartRuntimeGenerationArgs { - const dataSets = fullDataSets.slice(0, 1); - const labelValues = getChartLabelValues(getters, dataSets, labelRange); + dataSetsValues = dataSetsValues.slice(0, 1); let labels = labelValues.map(({ value, format }) => formatValue(value, { format, locale: getters.getLocale() }) ); - if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { - labels.shift(); - } - let dataSetsValues = getChartDatasetValues(getters, dataSets); ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues)); const format = @@ -437,18 +396,12 @@ export function getGeoChartData( export function getFunnelChartData( definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): ChartRuntimeGenerationArgs { - const labelValues = getChartLabelValues(getters, dataSets, labelRange); let labels = labelValues.map(({ value, format }) => formatValue(value, { format, locale: getters.getLocale() }) ); - let dataSetsValues = getChartDatasetValues(getters, dataSets); - if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { - labels.shift(); - } ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues)); if (definition.aggregated) { @@ -472,27 +425,17 @@ export function getFunnelChartData( export function getHierarchalChartData( definition: SunburstChartDefinition | TreeMapChartDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, + { labelValues, dataSetsValues }: ChartData, getters: Getters ): ChartRuntimeGenerationArgs { // In hierarchical charts, labels are the leaf values (numbers), and the hierarchy is defined in the dataSets (strings) - let labels = getChartLabelValues(getters, dataSets, labelRange); - let dataSetsValues = getHierarchicalDatasetValues(getters, dataSets); - const removeFirstLabel = shouldRemoveFirstLabel( - labelRange, - dataSets[0], - definition.dataSetsHaveTitle || false - ); - if (removeFirstLabel) { - labels.shift(); - } + let labels = labelValues; ({ labels, dataSetsValues } = filterValuesWithDifferentSigns(labels, dataSetsValues)); ({ labels, dataSetsValues } = filterInvalidHierarchicalPoints(labels, dataSetsValues)); return { dataSetsValues, - axisFormats: { y: getChartLabelFormat(getters, labelRange, removeFirstLabel) }, + axisFormats: { y: getChartLabelFormat(labels) }, labels: labels.map(({ value }) => String(value ?? "")), locale: getters.getLocale(), }; @@ -684,83 +627,49 @@ function normalizeLabels( function getChartAxisType( definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, - getters: Getters + data: ChartData ): AxisType { - if (isDateChart(definition, dataSets, labelRange, getters) && isLuxonTimeAdapterInstalled()) { + if (isDateChart(definition, data) && isLuxonTimeAdapterInstalled()) { return "time"; } - if (isLinearChart(definition, dataSets, labelRange, getters)) { + if (isLinearChart(definition, data)) { return "linear"; } return "category"; } -function isDateChart( - definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, - getters: Getters -): boolean { - return !definition.labelsAsText && canBeDateChart(definition, dataSets, labelRange, getters); +function isDateChart(definition: GenericDefinition, data: ChartData): boolean { + return !definition.labelsAsText && canBeDateChart(data); } function isLinearChart( definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, - getters: Getters + data: ChartData ): boolean { - return !definition.labelsAsText && canBeLinearChart(definition, dataSets, labelRange, getters); + return !definition.labelsAsText && canBeLinearChart(data); } export function canChartParseLabels( - definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, - getters: Getters + getters: Getters, + sheetId: UID, + definition: ChartWithDataSetDefinition ): boolean { - return ( - canBeDateChart(definition, dataSets, labelRange, getters) || - canBeLinearChart(definition, dataSets, labelRange, getters) - ); + const data = getChartData(getters, sheetId, definition); + return canBeDateChart(data) || canBeLinearChart(data); } -function canBeDateChart( - definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, - getters: Getters -): boolean { - if (!labelRange || !canBeLinearChart(definition, dataSets, labelRange, getters)) { +function canBeDateChart(data: ChartData): boolean { + if (!canBeLinearChart(data)) { return false; } - const removeFirstLabel = shouldRemoveFirstLabel( - labelRange, - dataSets[0], - definition.dataSetsHaveTitle || false - ); - const labelFormat = getChartLabelFormat(getters, labelRange, removeFirstLabel); + const labelFormat = getChartLabelFormat(data.labelValues); return Boolean(labelFormat && timeFormatLuxonCompatible.test(labelFormat)); } -function canBeLinearChart( - definition: GenericDefinition, - dataSets: DataSet[], - labelRange: Range | undefined, - getters: Getters -): boolean { - if (!labelRange) { - return false; - } - - const labels = getters.getRangeValues(labelRange); - if (shouldRemoveFirstLabel(labelRange, dataSets[0], definition.dataSetsHaveTitle || false)) { - labels.shift(); - } +function canBeLinearChart(data: ChartData): boolean { + const labels = data.labelValues; - if (labels.some((label) => label.type !== CellValueType.number && label.value)) { + if (labels.some((label) => !isNumberCell(label) && label.value)) { return false; } if (labels.every((label) => !label.value)) { @@ -998,23 +907,62 @@ function aggregateDataForLabels( }; } -export function getChartLabelFormat( - getters: Getters, - range: Range | undefined, - shouldRemoveFirstLabel: boolean -): Format | undefined { - if (!range) return undefined; - - const { sheetId, zone } = range; +export function getChartLabelFormat(labelValues: LabelValues): Format | undefined { + return labelValues.find(({ format }) => format !== undefined)?.format; +} - const formats = positions(zone).map( - (position) => getters.getEvaluatedCell({ sheetId, ...position }).format +export function getChartData( + getters: Getters, + sheetId: UID, + definition: ChartWithDataSetDefinition +): ChartData { + const dataSets = createDataSets( + getters, + definition.dataSets, + sheetId, + definition.dataSetsHaveTitle ); - if (shouldRemoveFirstLabel) { - formats.shift(); + const labelRange = createValidRange(getters, sheetId, definition.labelRange); + const labelValues = getChartLabelValues(getters, dataSets, labelRange); + const dataSetsValues = getChartDatasetValues(getters, dataSets); + const data = { labelValues, dataSetsValues }; + if ( + shouldRemoveFirstLabel( + labelValues.length, + dataSetsValues[0]?.data.length + (dataSetsValues[0]?.label !== undefined ? 1 : 0), + definition.dataSetsHaveTitle || false + ) + ) { + labelValues.shift(); } + return data; +} - return formats.find((format) => format !== undefined); +export function getHierarchicalData( + getters: Getters, + sheetId: UID, + definition: ChartWithDataSetDefinition +): ChartData { + const dataSets = createDataSets( + getters, + definition.dataSets, + sheetId, + definition.dataSetsHaveTitle + ); + const labelRange = createValidRange(getters, sheetId, definition.labelRange); + const labelValues = getChartLabelValues(getters, dataSets, labelRange); + const dataSetsValues = getHierarchicalDatasetValues(getters, dataSets); + const data = { labelValues, dataSetsValues }; + if ( + shouldRemoveFirstLabel( + labelValues.length, + dataSetsValues[0]?.data.length + (dataSetsValues[0]?.label !== undefined ? 1 : 0), + definition.dataSetsHaveTitle || false + ) + ) { + labelValues.shift(); + } + return data; } function getChartLabelValues( diff --git a/src/helpers/figures/charts/scatter_chart.ts b/src/helpers/figures/charts/scatter_chart.ts index 6115101fb9..9b7d5786e0 100644 --- a/src/helpers/figures/charts/scatter_chart.ts +++ b/src/helpers/figures/charts/scatter_chart.ts @@ -17,8 +17,10 @@ import { } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common"; import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; +import { getZoneArea } from "@odoo/o-spreadsheet-engine/helpers/zones"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDataset, @@ -166,10 +168,15 @@ export class ScatterChart extends AbstractChart { const dataSets: ExcelChartDataset[] = this.dataSets .map((ds: DataSet) => toExcelDataset(this.getters, ds)) .filter((ds) => ds.range !== ""); + + const datasetLength = this.dataSets[0] + ? getZoneArea(this.dataSets[0].dataRange.zone) + : undefined; + const labelLength = this.labelRange ? getZoneArea(this.labelRange.zone) : 0; const labelRange = toExcelLabelRange( this.getters, this.labelRange, - shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], definition.dataSetsHaveTitle) + shouldRemoveFirstLabel(labelLength, datasetLength, definition.dataSetsHaveTitle) ); return { ...definition, @@ -203,11 +210,12 @@ export class ScatterChart extends AbstractChart { } export function createScatterChartRuntime( + getters: Getters, chart: ScatterChart, - getters: Getters + data: ChartData ): ScatterChartRuntime { const definition = chart.getDefinition(); - const chartData = getLineChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getLineChartData(definition, data, getters); const config: ChartConfiguration<"line"> = { // use chartJS line chart and disable the lines instead of chartJS scatter chart. This is because the scatter chart diff --git a/src/helpers/figures/charts/sunburst_chart.ts b/src/helpers/figures/charts/sunburst_chart.ts index 19c574408a..f8c63034c4 100644 --- a/src/helpers/figures/charts/sunburst_chart.ts +++ b/src/helpers/figures/charts/sunburst_chart.ts @@ -18,6 +18,7 @@ import { } from "@odoo/o-spreadsheet-engine/types/chart"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDefinition, @@ -177,11 +178,12 @@ export class SunburstChart extends AbstractChart { } export function createSunburstChartRuntime( + getters: Getters, chart: SunburstChart, - getters: Getters + data: ChartData ): SunburstChartRuntime { const definition = chart.getDefinition(); - const chartData = getHierarchalChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getHierarchalChartData(definition, data, getters); const config: ChartConfiguration<"doughnut"> = { type: "doughnut", diff --git a/src/helpers/figures/charts/tree_map_chart.ts b/src/helpers/figures/charts/tree_map_chart.ts index 6d544fc96f..6d97ab372f 100644 --- a/src/helpers/figures/charts/tree_map_chart.ts +++ b/src/helpers/figures/charts/tree_map_chart.ts @@ -14,6 +14,7 @@ import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDefinition, @@ -185,11 +186,12 @@ export class TreeMapChart extends AbstractChart { } export function createTreeMapChartRuntime( + getters: Getters, chart: TreeMapChart, - getters: Getters + data: ChartData ): TreeMapChartRuntime { const definition = chart.getDefinition(); - const chartData = getHierarchalChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getHierarchalChartData(definition, data, getters); const config: ChartConfiguration = { type: "treemap", diff --git a/src/helpers/figures/charts/waterfall_chart.ts b/src/helpers/figures/charts/waterfall_chart.ts index e9ba0ecc18..24a1e42a1e 100644 --- a/src/helpers/figures/charts/waterfall_chart.ts +++ b/src/helpers/figures/charts/waterfall_chart.ts @@ -14,6 +14,7 @@ import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, + ChartData, CustomizedDataSet, DataSet, ExcelChartDefinition, @@ -190,11 +191,12 @@ export class WaterfallChart extends AbstractChart { } export function createWaterfallChartRuntime( + getters: Getters, chart: WaterfallChart, - getters: Getters + data: ChartData ): WaterfallChartRuntime { const definition = chart.getDefinition(); - const chartData = getBarChartData(definition, chart.dataSets, chart.labelRange, getters); + const chartData = getBarChartData(definition, data, getters); const { labels, datasets } = getWaterfallDatasetAndLabels(definition, chartData); const config: ChartConfiguration = { diff --git a/src/registries/chart_types.ts b/src/registries/chart_types.ts index ddc70ee442..f9d1e8b734 100644 --- a/src/registries/chart_types.ts +++ b/src/registries/chart_types.ts @@ -41,6 +41,7 @@ import { createFunnelChartRuntime, FunnelChart } from "../helpers/figures/charts import { createGeoChartRuntime, GeoChart } from "../helpers/figures/charts/geo_chart"; import { createPyramidChartRuntime, PyramidChart } from "../helpers/figures/charts/pyramid_chart"; import { createRadarChartRuntime, RadarChart } from "../helpers/figures/charts/radar_chart"; +import { getChartData, getHierarchicalData } from "../helpers/figures/charts/runtime"; import { createScatterChartRuntime, ScatterChart } from "../helpers/figures/charts/scatter_chart"; import { createSunburstChartRuntime, @@ -56,6 +57,7 @@ chartRegistry.add("bar", { match: (type) => type === "bar", createChart: (definition, sheetId, getters) => new BarChart(definition as BarChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createBarChartRuntime, validateChartDefinition: BarChart.validateChartDefinition, transformDefinition: BarChart.transformDefinition, @@ -67,6 +69,7 @@ chartRegistry.add("combo", { match: (type) => type === "combo", createChart: (definition, sheetId, getters) => new ComboChart(definition as ComboChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createComboChartRuntime, validateChartDefinition: ComboChart.validateChartDefinition, transformDefinition: ComboChart.transformDefinition, @@ -78,6 +81,7 @@ chartRegistry.add("line", { match: (type) => type === "line", createChart: (definition, sheetId, getters) => new LineChart(definition as LineChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createLineChartRuntime, validateChartDefinition: LineChart.validateChartDefinition, transformDefinition: LineChart.transformDefinition, @@ -89,6 +93,7 @@ chartRegistry.add("pie", { match: (type) => type === "pie", createChart: (definition, sheetId, getters) => new PieChart(definition as PieChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createPieChartRuntime, validateChartDefinition: PieChart.validateChartDefinition, transformDefinition: PieChart.transformDefinition, @@ -100,6 +105,7 @@ chartRegistry.add("scorecard", { match: (type) => type === "scorecard", createChart: (definition, sheetId, getters) => new ScorecardChart(definition as ScorecardChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => undefined, // totally custom. Handled in createScorecardChartRuntime getChartRuntime: createScorecardChartRuntime, validateChartDefinition: ScorecardChart.validateChartDefinition, transformDefinition: ScorecardChart.transformDefinition, @@ -111,6 +117,7 @@ chartRegistry.add("gauge", { match: (type) => type === "gauge", createChart: (definition, sheetId, getters) => new GaugeChart(definition as GaugeChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => undefined, // totally custom. Handled in createScorecardChartRuntime getChartRuntime: createGaugeChartRuntime, validateChartDefinition: GaugeChart.validateChartDefinition, transformDefinition: GaugeChart.transformDefinition, @@ -122,6 +129,7 @@ chartRegistry.add("scatter", { match: (type) => type === "scatter", createChart: (definition, sheetId, getters) => new ScatterChart(definition as ScatterChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createScatterChartRuntime, validateChartDefinition: ScatterChart.validateChartDefinition, transformDefinition: ScatterChart.transformDefinition, @@ -133,6 +141,7 @@ chartRegistry.add("waterfall", { match: (type) => type === "waterfall", createChart: (definition, sheetId, getters) => new WaterfallChart(definition as WaterfallChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createWaterfallChartRuntime, validateChartDefinition: WaterfallChart.validateChartDefinition, transformDefinition: WaterfallChart.transformDefinition, @@ -144,6 +153,7 @@ chartRegistry.add("pyramid", { match: (type) => type === "pyramid", createChart: (definition, sheetId, getters) => new PyramidChart(definition as PyramidChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createPyramidChartRuntime, validateChartDefinition: PyramidChart.validateChartDefinition, transformDefinition: PyramidChart.transformDefinition, @@ -156,6 +166,7 @@ chartRegistry.add("radar", { match: (type) => type === "radar", createChart: (definition, sheetId, getters) => new RadarChart(definition as RadarChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createRadarChartRuntime, validateChartDefinition: RadarChart.validateChartDefinition, transformDefinition: RadarChart.transformDefinition, @@ -167,6 +178,7 @@ chartRegistry.add("geo", { match: (type) => type === "geo", createChart: (definition, sheetId, getters) => new GeoChart(definition as GeoChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createGeoChartRuntime, validateChartDefinition: GeoChart.validateChartDefinition, transformDefinition: GeoChart.transformDefinition, @@ -179,6 +191,7 @@ chartRegistry.add("funnel", { match: (type) => type === "funnel", createChart: (definition, sheetId, getters) => new FunnelChart(definition as FunnelChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), getChartRuntime: createFunnelChartRuntime, validateChartDefinition: FunnelChart.validateChartDefinition, transformDefinition: FunnelChart.transformDefinition, @@ -191,6 +204,7 @@ chartRegistry.add("sunburst", { match: (type) => type === "sunburst", createChart: (definition, sheetId, getters) => new SunburstChart(definition as SunburstChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getHierarchicalData(getters, sheetId, definition), getChartRuntime: createSunburstChartRuntime, validateChartDefinition: SunburstChart.validateChartDefinition, transformDefinition: SunburstChart.transformDefinition, @@ -202,6 +216,7 @@ chartRegistry.add("treemap", { match: (type) => type === "treemap", createChart: (definition, sheetId, getters) => new TreeMapChart(definition as TreeMapChartDefinition, sheetId, getters), + extractData: (definition, sheetId, getters) => getHierarchicalData(getters, sheetId, definition), getChartRuntime: createTreeMapChartRuntime, validateChartDefinition: TreeMapChart.validateChartDefinition, transformDefinition: TreeMapChart.transformDefinition, @@ -213,9 +228,8 @@ chartRegistry.add("calendar", { match: (type) => type === "calendar", createChart: (definition, sheetId, getters) => new CalendarChart(definition as CalendarChartDefinition, sheetId, getters), - getChartRuntime: (chart, getters) => { - return createCalendarChartRuntime(chart as CalendarChart, getters); - }, + extractData: (definition, sheetId, getters) => getChartData(getters, sheetId, definition), + getChartRuntime: createCalendarChartRuntime, validateChartDefinition: CalendarChart.validateChartDefinition, transformDefinition: CalendarChart.transformDefinition, getChartDefinitionFromContextCreation: CalendarChart.getDefinitionFromContextCreation, From b0342f3f66209c54fb087ec4ec970ac98bb8bc80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Tue, 2 Dec 2025 11:37:39 +0100 Subject: [PATCH 8/9] [REF] chart: remove useless range conversion --- .../helpers/figures/charts/scorecard_chart.ts | 11 ++++------- src/helpers/figures/charts/bar_chart.ts | 16 ++++------------ src/helpers/figures/charts/calendar_chart.ts | 12 ++++-------- src/helpers/figures/charts/combo_chart.ts | 17 ++++------------- src/helpers/figures/charts/funnel_chart.ts | 16 ++++------------ src/helpers/figures/charts/gauge_chart.ts | 7 +++---- src/helpers/figures/charts/geo_chart.ts | 16 ++++------------ src/helpers/figures/charts/line_chart.ts | 16 ++++------------ src/helpers/figures/charts/pie_chart.ts | 11 ++++------- src/helpers/figures/charts/pyramid_chart.ts | 16 ++++------------ src/helpers/figures/charts/radar_chart.ts | 16 ++++------------ src/helpers/figures/charts/scatter_chart.ts | 16 ++++------------ src/helpers/figures/charts/sunburst_chart.ts | 15 ++++++--------- src/helpers/figures/charts/tree_map_chart.ts | 17 +++++++---------- src/helpers/figures/charts/waterfall_chart.ts | 16 ++++------------ 15 files changed, 64 insertions(+), 154 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts index 79c035dddb..28655d1be1 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/scorecard_chart.ts @@ -259,14 +259,11 @@ export class ScorecardChart extends AbstractChart { } getContextCreation(): ChartCreationContext { + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range: this.keyValue - ? [{ dataRange: this.getters.getRangeString(this.keyValue, this.sheetId) }] - : undefined, - auxiliaryRange: this.baseline - ? this.getters.getRangeString(this.baseline, this.sheetId) - : undefined, + ...definition, + range: definition.keyValue ? [{ dataRange: definition.keyValue }] : undefined, + auxiliaryRange: definition.baseline, }; } diff --git a/src/helpers/figures/charts/bar_chart.ts b/src/helpers/figures/charts/bar_chart.ts index a3f99b7bb9..2bd1fc2d00 100644 --- a/src/helpers/figures/charts/bar_chart.ts +++ b/src/helpers/figures/charts/bar_chart.ts @@ -107,19 +107,11 @@ export class BarChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/calendar_chart.ts b/src/helpers/figures/charts/calendar_chart.ts index 63d694b9c8..9ffc14e718 100644 --- a/src/helpers/figures/charts/calendar_chart.ts +++ b/src/helpers/figures/charts/calendar_chart.ts @@ -126,15 +126,11 @@ export class CalendarChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = [ - { dataRange: this.getters.getRangeString(this.dataSets[0].dataRange, this.sheetId) }, - ]; + const definition = this.getDefinition(); return { - ...this, - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: [definition.dataSets[0]], + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/combo_chart.ts b/src/helpers/figures/charts/combo_chart.ts index d95b32fe79..c53537beaa 100644 --- a/src/helpers/figures/charts/combo_chart.ts +++ b/src/helpers/figures/charts/combo_chart.ts @@ -26,7 +26,6 @@ import { ChartCreationContext, ChartData, CommandResult, - CustomizedDataSet, DataSet, ExcelChartDefinition, Getters, @@ -90,19 +89,11 @@ export class ComboChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/funnel_chart.ts b/src/helpers/figures/charts/funnel_chart.ts index 40ea58fcea..26f1bc664f 100644 --- a/src/helpers/figures/charts/funnel_chart.ts +++ b/src/helpers/figures/charts/funnel_chart.ts @@ -97,19 +97,11 @@ export class FunnelChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/gauge_chart.ts b/src/helpers/figures/charts/gauge_chart.ts index 332fbfbd88..2c60229f3d 100644 --- a/src/helpers/figures/charts/gauge_chart.ts +++ b/src/helpers/figures/charts/gauge_chart.ts @@ -272,11 +272,10 @@ export class GaugeChart extends AbstractChart { } getContextCreation(): ChartCreationContext { + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range: this.dataRange - ? [{ dataRange: this.getters.getRangeString(this.dataRange, this.sheetId) }] - : undefined, + ...definition, + range: definition.dataRange ? [{ dataRange: definition.dataRange }] : undefined, }; } diff --git a/src/helpers/figures/charts/geo_chart.ts b/src/helpers/figures/charts/geo_chart.ts index 04c4c615ec..a7cc614324 100644 --- a/src/helpers/figures/charts/geo_chart.ts +++ b/src/helpers/figures/charts/geo_chart.ts @@ -90,19 +90,11 @@ export class GeoChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/line_chart.ts b/src/helpers/figures/charts/line_chart.ts index f60ca0cda2..d8e052748d 100644 --- a/src/helpers/figures/charts/line_chart.ts +++ b/src/helpers/figures/charts/line_chart.ts @@ -134,19 +134,11 @@ export class LineChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/pie_chart.ts b/src/helpers/figures/charts/pie_chart.ts index ca1c576b5f..89bfc08264 100644 --- a/src/helpers/figures/charts/pie_chart.ts +++ b/src/helpers/figures/charts/pie_chart.ts @@ -101,14 +101,11 @@ export class PieChart extends AbstractChart { } getContextCreation(): ChartCreationContext { + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range: this.dataSets.map((ds: DataSet) => ({ - dataRange: this.getters.getRangeString(ds.dataRange, this.sheetId), - })), - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/pyramid_chart.ts b/src/helpers/figures/charts/pyramid_chart.ts index 5c9d8c5b37..00f4ad54bc 100644 --- a/src/helpers/figures/charts/pyramid_chart.ts +++ b/src/helpers/figures/charts/pyramid_chart.ts @@ -105,19 +105,11 @@ export class PyramidChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/radar_chart.ts b/src/helpers/figures/charts/radar_chart.ts index 2ae726a2a4..9a9ac052b3 100644 --- a/src/helpers/figures/charts/radar_chart.ts +++ b/src/helpers/figures/charts/radar_chart.ts @@ -101,19 +101,11 @@ export class RadarChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/scatter_chart.ts b/src/helpers/figures/charts/scatter_chart.ts index 9b7d5786e0..d7c3ebc4a6 100644 --- a/src/helpers/figures/charts/scatter_chart.ts +++ b/src/helpers/figures/charts/scatter_chart.ts @@ -133,19 +133,11 @@ export class ScatterChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } diff --git a/src/helpers/figures/charts/sunburst_chart.ts b/src/helpers/figures/charts/sunburst_chart.ts index f8c63034c4..e20adc0256 100644 --- a/src/helpers/figures/charts/sunburst_chart.ts +++ b/src/helpers/figures/charts/sunburst_chart.ts @@ -108,16 +108,13 @@ export class SunburstChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const leafRange = this.dataSets.at(-1)?.dataRange; + const definition = this.getDefinition(); + const leafRange = definition.dataSets.at(-1)?.dataRange; return { - ...this.getDefinition(), - range: this.labelRange - ? [{ dataRange: this.getters.getRangeString(this.labelRange, this.sheetId) }] - : [], - auxiliaryRange: leafRange ? this.getters.getRangeString(leafRange, this.sheetId) : undefined, - hierarchicalRanges: this.dataSets.map((ds: DataSet) => ({ - dataRange: this.getters.getRangeString(ds.dataRange, this.sheetId), - })), + ...definition, + range: definition.labelRange ? [{ dataRange: definition.labelRange }] : [], + auxiliaryRange: leafRange, + hierarchicalRanges: definition.dataSets, }; } diff --git a/src/helpers/figures/charts/tree_map_chart.ts b/src/helpers/figures/charts/tree_map_chart.ts index 6d97ab372f..09a229886a 100644 --- a/src/helpers/figures/charts/tree_map_chart.ts +++ b/src/helpers/figures/charts/tree_map_chart.ts @@ -111,17 +111,14 @@ export class TreeMapChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const leafRange = this.dataSets.at(-1)?.dataRange; + const definition = this.getDefinition(); + const leafRange = definition.dataSets.at(-1)?.dataRange; return { - ...this.getDefinition(), - treemapColoringOptions: this.definition.coloringOptions, - range: this.labelRange - ? [{ dataRange: this.getters.getRangeString(this.labelRange, this.sheetId) }] - : [], - auxiliaryRange: leafRange ? this.getters.getRangeString(leafRange, this.sheetId) : undefined, - hierarchicalRanges: this.dataSets.map((ds: DataSet) => ({ - dataRange: this.getters.getRangeString(ds.dataRange, this.sheetId), - })), + ...definition, + treemapColoringOptions: definition.coloringOptions, + range: definition.labelRange ? [{ dataRange: definition.labelRange }] : [], + auxiliaryRange: leafRange, + hierarchicalRanges: definition.dataSets, }; } diff --git a/src/helpers/figures/charts/waterfall_chart.ts b/src/helpers/figures/charts/waterfall_chart.ts index 24a1e42a1e..17278be0fc 100644 --- a/src/helpers/figures/charts/waterfall_chart.ts +++ b/src/helpers/figures/charts/waterfall_chart.ts @@ -108,19 +108,11 @@ export class WaterfallChart extends AbstractChart { } getContextCreation(): ChartCreationContext { - const range: CustomizedDataSet[] = []; - for (const [i, dataSet] of this.dataSets.entries()) { - range.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId), - }); - } + const definition = this.getDefinition(); return { - ...this.getDefinition(), - range, - auxiliaryRange: this.labelRange - ? this.getters.getRangeString(this.labelRange, this.sheetId) - : undefined, + ...definition, + range: definition.dataSets, + auxiliaryRange: definition.labelRange, }; } From 4bcf288db333945a8c21687811ecc593d466b3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre=20=28lul=29?= Date: Tue, 2 Dec 2025 11:15:06 +0100 Subject: [PATCH 9/9] decouple series style from rangeData into dataSource --- .../src/helpers/carousel_helpers.ts | 3 +- .../helpers/figures/charts/chart_common.ts | 149 ++++++++++-------- .../helpers/figures/charts/scorecard_chart.ts | 6 +- .../src/migrations/migration_steps.ts | 35 ++++ .../src/types/chart/bar_chart.ts | 1 + .../src/types/chart/chart.ts | 33 ++-- .../src/types/chart/combo_chart.ts | 7 +- .../src/types/chart/common_chart.ts | 5 +- .../src/types/chart/funnel_chart.ts | 5 +- .../src/types/chart/line_chart.ts | 1 + .../src/types/chart/pyramid_chart.ts | 1 + .../src/types/chart/radar_chart.ts | 1 + .../src/types/chart/sunburst_chart.ts | 5 +- .../src/types/chart/tree_map_chart.ts | 5 +- .../src/xlsx/conversion/figure_conversion.ts | 15 +- .../data_series/data_series.ts | 7 +- .../generic_side_panel/config_panel.ts | 107 ++++++++----- .../series_design/series_design_editor.ts | 54 ++++--- .../series_design/series_design_editor.xml | 10 +- .../series_with_axis_design_editor.ts | 94 ++++++----- .../series_with_axis_design_editor.xml | 24 +-- .../combo_chart/combo_chart_design_panel.ts | 17 +- .../combo_chart/combo_chart_design_panel.xml | 6 +- .../gauge_chart_config_panel.ts | 4 +- .../geo_chart_panel/geo_chart_config_panel.ts | 6 +- .../main_chart_panel_store.ts | 18 ++- src/helpers/figures/charts/bar_chart.ts | 57 +++---- src/helpers/figures/charts/calendar_chart.ts | 51 +++--- src/helpers/figures/charts/combo_chart.ts | 82 ++++++---- src/helpers/figures/charts/funnel_chart.ts | 48 +++--- src/helpers/figures/charts/gauge_chart.ts | 6 +- src/helpers/figures/charts/geo_chart.ts | 48 +++--- src/helpers/figures/charts/line_chart.ts | 48 +++--- src/helpers/figures/charts/pie_chart.ts | 42 ++--- src/helpers/figures/charts/pyramid_chart.ts | 57 +++---- src/helpers/figures/charts/radar_chart.ts | 57 +++---- .../charts/runtime/chart_data_extractor.ts | 35 ++-- .../figures/charts/runtime/chartjs_dataset.ts | 30 ++-- src/helpers/figures/charts/scatter_chart.ts | 52 +++--- .../figures/charts/smart_chart_engine.ts | 60 ++++--- src/helpers/figures/charts/sunburst_chart.ts | 67 ++++---- src/helpers/figures/charts/tree_map_chart.ts | 61 +++---- src/helpers/figures/charts/waterfall_chart.ts | 49 +++--- .../figures/chart/combo_chart_plugin.test.ts | 29 +++- .../chart/menu_item_insert_chart.test.ts | 6 +- tests/xlsx/xlsx_export.test.ts | 31 ++-- 46 files changed, 849 insertions(+), 686 deletions(-) diff --git a/packages/o-spreadsheet-engine/src/helpers/carousel_helpers.ts b/packages/o-spreadsheet-engine/src/helpers/carousel_helpers.ts index b3c19b0744..14add10138 100644 --- a/packages/o-spreadsheet-engine/src/helpers/carousel_helpers.ts +++ b/packages/o-spreadsheet-engine/src/helpers/carousel_helpers.ts @@ -9,7 +9,8 @@ export const CAROUSEL_DEFAULT_CHART_DEFINITION: ChartDefinition = { title: {}, stacked: false, dataSetsHaveTitle: false, - dataSets: [], + dataSets: {}, + dataSource: { dataSets: [] }, legendPosition: "top", humanize: true, }; diff --git a/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_common.ts b/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_common.ts index 7fdf905cf1..08e6758e40 100644 --- a/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_common.ts +++ b/packages/o-spreadsheet-engine/src/helpers/figures/charts/chart_common.ts @@ -2,8 +2,8 @@ import { DEFAULT_WINDOW_SIZE, MAX_CHAR_LABEL } from "../../../constants"; import { _t } from "../../../translation"; import { ChartAxisFormats, + ChartRangeDataSource, ChartWithDataSetDefinition, - CustomizedDataSet, DataSet, DatasetValues, ExcelChartDataset, @@ -54,50 +54,35 @@ export const SPREADSHEET_TO_EXCEL_TRENDLINE_TYPE_MAPPING = { */ export function updateChartRangesWithDataSets( getters: CoreGetters, + sheetId: UID, applyChange: ApplyRangeChange, - chartDataSets: DataSet[], + dataSource: ChartRangeDataSource, chartLabelRange?: Range ) { - let isStale = false; - const dataSetsWithUndefined: (DataSet | undefined)[] = []; - for (const index in chartDataSets) { - let ds: DataSet | undefined = chartDataSets[index]!; - if (ds.labelCell) { - const labelCell = adaptChartRange(ds.labelCell, applyChange); - if (ds.labelCell !== labelCell) { - isStale = true; - ds = { - ...ds, - labelCell: labelCell, - }; + const dataSetsWithUndefined = dataSource.dataSets + .map((ds) => { + const dataRange = adaptChartRangeString(getters, sheetId, ds.dataRange, applyChange); + if (dataRange === undefined) { + return undefined; } - } - const dataRange = adaptChartRange(ds.dataRange, applyChange); - if ( - dataRange === undefined || - getters.getRangeString(dataRange, dataRange.sheetId) === CellErrorType.InvalidReference - ) { - isStale = true; - ds = undefined; - } else if (dataRange !== ds.dataRange) { - isStale = true; - ds = { + return { ...ds, dataRange, }; - } - dataSetsWithUndefined[index] = ds; - } + }) + .filter(isDefined); let labelRange = chartLabelRange; const range = adaptChartRange(labelRange, applyChange); if (range !== labelRange) { - isStale = true; labelRange = range; } - const dataSets = dataSetsWithUndefined.filter(isDefined); + const dataSets = dataSetsWithUndefined; return { - isStale, - dataSets, + isStale: true, + dataSource: { + ...dataSource, + dataSets, + }, labelRange, }; } @@ -106,19 +91,23 @@ export function updateChartRangesWithDataSets( * Duplicate the dataSets. All ranges on sheetIdFrom are adapted to target * sheetIdTo. */ -export function duplicateDataSetsInDuplicatedSheet( +export function duplicateDataSourceInDuplicatedSheet( + getters: CoreGetters, sheetIdFrom: UID, sheetIdTo: UID, - dataSets: DataSet[] -): DataSet[] { - return dataSets.map((ds) => { - return { - dataRange: duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, ds.dataRange), - labelCell: ds.labelCell - ? duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, ds.labelCell) - : undefined, - }; - }); + dataSource: ChartRangeDataSource +): ChartRangeDataSource { + return { + ...dataSource, + dataSets: dataSource.dataSets.map((ds) => { + const range = getters.getRangeFromSheetXC(sheetIdFrom, ds.dataRange); + const newRange = duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, range); + return { + ...ds, + dataRange: getters.getRangeString(newRange, sheetIdTo), + }; + }), + }; } /** @@ -133,6 +122,27 @@ export function duplicateLabelRangeInDuplicatedSheet( return range ? duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, range) : undefined; } +/** + * Adapt a single range of a chart + */ +function adaptChartRangeString( + getters: CoreGetters, + defaultSheetId: UID, + rangeStr: string, + applyChange: ApplyRangeChange +): string | undefined { + const range = getters.getRangeFromSheetXC(defaultSheetId, rangeStr); + const adaptedRange = adaptChartRange(range, applyChange); + if (!adaptedRange) { + return undefined; + } + const newRangeStr = getters.getRangeString(adaptedRange, defaultSheetId); + if (newRangeStr === CellErrorType.InvalidReference) { + return undefined; + } + return newRangeStr; +} + /** * Adapt a single range of a chart */ @@ -159,17 +169,17 @@ export function adaptChartRange( */ export function createDataSets( getters: CoreGetters, - customizedDataSets: CustomizedDataSet[], sheetId: UID, - dataSetsHaveTitle: boolean + definition: ChartWithDataSetDefinition ): DataSet[] { const dataSets: DataSet[] = []; - for (const dataSet of customizedDataSets) { + for (const dataSet of definition.dataSource.dataSets) { const dataRange = getters.getRangeFromSheetXC(sheetId, dataSet.dataRange); const { unboundedZone: zone, sheetId: dataSetSheetId, invalidSheetName, invalidXc } = dataRange; if (invalidSheetName || invalidXc) { continue; } + const customizedDataSet = definition.dataSets[dataSet.id] ?? {}; // It's a rectangle. We treat all columns (arbitrary) as different data series. if (zone.left !== zone.right && zone.top !== zone.bottom) { if (zone.right === undefined) { @@ -188,7 +198,7 @@ export function createDataSets( getters, dataSetSheetId, columnZone, - dataSetsHaveTitle + definition.dataSetsHaveTitle ? { top: columnZone.top, bottom: columnZone.top, @@ -197,10 +207,11 @@ export function createDataSets( } : undefined ), - backgroundColor: dataSet.backgroundColor, - rightYAxis: dataSet.yAxisId === "y1", - customLabel: dataSet.label, - trend: dataSet.trend, + dataSetId: dataSet.id, + backgroundColor: customizedDataSet.backgroundColor, + rightYAxis: customizedDataSet.yAxisId === "y1", + customLabel: customizedDataSet.label, + trend: customizedDataSet.trend, }); } } else { @@ -210,7 +221,7 @@ export function createDataSets( getters, dataSetSheetId, zone, - dataSetsHaveTitle + definition.dataSetsHaveTitle ? { top: zone.top, bottom: zone.top, @@ -219,10 +230,11 @@ export function createDataSets( } : undefined ), - backgroundColor: dataSet.backgroundColor, - rightYAxis: dataSet.yAxisId === "y1", - customLabel: dataSet.label, - trend: dataSet.trend, + dataSetId: dataSet.id, + backgroundColor: customizedDataSet.backgroundColor, + rightYAxis: customizedDataSet.yAxisId === "y1", + customLabel: customizedDataSet.label, + trend: customizedDataSet.trend, }); } } @@ -234,7 +246,7 @@ function createDataSet( sheetId: UID, fullZone: Zone | UnboundedZone, titleZone: Zone | UnboundedZone | undefined -): DataSet { +): Pick { if (fullZone.left !== fullZone.right && fullZone.top !== fullZone.bottom) { throw new Error(`Zone should be a single column or row: ${zoneToXc(fullZone)}`); } @@ -336,20 +348,20 @@ export function transformChartDefinitionWithDataSetsWithZone !rangeReference.test(range.dataRange)) !== undefined; + definition.dataSource.dataSets.find((range) => !rangeReference.test(range.dataRange)) !== + undefined; if (invalidRanges) { return CommandResult.InvalidDataSet; } - const zones = definition.dataSets.map((ds) => toUnboundedZone(ds.dataRange)); + const zones = definition.dataSource.dataSets.map((ds) => toUnboundedZone(ds.dataRange)); if (zones.some((zone) => zone.top !== zone.bottom && isFullRow(zone))) { return CommandResult.InvalidDataSet; } @@ -428,7 +441,7 @@ export function getDefinedAxis(definition: GenericDefinition { + const dataSetId = i.toString(); + const dataRange = ds.dataRange; + delete ds.dataRange; + custo[dataSetId] = { ...ds }; + return { dataRange, id: dataSetId }; + }), + }; + definition.dataSets = custo; + return definition; + } + for (const sheet of data.sheets || []) { + for (const figure of sheet.figures || []) { + if (figure.tag === "chart" && "dataSets" in figure.data) { + figure.data = upgrade(figure.data); + } else if (figure.tag === "carousel") { + for (const chartId in figure.data.chartDefinitions) { + const definition = figure.data.chartDefinitions[chartId]; + figure.data.chartDefinitions[chartId] = upgrade(definition); + } + } + } + } + return data; + }, }); function fixOverlappingFilters(data: any): any { diff --git a/packages/o-spreadsheet-engine/src/types/chart/bar_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/bar_chart.ts index 1e06fa9f07..740c2ac1e6 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/bar_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/bar_chart.ts @@ -13,4 +13,5 @@ export type BarChartRuntime = { chartJsConfig: ChartConfiguration<"bar" | "line">; masterChartConfig?: ChartConfiguration<"bar">; background: Color; + customisableSeries: { dataSetId: string; label: string }[]; }; diff --git a/packages/o-spreadsheet-engine/src/types/chart/chart.ts b/packages/o-spreadsheet-engine/src/types/chart/chart.ts index be0c233c7d..84138876fa 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/chart.ts @@ -21,7 +21,7 @@ import { } from "./tree_map_chart"; import { WaterfallChartDefinition, WaterfallChartRuntime } from "./waterfall_chart"; -import { Align, Color, FunctionResultObject, VerticalAlign } from "../.."; +import { Align, Color, FunctionResultObject, UID, VerticalAlign } from "../.."; import { COLORSCHEMES } from "../../helpers/color"; import { Format } from "../format"; import { Locale } from "../locale"; @@ -64,7 +64,7 @@ export type ChartDefinition = export type ChartWithDataSetDefinition = Extract< ChartDefinition, - { dataSets: CustomizedDataSet[]; labelRange?: string; humanize?: boolean } + { dataSets: DataSetStyling; labelRange?: string; humanize?: boolean } >; export type ChartWithColorScaleDefinition = Extract< @@ -97,10 +97,16 @@ export type ChartJSRuntime = export type ChartRuntime = ChartJSRuntime | ScorecardChartRuntime | GaugeChartRuntime; +export type CustomisableSeriesChartRuntime = Extract< + ChartRuntime, + { customisableSeries: { dataSetId: string; label: string }[] } +>; + export type LabelValues = FunctionResultObject[]; export interface DatasetValues { - readonly label?: string; + readonly dataSetId: UID; + readonly label: string; readonly data: FunctionResultObject[]; readonly hidden?: boolean; } @@ -144,16 +150,22 @@ export interface TrendConfiguration { window?: number; } +export type DataSetStyling = Record; + export type CustomizedDataSet = { - readonly dataRange: string; readonly trend?: TrendConfiguration; } & DatasetDesign; +export interface ChartRangeDataSource { + readonly dataSets: { id: UID; dataRange: string }[]; +} + export type AxisType = "category" | "linear" | "time"; export type ChartDatasetOrientation = "rows" | "columns"; export interface DataSet { + readonly dataSetId: UID; readonly labelCell?: Range; // range of the label readonly dataRange: Range; // range of the data readonly rightYAxis?: boolean; // if the dataset should be on the right Y axis @@ -202,8 +214,9 @@ export interface ExcelChartDefinition { } export interface ChartCreationContext { - readonly range?: CustomizedDataSet[]; - readonly hierarchicalRanges?: CustomizedDataSet[]; + readonly dataSets?: DataSetStyling; + readonly hierarchicalDataSource?: ChartRangeDataSource; + readonly dataSource?: ChartRangeDataSource; readonly title?: TitleDesign; readonly background?: Color; readonly auxiliaryRange?: string; @@ -251,12 +264,8 @@ export interface ChartRuntimeGenerationArgs { topPadding?: number; } -/** Generic definition of chart to create a runtime: omit the chart type and the dataRange of the dataSets*/ -export type GenericDefinition = Partial< - Omit -> & { - dataSets: Omit[]; -}; +/** Generic definition of chart to create a runtime: omit the chart type*/ +export type GenericDefinition = Partial>; export interface ChartColorScale { minColor: Color; diff --git a/packages/o-spreadsheet-engine/src/types/chart/combo_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/combo_chart.ts index cd0485854a..e9953a3ffa 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/combo_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/combo_chart.ts @@ -1,19 +1,20 @@ import { ChartConfiguration } from "chart.js"; -import { Color } from "../misc"; +import { Color, UID } from "../misc"; import { CustomizedDataSet } from "./chart"; import { CommonChartDefinition } from "./common_chart"; export interface ComboChartDefinition extends CommonChartDefinition { - readonly dataSets: ComboChartDataSet[]; + readonly dataSets: ComboChartDataSetStyling; readonly type: "combo"; readonly hideDataMarkers?: boolean; readonly zoomable?: boolean; } -export type ComboChartDataSet = CustomizedDataSet & { type?: "bar" | "line" }; +export type ComboChartDataSetStyling = Record; export type ComboChartRuntime = { chartJsConfig: ChartConfiguration; masterChartConfig?: ChartConfiguration; background: Color; + customisableSeries: { dataSetId: string; label: string }[]; }; diff --git a/packages/o-spreadsheet-engine/src/types/chart/common_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/common_chart.ts index a6c5348483..8c03e6257d 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/common_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/common_chart.ts @@ -1,11 +1,12 @@ import { Color } from "../misc"; -import { AxesDesign, CustomizedDataSet, TitleDesign } from "./chart"; +import { AxesDesign, ChartRangeDataSource, DataSetStyling, TitleDesign } from "./chart"; export type VerticalAxisPosition = "left" | "right"; export type LegendPosition = "top" | "bottom" | "left" | "right" | "none"; export interface CommonChartDefinition { - readonly dataSets: CustomizedDataSet[]; + readonly dataSets: DataSetStyling; + readonly dataSource: ChartRangeDataSource; readonly dataSetsHaveTitle: boolean; readonly labelRange?: string; readonly title: TitleDesign; diff --git a/packages/o-spreadsheet-engine/src/types/chart/funnel_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/funnel_chart.ts index de633d3b21..04a62d13db 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/funnel_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/funnel_chart.ts @@ -1,11 +1,12 @@ import { ChartConfiguration } from "chart.js"; import { Color } from "../misc"; -import { AxesDesign, CustomizedDataSet, TitleDesign } from "./chart"; +import { AxesDesign, ChartRangeDataSource, DataSetStyling, TitleDesign } from "./chart"; import { LegendPosition } from "./common_chart"; export interface FunnelChartDefinition { readonly type: "funnel"; - readonly dataSets: CustomizedDataSet[]; + readonly dataSets: DataSetStyling; + readonly dataSource: ChartRangeDataSource; readonly dataSetsHaveTitle: boolean; readonly labelRange?: string; readonly title: TitleDesign; diff --git a/packages/o-spreadsheet-engine/src/types/chart/line_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/line_chart.ts index 6e056932af..d203f502c8 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/line_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/line_chart.ts @@ -17,4 +17,5 @@ export type LineChartRuntime = { chartJsConfig: ChartConfiguration<"line">; masterChartConfig?: ChartConfiguration<"line">; background: Color; + customisableSeries: { dataSetId: string; label: string }[]; }; diff --git a/packages/o-spreadsheet-engine/src/types/chart/pyramid_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/pyramid_chart.ts index ed4a737099..7ab9af5ac0 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/pyramid_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/pyramid_chart.ts @@ -9,4 +9,5 @@ export interface PyramidChartDefinition extends Omit export type PyramidChartRuntime = { chartJsConfig: ChartConfiguration; background: Color; + customisableSeries: { dataSetId: string; label: string }[]; }; diff --git a/packages/o-spreadsheet-engine/src/types/chart/radar_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/radar_chart.ts index d7ffe4c57a..7351ff2e32 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/radar_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/radar_chart.ts @@ -14,4 +14,5 @@ export interface RadarChartDefinition extends CommonChartDefinition { export type RadarChartRuntime = { chartJsConfig: ChartConfiguration; background: Color; + customisableSeries: { dataSetId: string; label: string }[]; }; diff --git a/packages/o-spreadsheet-engine/src/types/chart/sunburst_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/sunburst_chart.ts index 608243dada..f2a0004b92 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/sunburst_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/sunburst_chart.ts @@ -1,11 +1,12 @@ import type { ChartConfiguration, ChartDataset } from "chart.js"; import { Color } from "../misc"; -import { ChartStyle, CustomizedDataSet, TitleDesign } from "./chart"; +import { ChartRangeDataSource, ChartStyle, DataSetStyling, TitleDesign } from "./chart"; import { LegendPosition } from "./common_chart"; export interface SunburstChartDefinition { readonly type: "sunburst"; - readonly dataSets: CustomizedDataSet[]; + readonly dataSets: DataSetStyling; + readonly dataSource: ChartRangeDataSource; readonly dataSetsHaveTitle: boolean; readonly labelRange?: string; readonly title: TitleDesign; diff --git a/packages/o-spreadsheet-engine/src/types/chart/tree_map_chart.ts b/packages/o-spreadsheet-engine/src/types/chart/tree_map_chart.ts index e014fedadf..501088aa2e 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/tree_map_chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/tree_map_chart.ts @@ -1,12 +1,13 @@ import { ChartConfiguration } from "chart.js"; import { Color } from "../misc"; -import { CustomizedDataSet, TitleDesign } from "./chart"; +import { ChartRangeDataSource, DataSetStyling, TitleDesign } from "./chart"; import { TreemapDataPoint } from "./chartjs_tree_map_type"; import { LegendPosition } from "./common_chart"; export interface TreeMapChartDefinition { readonly type: "treemap"; - readonly dataSets: CustomizedDataSet[]; + readonly dataSets: DataSetStyling; + readonly dataSource: ChartRangeDataSource; readonly dataSetsHaveTitle: boolean; readonly labelRange?: string; readonly title: TitleDesign; diff --git a/packages/o-spreadsheet-engine/src/xlsx/conversion/figure_conversion.ts b/packages/o-spreadsheet-engine/src/xlsx/conversion/figure_conversion.ts index b6bee59079..b240dd191c 100644 --- a/packages/o-spreadsheet-engine/src/xlsx/conversion/figure_conversion.ts +++ b/packages/o-spreadsheet-engine/src/xlsx/conversion/figure_conversion.ts @@ -6,6 +6,7 @@ import { chartRegistry } from "../../registries/chart_registry"; import { ChartCreationContext, ChartDefinition, + DataSetStyling, ExcelChartDefinition, ExcelChartTrendConfiguration, TrendConfiguration, @@ -82,27 +83,33 @@ function isImageData(data: ExcelChartDefinition | ExcelImage): data is ExcelImag function convertChartData(chartData: ExcelChartDefinition): ChartDefinition | undefined { const dataSetsHaveTitle = chartData.dataSets.some((ds) => "reference" in (ds.label ?? {})); + const dataSetsStyling: DataSetStyling = {}; const labelRange = chartData.labelRange ? convertExcelRangeToSheetXC(chartData.labelRange, dataSetsHaveTitle) : undefined; - const dataSets = chartData.dataSets.map((data) => { + const dataSets = chartData.dataSets.map((data, i) => { let label: string | undefined = undefined; if (data.label && "text" in data.label) { label = data.label.text; } - return { - dataRange: convertExcelRangeToSheetXC(data.range, dataSetsHaveTitle), + const dataSetId = i.toString(); + dataSetsStyling[dataSetId] = { label, backgroundColor: data.backgroundColor, trend: convertExcelTrendline(data.trend), }; + return { + id: dataSetId, + dataRange: convertExcelRangeToSheetXC(data.range, dataSetsHaveTitle), + }; }); // For doughnut charts, in chartJS first dataset = outer dataset, in excel first dataset = inner dataset if (chartData.type === "pie") { dataSets.reverse(); } const creationContext: ChartCreationContext = { - range: dataSets, + dataSource: { dataSets }, + dataSets: dataSetsStyling, dataSetsHaveTitle, auxiliaryRange: labelRange, title: chartData.title ?? { text: "" }, diff --git a/src/components/side_panel/chart/building_blocks/data_series/data_series.ts b/src/components/side_panel/chart/building_blocks/data_series/data_series.ts index 1cf6d675aa..e074cdfb3b 100644 --- a/src/components/side_panel/chart/building_blocks/data_series/data_series.ts +++ b/src/components/side_panel/chart/building_blocks/data_series/data_series.ts @@ -1,12 +1,13 @@ import { _t } from "@odoo/o-spreadsheet-engine/translation"; import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; import { Component } from "@odoo/owl"; -import { ChartDatasetOrientation, Color, CustomizedDataSet } from "../../../../../types"; +import { ChartDatasetOrientation, Color, DataSetStyling, UID } from "../../../../../types"; import { SelectionInput } from "../../../../selection_input/selection_input"; import { Section } from "../../../components/section/section"; interface Props { - ranges: CustomizedDataSet[]; + ranges: { dataRange: string; dataSetId: UID }[]; + colors: DataSetStyling; hasSingleRange?: boolean; onSelectionChanged: (ranges: string[]) => void; onSelectionReordered?: (indexes: number[]) => void; @@ -47,7 +48,7 @@ export class ChartDataSeries extends Component { } get colors(): (Color | undefined)[] { - return this.props.ranges.map((r) => r.backgroundColor); + return this.props.ranges.map((r) => this.props.colors?.[r.dataSetId]?.backgroundColor); } get title() { diff --git a/src/components/side_panel/chart/building_blocks/generic_side_panel/config_panel.ts b/src/components/side_panel/chart/building_blocks/generic_side_panel/config_panel.ts index eb1fc45fe1..942d658f51 100644 --- a/src/components/side_panel/chart/building_blocks/generic_side_panel/config_panel.ts +++ b/src/components/side_panel/chart/building_blocks/generic_side_panel/config_panel.ts @@ -18,10 +18,12 @@ import { createDataSets } from "../../../../../helpers/figures/charts"; import { getChartColorsGenerator } from "../../../../../helpers/figures/charts/runtime"; import { ChartDatasetOrientation, + ChartRangeDataSource, ChartWithDataSetDefinition, CommandResult, - CustomizedDataSet, + DataSetStyling, DispatchResult, + UID, Zone, } from "../../../../../types"; import { Checkbox } from "../../../components/checkbox/checkbox"; @@ -54,14 +56,14 @@ export class GenericChartConfigPanel< labelsDispatchResult: undefined, }); - protected dataSets: CustomizedDataSet[] = []; + protected dataSets: ChartRangeDataSource["dataSets"] = []; private labelRange: string | undefined; private datasetOrientation: ChartDatasetOrientation | undefined = undefined; protected chartTerms = ChartTerms; setup() { - this.dataSets = this.props.definition.dataSets; + this.dataSets = this.props.definition.dataSource.dataSets; this.labelRange = this.props.definition.labelRange; this.datasetOrientation = this.computeDatasetOrientation(); } @@ -185,12 +187,13 @@ export class GenericChartConfigPanel< } setDatasetOrientation(datasetOrientation: ChartDatasetOrientation) { - const oldDataSets = this.props.definition.dataSets; + const oldDataSets = this.props.definition.dataSource.dataSets; const dataRanges = oldDataSets.map((d) => d.dataRange); const dataSets = this.transposeDataSet( [this.props.definition.labelRange, ...dataRanges], datasetOrientation ); + // TODO: kill design if (dataSets.length === 0) { return; } @@ -198,7 +201,7 @@ export class GenericChartConfigPanel< this.props.updateChart(this.props.chartId, { labelRange, - dataSets, + dataSource: { dataSets }, }); this.dataSets = dataSets; this.labelRange = labelRange; @@ -215,29 +218,35 @@ export class GenericChartConfigPanel< dataRange, })); this.state.datasetDispatchResult = this.props.canUpdateChart(this.props.chartId, { - dataSets: this.dataSets, + dataSource: { dataSets: this.dataSets }, }); } onDataSeriesReordered(indexes: number[]) { const colorGenerator = getChartColorsGenerator( - { dataSets: this.dataSets }, + { dataSets: this.props.definition.dataSets, dataSource: { dataSets: this.dataSets } }, this.dataSets.length ); this.datasetOrientation = undefined; - const colors = this.dataSets.map((ds) => colorGenerator.next()); - this.dataSets = indexes.map((i) => ({ - backgroundColor: colors[i], - ...this.dataSets[i], - })); + const design = this.props.definition.dataSets; + for (const ds of this.dataSets) { + const color = colorGenerator.next(); + design[ds.id] = { backgroundColor: color, ...design[ds.id] }; + } + // const colors = this.dataSets.map((ds) => colorGenerator.next()); + // this.dataSets = indexes.map((i) => ({ + // backgroundColor: colors[i], + // ...this.dataSets[i], + // })); this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, { - dataSets: this.dataSets, + dataSource: { dataSets: this.dataSets }, + dataSets: design, }); } onDataSeriesRemoved(index: number) { const colorGenerator = getChartColorsGenerator( - { dataSets: this.dataSets }, + { dataSets: this.props.definition.dataSets, dataSource: { dataSets: this.dataSets } }, this.dataSets.length ); const colors = this.dataSets.map((ds) => colorGenerator.next()); @@ -248,25 +257,29 @@ export class GenericChartConfigPanel< })) .filter((_, i) => i !== index); this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, { - dataSets: this.dataSets, + dataSource: { dataSets: this.dataSets }, }); } onDataSeriesConfirmed() { - this.dataSets = this.splitRanges; + const { dataSets, design } = this.splitRanges(); + this.dataSets = dataSets; this.datasetOrientation = this.computeDatasetOrientation(); this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, { - dataSets: this.dataSets, + dataSource: { dataSets: this.dataSets }, + dataSets: design, }); if (this.state.datasetDispatchResult.isSuccessful) { this.dataSets = ( this.env.model.getters.getChartDefinition(this.props.chartId) as ChartWithDataSetDefinition - ).dataSets; + ).dataSource.dataSets; } } - get splitRanges(): CustomizedDataSet[] { - const postProcessedRanges: CustomizedDataSet[] = []; + splitRanges() { + const postProcessedRanges: ChartRangeDataSource["dataSets"] = []; + const postProcessedDesign: DataSetStyling = {}; + const design = this.props.definition.dataSets; for (const dataSet of this.dataSets) { const range = dataSet.dataRange; if (!this.env.model.getters.isRangeValid(range)) { @@ -281,9 +294,11 @@ export class GenericChartConfigPanel< if (this.datasetOrientation !== "rows") { if (zone.right !== undefined) { for (let j = zone.left; j <= zone.right; ++j) { - const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId }; + const datasetOptions = + j === zone.left ? design[dataSet.id] : { yAxisId: design[dataSet.id].yAxisId }; + postProcessedDesign[dataSet.id] = datasetOptions; postProcessedRanges.push({ - ...datasetOptions, + ...dataSet, dataRange: `${sheetPrefix}${zoneToXc({ left: j, right: j, @@ -294,9 +309,11 @@ export class GenericChartConfigPanel< } } else if (zone.bottom !== undefined) { for (let j = zone.top; j <= zone.bottom; ++j) { - const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId }; + const datasetOptions = + j === zone.top ? design[dataSet.id] : { yAxisId: design[dataSet.id].yAxisId }; + postProcessedDesign[dataSet.id] = datasetOptions; postProcessedRanges.push({ - ...datasetOptions, + ...dataSet, dataRange: `${sheetPrefix}${zoneToXc({ left: zone.left, right: zone.right, @@ -309,9 +326,11 @@ export class GenericChartConfigPanel< } else { if (zone.bottom !== undefined) { for (let j = zone.top; j <= zone.bottom; ++j) { - const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId }; + const datasetOptions = + j === zone.top ? design[dataSet.id] : { yAxisId: design[dataSet.id].yAxisId }; + postProcessedDesign[dataSet.id] = datasetOptions; postProcessedRanges.push({ - ...datasetOptions, + ...dataSet, dataRange: `${sheetPrefix}${zoneToXc({ left: zone.left, right: zone.right, @@ -322,9 +341,11 @@ export class GenericChartConfigPanel< } } else if (zone.right !== undefined) { for (let j = zone.left; j <= zone.right; ++j) { - const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId }; + const datasetOptions = + j === zone.left ? design[dataSet.id] : { yAxisId: design[dataSet.id].yAxisId }; + postProcessedDesign[dataSet.id] = datasetOptions; postProcessedRanges.push({ - ...datasetOptions, + ...dataSet, dataRange: `${sheetPrefix}${zoneToXc({ left: j, right: j, @@ -337,9 +358,13 @@ export class GenericChartConfigPanel< } } else { postProcessedRanges.push(dataSet); + postProcessedDesign[dataSet.id] = design[dataSet.id]; } } - return postProcessedRanges; + return { + dataSets: postProcessedRanges, + design: postProcessedDesign, + }; } getDataSeriesRanges() { @@ -380,11 +405,16 @@ export class GenericChartConfigPanel< const getters = this.env.model.getters; const sheetId = getters.getActiveSheetId(); const labelRange = createValidRange(getters, sheetId, this.labelRange); + // const dataSets = createDataSets( + // getters, + // this.dataSets, + // sheetId, + // this.props.definition.dataSetsHaveTitle + // ); const dataSets = createDataSets( getters, - this.dataSets, - sheetId, - this.props.definition.dataSetsHaveTitle + sheetId, // TODO check: this was using this.dataSets before + this.props.definition ); if (dataSets.length) { return this.datasetOrientation === "rows" @@ -403,13 +433,14 @@ export class GenericChartConfigPanel< private transposeDataSet( dataRanges: (string | undefined)[], datasetOrientation: ChartDatasetOrientation | undefined - ): { dataRange: string }[] { + ): { dataRange: string; id: UID }[] { const getters = this.env.model.getters; + const smallUuid = this.env.model.uuidGenerator.smallUuid; if (datasetOrientation === undefined) { - return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange })); + return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange, id: smallUuid() })); } const zonesBySheetName = {}; - const transposedDatasets: { dataRange: string }[] = []; + const transposedDatasets: ChartRangeDataSource["dataSets"] = []; const figureId = getters.getFigureIdFromChartId(this.props.chartId); const figureSheetId = getters.getFigureSheetId(figureId); let name = getters.getActiveSheet().name; @@ -421,7 +452,7 @@ export class GenericChartConfigPanel< continue; } if (!isXcRepresentation(dataRange)) { - return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange })); + return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange, id: smallUuid() })); } let { sheetName, xc } = splitReference(dataRange); sheetName = sheetName ?? name; @@ -441,7 +472,7 @@ export class GenericChartConfigPanel< left: col, right: col, })}`; - transposedDatasets.push({ dataRange }); + transposedDatasets.push({ dataRange, id: smallUuid() }); } } } else { @@ -452,7 +483,7 @@ export class GenericChartConfigPanel< top: row, bottom: row, })}`; - transposedDatasets.push({ dataRange }); + transposedDatasets.push({ dataRange, id: smallUuid() }); } } } diff --git a/src/components/side_panel/chart/building_blocks/series_design/series_design_editor.ts b/src/components/side_panel/chart/building_blocks/series_design/series_design_editor.ts index cb83f9931e..eb3c464ec4 100644 --- a/src/components/side_panel/chart/building_blocks/series_design/series_design_editor.ts +++ b/src/components/side_panel/chart/building_blocks/series_design/series_design_editor.ts @@ -1,8 +1,7 @@ import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; import { Component, useState } from "@odoo/owl"; import { getColorsPalette, getNthColor, toHex } from "../../../../../helpers"; -import { isTrendLineAxis } from "../../../../../helpers/figures/charts"; -import { ChartWithDataSetDefinition } from "../../../../../types"; +import { ChartWithDataSetDefinition, CustomisableSeriesChartRuntime } from "../../../../../types"; import { SidePanelCollapsible } from "../../../components/collapsible/side_panel_collapsible"; import { RoundColorPicker } from "../../../components/round_color_picker/round_color_picker"; import { Section } from "../../../components/section/section"; @@ -24,27 +23,30 @@ export class SeriesDesignEditor extends Component { slots: { type: Object, optional: true }, }; - protected state = useState({ index: 0 }); + protected state = useState({ dataSetId: this.getDataSeries()[0]?.dataSetId || "" }); - getDataSeries() { + private getRuntime(): CustomisableSeriesChartRuntime { const runtime = this.env.model.getters.getChartRuntime(this.props.chartId); - if (!runtime || !("chartJsConfig" in runtime)) { - return []; + if (!runtime || !("customisableSeries" in runtime)) { + throw new Error( + "SeriesDesignEditor: chart runtime is not compatible with series customization." + ); } - return runtime.chartJsConfig.data.datasets - .filter((d) => !isTrendLineAxis(d["xAxisID"] ?? "")) - .map((d) => d.label); + return runtime as CustomisableSeriesChartRuntime; + } + + getDataSeries() { + return this.getRuntime().customisableSeries; } updateEditedSeries(ev: Event) { - this.state.index = (ev.target as HTMLSelectElement).selectedIndex; + this.state.dataSetId = (ev.target as HTMLSelectElement).value; } updateDataSeriesColor(color: string) { - const dataSets = this.props.definition.dataSets; - if (!dataSets?.[this.state.index]) return; - dataSets[this.state.index] = { - ...dataSets[this.state.index], + const dataSets = { ...this.props.definition.dataSets }; + dataSets[this.state.dataSetId] = { + ...dataSets[this.state.dataSetId], backgroundColor: color, }; this.props.updateChart(this.props.chartId, { dataSets }); @@ -52,26 +54,28 @@ export class SeriesDesignEditor extends Component { getDataSeriesColor() { const dataSets = this.props.definition.dataSets; - if (!dataSets?.[this.state.index]) return ""; - const color = dataSets[this.state.index].backgroundColor; - return color - ? toHex(color) - : getNthColor(this.state.index, getColorsPalette(this.props.definition.dataSets.length)); + const color = dataSets[this.state.dataSetId]?.backgroundColor; + const dataSeries = this.getDataSeries(); + const index = dataSeries.findIndex((series) => series.dataSetId === this.state.dataSetId); + return color ? toHex(color) : getNthColor(index, getColorsPalette(dataSeries.length)); } updateDataSeriesLabel(ev: Event) { const label = (ev.target as HTMLInputElement).value; - const dataSets = this.props.definition.dataSets; - if (!dataSets?.[this.state.index]) return; - dataSets[this.state.index] = { - ...dataSets[this.state.index], + const dataSets = { ...this.props.definition.dataSets }; + dataSets[this.state.dataSetId] = { + ...dataSets[this.state.dataSetId], label, }; this.props.updateChart(this.props.chartId, { dataSets }); } - getDataSeriesLabel() { + getDataSeriesLabel(): string { const dataSets = this.props.definition.dataSets; - return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index]; + return ( + dataSets[this.state.dataSetId]?.label || + this.getDataSeries().find((series) => series.dataSetId === this.state.dataSetId)?.label || + "" + ); } } diff --git a/src/components/side_panel/chart/building_blocks/series_design/series_design_editor.xml b/src/components/side_panel/chart/building_blocks/series_design/series_design_editor.xml index 5da018cb7a..180f76eeaf 100644 --- a/src/components/side_panel/chart/building_blocks/series_design/series_design_editor.xml +++ b/src/components/side_panel/chart/building_blocks/series_design/series_design_editor.xml @@ -7,11 +7,11 @@ class="o-input data-series-selector" t-model="state.label" t-on-change="(ev) => this.updateEditedSeries(ev)"> - + @@ -33,7 +33,7 @@ /> - + diff --git a/src/components/side_panel/chart/building_blocks/series_design/series_with_axis_design_editor.ts b/src/components/side_panel/chart/building_blocks/series_design/series_with_axis_design_editor.ts index e8a9238db6..a8b0a874fe 100644 --- a/src/components/side_panel/chart/building_blocks/series_design/series_with_axis_design_editor.ts +++ b/src/components/side_panel/chart/building_blocks/series_design/series_with_axis_design_editor.ts @@ -4,10 +4,11 @@ import { Component } from "@odoo/owl"; import { getColorsPalette, getNthColor, range, setColorAlpha, toHex } from "../../../../../helpers"; import { CHART_AXIS_CHOICES } from "../../../../../helpers/figures/charts"; import { - ChartJSRuntime, ChartWithDataSetDefinition, Color, + CustomisableSeriesChartRuntime, TrendConfiguration, + UID, } from "../../../../../types"; import { NumberInput } from "../../../../number_input/number_input"; import { Checkbox } from "../../../components/checkbox/checkbox"; @@ -38,50 +39,50 @@ export class SeriesWithAxisDesignEditor extends Component series.dataSetId === dataSetId); return Math.min(10, runtime.chartJsConfig.data.datasets[index].data.length - 1); } @@ -131,43 +135,47 @@ export class SeriesWithAxisDesignEditor extends Component series.dataSetId === dataSetId); return color ? toHex(color) - : getNthColor(index, getColorsPalette(this.props.definition.dataSets.length)); + : getNthColor(index, getColorsPalette(runtime.customisableSeries.length)); } - getTrendLineColor(index: number) { + getTrendLineColor(dataSetId: UID) { return ( - this.getTrendLineConfiguration(index)?.color ?? - setColorAlpha(this.getDataSeriesColor(index), 0.5) + this.getTrendLineConfiguration(dataSetId)?.color ?? + setColorAlpha(this.getDataSeriesColor(dataSetId), 0.5) ); } - updateTrendLineColor(index: number, color: Color) { - this.updateTrendLineValue(index, { color }); + updateTrendLineColor(dataSetId: UID, color: Color) { + this.updateTrendLineValue(dataSetId, { color }); } - updateTrendLineValue(index: number, config: any) { - const dataSets = [...this.props.definition.dataSets]; - if (!dataSets?.[index]) { + private updateTrendLineValue(dataSetId: UID, config: TrendConfiguration) { + const dataSets = { ...this.props.definition.dataSets }; + if (!dataSets?.[dataSetId]) { return; } - dataSets[index] = { - ...dataSets[index], + dataSets[dataSetId] = { + ...dataSets[dataSetId], trend: { - ...dataSets[index].trend, + ...dataSets[dataSetId].trend, ...config, }, }; diff --git a/src/components/side_panel/chart/building_blocks/series_design/series_with_axis_design_editor.xml b/src/components/side_panel/chart/building_blocks/series_design/series_with_axis_design_editor.xml index 76a5d4a31d..bfde1bdd99 100644 --- a/src/components/side_panel/chart/building_blocks/series_design/series_with_axis_design_editor.xml +++ b/src/components/side_panel/chart/building_blocks/series_design/series_with_axis_design_editor.xml @@ -2,8 +2,8 @@ - - + +
Show trend line - +
@@ -35,7 +35,7 @@ Type - + t-on-change="(ev) => this.onChangePolynomialDegree(dataSetId, ev)"> + @@ -79,8 +79,8 @@
Trend line color
diff --git a/src/components/side_panel/chart/combo_chart/combo_chart_design_panel.ts b/src/components/side_panel/chart/combo_chart/combo_chart_design_panel.ts index 995bb59c84..22e2c6eaed 100644 --- a/src/components/side_panel/chart/combo_chart/combo_chart_design_panel.ts +++ b/src/components/side_panel/chart/combo_chart/combo_chart_design_panel.ts @@ -1,3 +1,4 @@ +import { UID } from "@odoo/o-spreadsheet-engine"; import { _t } from "@odoo/o-spreadsheet-engine/translation"; import { ComboChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart/combo_chart"; import { RadioSelection } from "../../components/radio_selection/radio_selection"; @@ -19,23 +20,23 @@ export class ComboChartDesignPanel extends GenericZoomableChartDesignPanel< { value: "line", label: _t("Line") }, ]; - updateDataSeriesType(index: number, type: "bar" | "line") { - const dataSets = [...this.props.definition.dataSets]; - if (!dataSets?.[index]) { + updateDataSeriesType(dataSetId: UID, type: "bar" | "line") { + const dataSets = { ...this.props.definition.dataSets }; + if (!dataSets?.[dataSetId]) { return; } - dataSets[index] = { - ...dataSets[index], + dataSets[dataSetId] = { + ...dataSets[dataSetId], type, }; this.props.updateChart(this.props.chartId, { dataSets }); } - getDataSeriesType(index: number) { + getDataSeriesType(dataSetId: UID) { const dataSets = this.props.definition.dataSets as ComboChartDefinition["dataSets"]; - if (!dataSets?.[index]) { + if (!dataSets?.[dataSetId]) { return "bar"; } - return dataSets[index].type ?? "line"; + return dataSets[dataSetId].type ?? "line"; } } diff --git a/src/components/side_panel/chart/combo_chart/combo_chart_design_panel.xml b/src/components/side_panel/chart/combo_chart/combo_chart_design_panel.xml index 07b2452ed3..2c585b280d 100644 --- a/src/components/side_panel/chart/combo_chart/combo_chart_design_panel.xml +++ b/src/components/side_panel/chart/combo_chart/combo_chart_design_panel.xml @@ -23,13 +23,13 @@ - +
diff --git a/src/components/side_panel/chart/gauge_chart_panel/gauge_chart_config_panel.ts b/src/components/side_panel/chart/gauge_chart_panel/gauge_chart_config_panel.ts index 557a990a4a..78e7a48b1e 100644 --- a/src/components/side_panel/chart/gauge_chart_panel/gauge_chart_config_panel.ts +++ b/src/components/side_panel/chart/gauge_chart_panel/gauge_chart_config_panel.ts @@ -2,7 +2,7 @@ import { ChartTerms } from "@odoo/o-spreadsheet-engine/components/translations_t import { GaugeChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart/gauge_chart"; import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; import { Component, useState } from "@odoo/owl"; -import { CommandResult, CustomizedDataSet, DispatchResult } from "../../../../types/index"; +import { CommandResult, DispatchResult } from "../../../../types/index"; import { ChartDataSeries } from "../building_blocks/data_series/data_series"; import { ChartErrorSection } from "../building_blocks/error_section/error_section"; import { ChartSidePanelProps, ChartSidePanelPropsObject } from "../common"; @@ -51,7 +51,7 @@ export class GaugeChartConfigPanel extends Component< }); } - getDataRange(): CustomizedDataSet { + getDataRange() { return { dataRange: this.dataRange || "" }; } } diff --git a/src/components/side_panel/chart/geo_chart_panel/geo_chart_config_panel.ts b/src/components/side_panel/chart/geo_chart_panel/geo_chart_config_panel.ts index 040bfd7be3..b54b1ea2cf 100644 --- a/src/components/side_panel/chart/geo_chart_panel/geo_chart_config_panel.ts +++ b/src/components/side_panel/chart/geo_chart_panel/geo_chart_config_panel.ts @@ -12,9 +12,9 @@ export class GeoChartConfigPanel extends GenericChartConfigPanel { return this.getDataSeriesRanges(); } - get disabledRanges() { - return this.props.definition.dataSets.map((ds, i) => i > 0); - } + // get disabledRanges() { + // return this.props.definition.dataSets.map((ds, i) => i > 0); + // } getLabelRangeOptions() { return [ diff --git a/src/components/side_panel/chart/main_chart_panel/main_chart_panel_store.ts b/src/components/side_panel/chart/main_chart_panel/main_chart_panel_store.ts index 3b7758fb6a..bbce0c3601 100644 --- a/src/components/side_panel/chart/main_chart_panel/main_chart_panel_store.ts +++ b/src/components/side_panel/chart/main_chart_panel/main_chart_panel_store.ts @@ -17,15 +17,25 @@ export class MainChartPanelStore extends SpreadsheetStore { const currentCreationContext = this.getters.getContextCreationChart(chartId); const savedCreationContext = this.creationContexts[chartId] || {}; - let newRanges = currentCreationContext?.range; - if (newRanges?.every((range, i) => deepEquals(range, savedCreationContext.range?.[i]))) { - newRanges = Object.assign([], savedCreationContext.range, currentCreationContext?.range); + let newRanges = currentCreationContext?.dataSource?.dataSets; + // TODO FIXME: we probably shouldn't compare ids. + if ( + newRanges?.every((range, i) => + deepEquals(range, savedCreationContext.dataSource?.dataSets?.[i]) + ) + ) { + newRanges = Object.assign( + [], + savedCreationContext.dataSource?.dataSets, + currentCreationContext?.dataSource?.dataSets + ); } this.creationContexts[chartId] = { ...savedCreationContext, ...currentCreationContext, - range: newRanges, + dataSource: { dataSets: newRanges ?? [] }, + // dataSets: newRanges, }; const figureId = this.getters.getFigureIdFromChartId(chartId); const sheetId = this.getters.getFigureSheetId(figureId); diff --git a/src/helpers/figures/charts/bar_chart.ts b/src/helpers/figures/charts/bar_chart.ts index 2bd1fc2d00..e040dd01fd 100644 --- a/src/helpers/figures/charts/bar_chart.ts +++ b/src/helpers/figures/charts/bar_chart.ts @@ -6,7 +6,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, transformChartDefinitionWithDataSetsWithZone, @@ -21,7 +21,7 @@ import { import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -49,6 +49,7 @@ export class BarChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof BarChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -63,12 +64,7 @@ export class BarChart extends AbstractChart { constructor(private definition: BarChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -90,7 +86,8 @@ export class BarChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): BarChartDefinition { return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, stacked: context.stacked ?? false, aggregated: context.aggregated ?? false, @@ -110,25 +107,29 @@ export class BarChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } duplicateInDuplicatedSheet(newSheetId: UID): BarChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new BarChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): BarChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -136,25 +137,17 @@ export class BarChart extends AbstractChart { } getDefinition(): BarChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): BarChartDefinition { - const ranges: CustomizedDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - }); - } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -179,16 +172,17 @@ export class BarChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): BarChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new BarChart(definition, this.sheetId, this.getters); } } @@ -221,5 +215,12 @@ export function createBarChartRuntime( }, }; - return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; + return { + chartJsConfig: config, + background: definition.background || BACKGROUND_CHART_COLOR, + customisableSeries: chartData.dataSetsValues.map(({ label, dataSetId }) => ({ + dataSetId, + label, + })), + }; } diff --git a/src/helpers/figures/charts/calendar_chart.ts b/src/helpers/figures/charts/calendar_chart.ts index 9ffc14e718..a5714ee8be 100644 --- a/src/helpers/figures/charts/calendar_chart.ts +++ b/src/helpers/figures/charts/calendar_chart.ts @@ -5,7 +5,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, @@ -13,16 +13,16 @@ import { import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { - BarChartRuntime, ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, ExcelChartDefinition, LegendPosition, } from "@odoo/o-spreadsheet-engine/types/chart"; import { CALENDAR_CHART_GRANULARITIES, CalendarChartDefinition, + CalendarChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/calendar_chart"; import type { ChartConfiguration } from "chart.js"; import { @@ -62,6 +62,7 @@ export class CalendarChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof CalendarChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "dataSets", "labelRange", "dataSetsHaveTitle", @@ -76,12 +77,7 @@ export class CalendarChart extends AbstractChart { constructor(private definition: CalendarChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -112,7 +108,8 @@ export class CalendarChart extends AbstractChart { } return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, title: context.title || { text: "" }, type: "calendar", @@ -129,25 +126,32 @@ export class CalendarChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: [definition.dataSets[0]], + dataSource: { + dataSets: [definition.dataSource.dataSets[0]], + }, auxiliaryRange: definition.labelRange, }; } duplicateInDuplicatedSheet(newSheetId: UID): CalendarChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new CalendarChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): CalendarChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -155,21 +159,17 @@ export class CalendarChart extends AbstractChart { } getDefinition(): CalendarChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): CalendarChartDefinition { - const ranges: CustomizedDataSet[] = dataSets.map((dataSet) => ({ - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - })); return { ...this.definition, - dataSets: ranges, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -181,16 +181,17 @@ export class CalendarChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): CalendarChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new CalendarChart(definition, this.sheetId, this.getters); } } @@ -199,7 +200,7 @@ export function createCalendarChartRuntime( getters: Getters, chart: CalendarChart, data: ChartData -): BarChartRuntime { +): CalendarChartRuntime { const definition = chart.getDefinition(); const chartData = getCalendarChartData(definition, data, getters); const { labels, datasets } = getCalendarChartDatasetAndLabels(definition, chartData); diff --git a/src/helpers/figures/charts/combo_chart.ts b/src/helpers/figures/charts/combo_chart.ts index c53537beaa..2f5e62e626 100644 --- a/src/helpers/figures/charts/combo_chart.ts +++ b/src/helpers/figures/charts/combo_chart.ts @@ -6,7 +6,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, transformChartDefinitionWithDataSetsWithZone, @@ -15,7 +15,7 @@ import { import { CHART_COMMON_OPTIONS } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_ui_common"; import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { - ComboChartDataSet, + ComboChartDataSetStyling, ComboChartDefinition, ComboChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/combo_chart"; @@ -25,6 +25,7 @@ import { ApplyRangeChange, ChartCreationContext, ChartData, + ChartRangeDataSource, CommandResult, DataSet, ExcelChartDefinition, @@ -51,6 +52,7 @@ export class ComboChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof ComboChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -64,12 +66,7 @@ export class ComboChart extends AbstractChart { constructor(private definition: ComboChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -92,32 +89,31 @@ export class ComboChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } getDefinition(): ComboChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } - getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + private getDefinitionWithSpecificDataSets( + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): ComboChartDefinition { - const ranges: ComboChartDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - type: this.definition.dataSets?.[i]?.type ?? (i ? "line" : "bar"), - }); - } + // TODO bar vs line + // const ranges: ComboChartDataSet[] = []; + // for (const [i, dataSet] of dataSets.entries()) { + // ranges.push({ + // ...this.definition.dataSets?.[i], + // dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), + // type: this.definition.dataSets?.[i]?.type ?? (i ? "line" : "bar"), + // }); + // } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -141,27 +137,33 @@ export class ComboChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): ComboChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new ComboChart(definition, this.sheetId, this.getters); } static getDefinitionFromContextCreation(context: ChartCreationContext): ComboChartDefinition { - const dataSets: ComboChartDataSet[] = (context.range ?? []).map((ds, index) => ({ - ...ds, - type: index ? "line" : "bar", - })); + const dataSetsStyles: ComboChartDataSetStyling = {}; + const firstDataSetId = context.dataSource?.dataSets[0]?.id; + for (const dataSet of context.dataSource?.dataSets || []) { + dataSetsStyles[dataSet.id] = { + ...(context.dataSets?.[dataSet.id] || {}), + type: dataSet.id === firstDataSetId ? "bar" : "line", + }; + } return { background: context.background, - dataSets, + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: dataSetsStyles, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, aggregated: context.aggregated, legendPosition: context.legendPosition ?? "top", @@ -177,19 +179,24 @@ export class ComboChart extends AbstractChart { } duplicateInDuplicatedSheet(newSheetId: UID): ComboChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new ComboChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): ComboChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -224,5 +231,12 @@ export function createComboChartRuntime( }, }; - return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; + return { + chartJsConfig: config, + background: definition.background || BACKGROUND_CHART_COLOR, + customisableSeries: chartData.dataSetsValues.map(({ dataSetId, label }) => ({ + dataSetId, + label, + })), + }; } diff --git a/src/helpers/figures/charts/funnel_chart.ts b/src/helpers/figures/charts/funnel_chart.ts index 26f1bc664f..b61c6ad3ae 100644 --- a/src/helpers/figures/charts/funnel_chart.ts +++ b/src/helpers/figures/charts/funnel_chart.ts @@ -5,7 +5,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, @@ -16,7 +16,7 @@ import { FunnelChartDefinition, FunnelChartRuntime } from "@odoo/o-spreadsheet-e import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -39,6 +39,7 @@ export class FunnelChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof FunnelChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "dataSets", "dataSetsHaveTitle", "labelRange", @@ -53,12 +54,7 @@ export class FunnelChart extends AbstractChart { constructor(private definition: FunnelChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -80,7 +76,8 @@ export class FunnelChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): FunnelChartDefinition { return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, aggregated: context.aggregated ?? false, legendPosition: "none", @@ -100,25 +97,29 @@ export class FunnelChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } duplicateInDuplicatedSheet(newSheetId: UID): FunnelChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new FunnelChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): FunnelChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -126,25 +127,17 @@ export class FunnelChart extends AbstractChart { } getDefinition(): FunnelChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): FunnelChartDefinition { - const ranges: CustomizedDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - }); - } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -156,16 +149,17 @@ export class FunnelChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): FunnelChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new FunnelChart(definition, this.sheetId, this.getters); } } diff --git a/src/helpers/figures/charts/gauge_chart.ts b/src/helpers/figures/charts/gauge_chart.ts index 2c60229f3d..ad3aa847fd 100644 --- a/src/helpers/figures/charts/gauge_chart.ts +++ b/src/helpers/figures/charts/gauge_chart.ts @@ -196,7 +196,7 @@ export class GaugeChart extends AbstractChart { background: context.background, title: context.title || { text: "" }, type: "gauge", - dataRange: context.range?.[0]?.dataRange, + dataRange: context.dataSource?.dataSets?.[0]?.dataRange, sectionRule: { colors: { lowerColor: DEFAULT_GAUGE_LOWER_COLOR, @@ -275,7 +275,9 @@ export class GaugeChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataRange ? [{ dataRange: definition.dataRange }] : undefined, + dataSource: { + dataSets: definition.dataRange ? [{ dataRange: definition.dataRange, id: "1" }] : [], + }, }; } diff --git a/src/helpers/figures/charts/geo_chart.ts b/src/helpers/figures/charts/geo_chart.ts index a7cc614324..36b2f8c112 100644 --- a/src/helpers/figures/charts/geo_chart.ts +++ b/src/helpers/figures/charts/geo_chart.ts @@ -5,7 +5,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, @@ -15,7 +15,7 @@ import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -41,6 +41,7 @@ export class GeoChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof GeoChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -52,12 +53,7 @@ export class GeoChart extends AbstractChart { constructor(private definition: GeoChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -79,7 +75,8 @@ export class GeoChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): GeoChartDefinition { return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, legendPosition: context.legendPosition ?? "top", title: context.title || { text: "" }, @@ -93,25 +90,29 @@ export class GeoChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } duplicateInDuplicatedSheet(newSheetId: UID): GeoChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new GeoChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): GeoChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -119,25 +120,17 @@ export class GeoChart extends AbstractChart { } getDefinition(): GeoChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): GeoChartDefinition { - const ranges: CustomizedDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - }); - } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -149,16 +142,17 @@ export class GeoChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): GeoChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new GeoChart(definition, this.sheetId, this.getters); } } diff --git a/src/helpers/figures/charts/line_chart.ts b/src/helpers/figures/charts/line_chart.ts index d8e052748d..1d4734dd7b 100644 --- a/src/helpers/figures/charts/line_chart.ts +++ b/src/helpers/figures/charts/line_chart.ts @@ -6,7 +6,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, transformChartDefinitionWithDataSetsWithZone, @@ -18,7 +18,7 @@ import { ChartCreationContext, ChartData, ChartJSRuntime, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -44,6 +44,7 @@ export class LineChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof LineChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -61,12 +62,7 @@ export class LineChart extends AbstractChart { constructor(private definition: LineChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - this.getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(this.getters, sheetId, definition.labelRange); } @@ -88,7 +84,8 @@ export class LineChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): LineChartDefinition { return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, labelsAsText: context.labelsAsText ?? false, legendPosition: context.legendPosition ?? "top", @@ -108,25 +105,17 @@ export class LineChart extends AbstractChart { } getDefinition(): LineChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): LineChartDefinition { - const ranges: CustomizedDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - }); - } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -137,22 +126,22 @@ export class LineChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } updateRanges(applyChange: ApplyRangeChange): LineChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new LineChart(definition, this.sheetId, this.getters); } @@ -173,19 +162,24 @@ export class LineChart extends AbstractChart { } duplicateInDuplicatedSheet(newSheetId: UID): LineChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new LineChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): LineChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); diff --git a/src/helpers/figures/charts/pie_chart.ts b/src/helpers/figures/charts/pie_chart.ts index 89bfc08264..5625f444b0 100644 --- a/src/helpers/figures/charts/pie_chart.ts +++ b/src/helpers/figures/charts/pie_chart.ts @@ -6,7 +6,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, @@ -16,6 +16,7 @@ import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, ChartData, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -43,6 +44,7 @@ export class PieChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof PieChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -55,12 +57,7 @@ export class PieChart extends AbstractChart { constructor(private definition: PieChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -82,7 +79,8 @@ export class PieChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): PieChartDefinition { return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, legendPosition: context.legendPosition ?? "top", title: context.title || { text: "" }, @@ -97,29 +95,25 @@ export class PieChart extends AbstractChart { } getDefinition(): PieChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } getContextCreation(): ChartCreationContext { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): PieChartDefinition { return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: dataSets.map((ds: DataSet) => ({ - dataRange: this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId), - })), + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -127,19 +121,24 @@ export class PieChart extends AbstractChart { } duplicateInDuplicatedSheet(newSheetId: UID): PieChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new PieChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): PieChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -162,16 +161,17 @@ export class PieChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): PieChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new PieChart(definition, this.sheetId, this.getters); } } diff --git a/src/helpers/figures/charts/pyramid_chart.ts b/src/helpers/figures/charts/pyramid_chart.ts index 00f4ad54bc..54fabf1712 100644 --- a/src/helpers/figures/charts/pyramid_chart.ts +++ b/src/helpers/figures/charts/pyramid_chart.ts @@ -7,7 +7,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, transformChartDefinitionWithDataSetsWithZone, @@ -18,7 +18,7 @@ import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -48,6 +48,7 @@ export class PyramidChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof PyramidChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -62,12 +63,7 @@ export class PyramidChart extends AbstractChart { constructor(private definition: PyramidChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -89,7 +85,8 @@ export class PyramidChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): PyramidChartDefinition { return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, aggregated: context.aggregated ?? false, legendPosition: context.legendPosition ?? "top", @@ -108,25 +105,29 @@ export class PyramidChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } duplicateInDuplicatedSheet(newSheetId: UID): PyramidChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new PyramidChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): PyramidChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -134,25 +135,17 @@ export class PyramidChart extends AbstractChart { } getDefinition(): PyramidChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): PyramidChartDefinition { - const ranges: CustomizedDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - }); - } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -190,16 +183,17 @@ export class PyramidChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): PyramidChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new PyramidChart(definition, this.sheetId, this.getters); } } @@ -232,5 +226,12 @@ export function createPyramidChartRuntime( }, }; - return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; + return { + chartJsConfig: config, + background: definition.background || BACKGROUND_CHART_COLOR, + customisableSeries: chartData.dataSetsValues.map(({ dataSetId, label }) => ({ + dataSetId, + label, + })), + }; } diff --git a/src/helpers/figures/charts/radar_chart.ts b/src/helpers/figures/charts/radar_chart.ts index 9a9ac052b3..e3d5d4743e 100644 --- a/src/helpers/figures/charts/radar_chart.ts +++ b/src/helpers/figures/charts/radar_chart.ts @@ -6,7 +6,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, @@ -16,7 +16,7 @@ import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart"; @@ -45,6 +45,7 @@ export class RadarChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof RadarChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -58,12 +59,7 @@ export class RadarChart extends AbstractChart { constructor(private definition: RadarChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -85,7 +81,8 @@ export class RadarChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): RadarChartDefinition { return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, stacked: context.stacked ?? false, aggregated: context.aggregated ?? false, @@ -104,25 +101,29 @@ export class RadarChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } duplicateInDuplicatedSheet(newSheetId: UID): RadarChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new RadarChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): RadarChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -130,25 +131,17 @@ export class RadarChart extends AbstractChart { } getDefinition(): RadarChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): RadarChartDefinition { - const ranges: CustomizedDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - }); - } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -171,16 +164,17 @@ export class RadarChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): RadarChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new RadarChart(definition, this.sheetId, this.getters); } } @@ -212,5 +206,12 @@ export function createRadarChartRuntime( }, }; - return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR }; + return { + chartJsConfig: config, + background: definition.background || BACKGROUND_CHART_COLOR, + customisableSeries: chartData.dataSetsValues.map(({ dataSetId, label }) => ({ + dataSetId, + label, + })), + }; } diff --git a/src/helpers/figures/charts/runtime/chart_data_extractor.ts b/src/helpers/figures/charts/runtime/chart_data_extractor.ts index 2d72872570..af5ff02a2b 100644 --- a/src/helpers/figures/charts/runtime/chart_data_extractor.ts +++ b/src/helpers/figures/charts/runtime/chart_data_extractor.ts @@ -33,8 +33,8 @@ import { ChartData, ChartRuntimeGenerationArgs, ChartWithDataSetDefinition, - CustomizedDataSet, DataSet, + DataSetStyling, DatasetValues, FunnelChartDefinition, LabelValues, @@ -91,9 +91,9 @@ export function getBarChartData( const trendDataSetsValues: (Point[] | undefined)[] = []; for (const index in dataSetsValues) { - const { data } = dataSetsValues[index]; + const { data, dataSetId } = dataSetsValues[index]; - const trend = definition.dataSets?.[index].trend; + const trend = definition.dataSets?.[dataSetId]?.trend; if (!trend?.display || definition.horizontal) { trendDataSetsValues.push(undefined); continue; @@ -190,6 +190,7 @@ function computeValuesAndLabels( data: xValues.map((x) => grouping?.[x]?.[y]), label: getDateTimeLabel(y, verticalGroupBy), hidden: false, + dataSetId: "0", })); return { @@ -410,7 +411,7 @@ export function getFunnelChartData( if (definition.cumulative) { dataSetsValues = makeDatasetsCumulative(dataSetsValues, "desc"); } - + definition.dataSets; const format = getChartDatasetFormat(definition.dataSets, dataSetsValues, "left") || getChartDatasetFormat(definition.dataSets, dataSetsValues, "right"); @@ -916,12 +917,7 @@ export function getChartData( sheetId: UID, definition: ChartWithDataSetDefinition ): ChartData { - const dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + const dataSets = createDataSets(getters, sheetId, definition); const labelRange = createValidRange(getters, sheetId, definition.labelRange); const labelValues = getChartLabelValues(getters, dataSets, labelRange); const dataSetsValues = getChartDatasetValues(getters, dataSets); @@ -943,12 +939,7 @@ export function getHierarchicalData( sheetId: UID, definition: ChartWithDataSetDefinition ): ChartData { - const dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + const dataSets = createDataSets(getters, sheetId, definition); const labelRange = createValidRange(getters, sheetId, definition.labelRange); const labelValues = getChartLabelValues(getters, dataSets, labelRange); const dataSetsValues = getHierarchicalDatasetValues(getters, dataSets); @@ -1002,12 +993,12 @@ function getChartLabelValues( * found in the dataset ranges that isn't a date format. */ function getChartDatasetFormat( - dataSetDefinitions: Pick[], // TODO simplify once CustomizedDataSet no longer contains ranges + dataSetStyling: DataSetStyling | undefined, dataSetValues: DatasetValues[], axis: "left" | "right" ): Format | undefined { const dataSets = dataSetValues.filter( - (ds, i) => (axis === "right") === (dataSetDefinitions[i].yAxisId === "y1") + (ds) => (axis === "right") === (dataSetStyling?.[ds.dataSetId]?.yAxisId === "y1") ); for (const ds of dataSets) { const cell = ds.data.find(({ format }) => format !== undefined && !isDateTimeFormat(format)); @@ -1041,7 +1032,7 @@ function getChartDatasetValues(getters: Getters, dataSets: DataSet[]): DatasetVa } else if (data.every((cell) => !isNumberCell(cell))) { hidden = true; } - datasetValues.push({ data, label, hidden }); + datasetValues.push({ data, label, hidden, dataSetId: ds.dataSetId }); } return datasetValues; } @@ -1063,7 +1054,11 @@ function getHierarchicalDatasetValues(getters: Getters, dataSets: DataSet[]): Da dataSets = dataSets.filter( (ds) => !getters.isColHidden(ds.dataRange.sheetId, ds.dataRange.zone.left) ); - const datasetValues: DatasetValues[] = dataSets.map(() => ({ data: [], label: "" })); + const datasetValues: DatasetValues[] = dataSets.map((ds) => ({ + data: [], + label: "", + dataSetId: ds.dataSetId, + })); const dataSetsData = dataSets.map((ds) => getData(getters, ds)); if (!dataSetsData.length) { return datasetValues; diff --git a/src/helpers/figures/charts/runtime/chartjs_dataset.ts b/src/helpers/figures/charts/runtime/chartjs_dataset.ts index 4bf540b775..a2ce1886eb 100644 --- a/src/helpers/figures/charts/runtime/chartjs_dataset.ts +++ b/src/helpers/figures/charts/runtime/chartjs_dataset.ts @@ -77,8 +77,8 @@ export function getBarChartDatasets( const trendDatasets: ChartDataset<"line">[] = []; for (const index in dataSetsValues) { - let { label, data, hidden } = dataSetsValues[index]; - label = definition.dataSets?.[index].label || label; + let { label, data, hidden, dataSetId } = dataSetsValues[index]; + label = definition.dataSets?.[dataSetId]?.label ?? label; const backgroundColor = colors.next(); const dataset: ChartDataset<"bar"> = { @@ -88,7 +88,7 @@ export function getBarChartDatasets( borderColor: definition.background || BACKGROUND_CHART_COLOR, borderWidth: definition.stacked ? 1 : 0, backgroundColor, - yAxisID: definition.horizontal ? "y" : definition.dataSets?.[index].yAxisId || "y", + yAxisID: definition.horizontal ? "y" : definition.dataSets?.[dataSetId]?.yAxisId ?? "y", xAxisID: "x", barPercentage: 0.9, categoryPercentage: dataSetsValues.length > 1 ? 0.8 : 1, @@ -96,7 +96,7 @@ export function getBarChartDatasets( }; dataSets.push(dataset); - const trendConfig = definition.dataSets?.[index].trend; + const trendConfig = definition.dataSets?.[dataSetId]?.trend; const trendData = args.trendDataSetsValues?.[index]; if (!trendConfig?.display || definition.horizontal || !trendData) { continue; @@ -224,8 +224,8 @@ export function getLineChartDatasets( const colors = getChartColorsGenerator(definition, dataSetsValues.length); for (let index = 0; index < dataSetsValues.length; index++) { - let { label, data, hidden } = dataSetsValues[index]; - label = definition.dataSets?.[index].label || label; + let { label, data, hidden, dataSetId } = dataSetsValues[index]; + label = definition.dataSets?.[dataSetId]?.label ?? label; let dataValues: (number | { x: number; y: number })[] = []; const color = colors.next(); @@ -311,14 +311,14 @@ export function getComboChartDatasets( const colors = getChartColorsGenerator(definition, dataSetsValues.length); const trendDatasets: ChartDataset<"line">[] = []; const barDatasets = dataSetsValues.filter( - (_, i) => (definition.dataSets?.[i].type ?? "line") === "bar" + ({ dataSetId }) => (definition.dataSets?.[dataSetId].type ?? "line") === "bar" ); for (let index = 0; index < dataSetsValues.length; index++) { - let { label, data, hidden } = dataSetsValues[index]; - label = definition.dataSets?.[index].label || label; + let { label, data, hidden, dataSetId } = dataSetsValues[index]; + label = definition.dataSets?.[dataSetId].label || label; - const design = definition.dataSets?.[index]; + const design = definition.dataSets?.[dataSetId]; const color = colors.next(); const type = design?.type ?? "line"; @@ -328,7 +328,7 @@ export function getComboChartDatasets( hidden, borderColor: color, backgroundColor: color, - yAxisID: definition.dataSets?.[index].yAxisId || "y", + yAxisID: definition.dataSets?.[dataSetId].yAxisId || "y", xAxisID: "x", type, order: type === "bar" ? dataSetsValues.length + index : index, @@ -341,7 +341,7 @@ export function getComboChartDatasets( } dataSets.push(dataset); - const trendConfig = definition.dataSets?.[index].trend; + const trendConfig = definition.dataSets?.[dataSetId].trend; const trendData = args.trendDataSetsValues?.[index]; if (!trendConfig?.display || !trendData) { continue; @@ -747,10 +747,10 @@ export function getChartColorsGenerator( definition: GenericDefinition, dataSetsSize: number ) { - return new ColorGenerator( - dataSetsSize, - definition.dataSets?.map((ds) => ds.backgroundColor) || [] + const colors = definition.dataSource?.dataSets?.map( + (ds) => definition.dataSets?.[ds.id]?.backgroundColor ); + return new ColorGenerator(dataSetsSize, colors); } function getTreeMapGroupColors( diff --git a/src/helpers/figures/charts/scatter_chart.ts b/src/helpers/figures/charts/scatter_chart.ts index d7c3ebc4a6..dfe14b426a 100644 --- a/src/helpers/figures/charts/scatter_chart.ts +++ b/src/helpers/figures/charts/scatter_chart.ts @@ -6,7 +6,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, getDefinedAxis, shouldRemoveFirstLabel, @@ -21,7 +21,7 @@ import { getZoneArea } from "@odoo/o-spreadsheet-engine/helpers/zones"; import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDataset, ExcelChartDefinition, @@ -51,6 +51,7 @@ export class ScatterChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof ScatterChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -64,12 +65,7 @@ export class ScatterChart extends AbstractChart { constructor(private definition: ScatterChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - this.getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(this.getters, sheetId, definition.labelRange); } @@ -91,7 +87,8 @@ export class ScatterChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): ScatterChartDefinition { return { background: context.background, - dataSets: context.range ?? [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ?? {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, labelsAsText: context.labelsAsText ?? false, legendPosition: context.legendPosition ?? "top", @@ -107,25 +104,17 @@ export class ScatterChart extends AbstractChart { } getDefinition(): ScatterChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): ScatterChartDefinition { - const ranges: CustomizedDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - }); - } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -136,22 +125,22 @@ export class ScatterChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, auxiliaryRange: definition.labelRange, }; } updateRanges(applyChange: ApplyRangeChange): ScatterChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new ScatterChart(definition, this.sheetId, this.getters); } @@ -181,19 +170,24 @@ export class ScatterChart extends AbstractChart { } duplicateInDuplicatedSheet(newSheetId: UID): ScatterChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new ScatterChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): ScatterChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -233,5 +227,9 @@ export function createScatterChartRuntime( return { chartJsConfig: config, background: definition.background || BACKGROUND_CHART_COLOR, + customisableSeries: chartData.dataSetsValues.map(({ dataSetId, label }) => ({ + dataSetId, + label, + })), }; } diff --git a/src/helpers/figures/charts/smart_chart_engine.ts b/src/helpers/figures/charts/smart_chart_engine.ts index f43ef4f537..65a2b48f12 100644 --- a/src/helpers/figures/charts/smart_chart_engine.ts +++ b/src/helpers/figures/charts/smart_chart_engine.ts @@ -13,7 +13,8 @@ type ColumnType = "number" | "text" | "date" | "percentage" | "empty"; const DEFAULT_BAR_CHART_CONFIG: BarChartDefinition = { type: "bar", title: {}, - dataSets: [], + dataSource: { dataSets: [] }, + dataSets: {}, legendPosition: "none", dataSetsHaveTitle: false, stacked: false, @@ -23,7 +24,8 @@ const DEFAULT_BAR_CHART_CONFIG: BarChartDefinition = { const DEFAULT_LINE_CHART_CONFIG: LineChartDefinition = { type: "line", title: {}, - dataSets: [], + dataSource: { dataSets: [] }, + dataSets: {}, legendPosition: "none", dataSetsHaveTitle: false, stacked: false, @@ -123,7 +125,8 @@ function buildSingleColumnChart(column: ColumnInfo, getters: Getters): ChartDefi return { type: "pie", title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {}, - dataSets: [{ dataRange }], + dataSource: { dataSets: [{ dataRange, id: "1" }] }, + dataSets: {}, legendPosition: "none", dataSetsHaveTitle, }; @@ -138,7 +141,8 @@ function buildSingleColumnChart(column: ColumnInfo, getters: Getters): ChartDefi return { type: "pie", title: hasUniqueTitle ? { text: String(titleCell.value) } : {}, - dataSets: [{ dataRange }], + dataSource: { dataSets: [{ dataRange, id: "1" }] }, + dataSets: {}, labelRange: dataRange, dataSetsHaveTitle: hasUniqueTitle, aggregated: true, @@ -150,14 +154,16 @@ function buildSingleColumnChart(column: ColumnInfo, getters: Getters): ChartDefi ...DEFAULT_LINE_CHART_CONFIG, type: "line", title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {}, - dataSets: [{ dataRange }], + dataSource: { dataSets: [{ dataRange, id: "1" }] }, + dataSets: {}, dataSetsHaveTitle, }; } return { ...DEFAULT_BAR_CHART_CONFIG, title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {}, - dataSets: [{ dataRange }], + dataSource: { dataSets: [{ dataRange, id: "1" }] }, + dataSets: {}, dataSetsHaveTitle, }; } @@ -180,7 +186,8 @@ function buildTwoColumnChart(columns: ColumnInfo[], getters: Getters): ChartDefi return { type: "pie", title: {}, - dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }], + dataSource: { dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone), id: "1" }] }, + dataSets: {}, labelRange: getUnboundRange(getters, columns[0].zone), dataSetsHaveTitle: isDatasetTitled(getters, columns[1]), aggregated: true, @@ -192,7 +199,8 @@ function buildTwoColumnChart(columns: ColumnInfo[], getters: Getters): ChartDefi return { type: "scatter", title: {}, - dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }], + dataSource: { dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone), id: "1" }] }, + dataSets: {}, labelRange: getUnboundRange(getters, columns[0].zone), dataSetsHaveTitle: isDatasetTitled(getters, columns[1]), labelsAsText: false, @@ -205,7 +213,8 @@ function buildTwoColumnChart(columns: ColumnInfo[], getters: Getters): ChartDefi return { ...DEFAULT_LINE_CHART_CONFIG, type: "line", - dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }], + dataSource: { dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone), id: "1" }] }, + dataSets: {}, labelRange: getUnboundRange(getters, columns[0].zone), dataSetsHaveTitle: isDatasetTitled(getters, columns[0]), }; @@ -222,7 +231,10 @@ function buildTwoColumnChart(columns: ColumnInfo[], getters: Getters): ChartDefi return { type: "treemap", title: {}, - dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone) }], + dataSource: { + dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone), id: "1" }], + }, + dataSets: {}, labelRange: getUnboundRange(getters, numberColumn.zone), dataSetsHaveTitle, legendPosition: "none", @@ -232,7 +244,8 @@ function buildTwoColumnChart(columns: ColumnInfo[], getters: Getters): ChartDefi return { ...DEFAULT_BAR_CHART_CONFIG, - dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }], + dataSource: { dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone), id: "1" }] }, + dataSets: {}, labelRange: getUnboundRange(getters, columns[0].zone), dataSetsHaveTitle: isDatasetTitled(getters, columns[1]), }; @@ -262,13 +275,15 @@ function buildMultiColumnChart(columns: ColumnInfo[], getters: Getters): ChartDe (lastColumn.type === "percentage" || lastColumn.type === "number") && columnsExceptLast.every((col) => col.type === "text") ) { - const dataSets = columnsExceptLast.map(({ zone }) => ({ + const dataSets = columnsExceptLast.map(({ zone }, i) => ({ dataRange: getUnboundRange(getters, zone), + id: i.toString(), })); return { type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap", title: {}, - dataSets, + dataSource: { dataSets }, + dataSets: {}, labelRange: getUnboundRange(getters, lastColumn.zone), dataSetsHaveTitle, legendPosition: "none", @@ -277,15 +292,17 @@ function buildMultiColumnChart(columns: ColumnInfo[], getters: Getters): ChartDe const firstColumn = columns[0]; const columnsExceptFirst = columns.slice(1); - const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }) => ({ + const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => ({ dataRange: getUnboundRange(getters, zone), + id: i.toString(), })); if (columnsExceptFirst.every((col) => col.type === "percentage")) { return { type: "pie", title: {}, - dataSets: rangesOfColumnsExceptFirst, + dataSource: { dataSets: rangesOfColumnsExceptFirst }, + dataSets: {}, labelRange: getUnboundRange(getters, firstColumn.zone), dataSetsHaveTitle, aggregated: false, @@ -297,7 +314,8 @@ function buildMultiColumnChart(columns: ColumnInfo[], getters: Getters): ChartDe return { ...DEFAULT_LINE_CHART_CONFIG, type: "line", - dataSets: rangesOfColumnsExceptFirst, + dataSource: { dataSets: rangesOfColumnsExceptFirst }, + dataSets: {}, labelRange: getUnboundRange(getters, firstColumn.zone), dataSetsHaveTitle, legendPosition: "top", @@ -306,7 +324,8 @@ function buildMultiColumnChart(columns: ColumnInfo[], getters: Getters): ChartDe return { ...DEFAULT_BAR_CHART_CONFIG, - dataSets: rangesOfColumnsExceptFirst, + dataSource: { dataSets: rangesOfColumnsExceptFirst }, + dataSets: {}, labelRange: getUnboundRange(getters, firstColumn.zone), dataSetsHaveTitle, legendPosition: "top", @@ -337,8 +356,11 @@ export function getSmartChartDefinition(zones: Zone[], getters: Getters): ChartD const columns = categorizeColumns(zones, getters); if (columns.length === 0 || columns.every((col) => col.type === "empty")) { - const dataSets = columns.map(({ zone }) => ({ dataRange: getUnboundRange(getters, zone) })); - return { ...DEFAULT_BAR_CHART_CONFIG, dataSets }; + const dataSets = columns.map(({ zone }, i) => ({ + dataRange: getUnboundRange(getters, zone), + id: i.toString(), + })); + return { ...DEFAULT_BAR_CHART_CONFIG, dataSource: { dataSets } }; } const nonEmptyColumns = columns.filter((col) => col.type !== "empty"); diff --git a/src/helpers/figures/charts/sunburst_chart.ts b/src/helpers/figures/charts/sunburst_chart.ts index e20adc0256..588b1df7af 100644 --- a/src/helpers/figures/charts/sunburst_chart.ts +++ b/src/helpers/figures/charts/sunburst_chart.ts @@ -5,7 +5,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, @@ -19,7 +19,7 @@ import { import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -42,6 +42,7 @@ export class SunburstChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof SunburstChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -55,12 +56,7 @@ export class SunburstChart extends AbstractChart { constructor(private definition: SunburstChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -80,20 +76,28 @@ export class SunburstChart extends AbstractChart { } static getDefinitionFromContextCreation(context: ChartCreationContext): SunburstChartDefinition { - const dataSets: CustomizedDataSet[] = []; - if (context.hierarchicalRanges?.length) { - dataSets.push(...context.hierarchicalRanges); + // const dataSets: DataSetStyling = []; + // if (context.hierarchicalDataSource) { + // dataSets.push(...context.hierarchicalDataSource); + // } else if (context.auxiliaryRange) { + // dataSets.push({ ...context.dataSets?.[0], dataRange: context.auxiliaryRange }); + // } + + let dataSource: ChartRangeDataSource = { dataSets: [] }; + if (context.hierarchicalDataSource) { + dataSource = context.hierarchicalDataSource; } else if (context.auxiliaryRange) { - dataSets.push({ ...context.range?.[0], dataRange: context.auxiliaryRange }); + dataSource = { dataSets: [{ dataRange: context.auxiliaryRange, id: "0" }] }; } return { background: context.background, - dataSets, + dataSets: context.dataSets ?? {}, + dataSource, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, legendPosition: context.legendPosition ?? "top", title: context.title || { text: "" }, type: "sunburst", - labelRange: context.range?.[0]?.dataRange, + labelRange: dataSource.dataSets?.[0]?.dataRange, showValues: context.showValues, showLabels: context.showLabels, valuesDesign: context.valuesDesign, @@ -104,31 +108,30 @@ export class SunburstChart extends AbstractChart { } getDefinition(): SunburstChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } getContextCreation(): ChartCreationContext { const definition = this.getDefinition(); - const leafRange = definition.dataSets.at(-1)?.dataRange; + const leafRange = definition.dataSource.dataSets.at(-1)?.dataRange; return { ...definition, - range: definition.labelRange ? [{ dataRange: definition.labelRange }] : [], + dataSource: definition.labelRange + ? { dataSets: [{ dataRange: definition.labelRange, id: "0" }] } + : { dataSets: [] }, auxiliaryRange: leafRange, - hierarchicalRanges: definition.dataSets, + hierarchicalDataSource: definition.dataSource, }; } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): SunburstChartDefinition { return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: dataSets.map((ds: DataSet) => ({ - dataRange: this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId), - })), + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -136,19 +139,24 @@ export class SunburstChart extends AbstractChart { } duplicateInDuplicatedSheet(newSheetId: UID): SunburstChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new SunburstChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): SunburstChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -160,16 +168,17 @@ export class SunburstChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): SunburstChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new SunburstChart(definition, this.sheetId, this.getters); } } diff --git a/src/helpers/figures/charts/tree_map_chart.ts b/src/helpers/figures/charts/tree_map_chart.ts index 09a229886a..c4fd1f0355 100644 --- a/src/helpers/figures/charts/tree_map_chart.ts +++ b/src/helpers/figures/charts/tree_map_chart.ts @@ -5,7 +5,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, @@ -15,7 +15,7 @@ import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -47,6 +47,7 @@ export class TreeMapChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof TreeMapChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -61,12 +62,7 @@ export class TreeMapChart extends AbstractChart { constructor(private definition: TreeMapChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -86,20 +82,21 @@ export class TreeMapChart extends AbstractChart { } static getDefinitionFromContextCreation(context: ChartCreationContext): TreeMapChartDefinition { - const dataSets: CustomizedDataSet[] = []; - if (context.hierarchicalRanges?.length) { - dataSets.push(...context.hierarchicalRanges); + let dataSource: ChartRangeDataSource = { dataSets: [] }; + if (context.hierarchicalDataSource) { + dataSource = context.hierarchicalDataSource; } else if (context.auxiliaryRange) { - dataSets.push({ ...context.range?.[0], dataRange: context.auxiliaryRange }); + dataSource = { dataSets: [{ dataRange: context.auxiliaryRange, id: "0" }] }; } return { background: context.background, - dataSets, + dataSets: context.dataSets ?? {}, + dataSource, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, legendPosition: context.legendPosition ?? "top", title: context.title || { text: "" }, type: "treemap", - labelRange: context.range?.[0]?.dataRange, + labelRange: dataSource.dataSets?.[0]?.dataRange, showValues: context.showValues, showHeaders: context.showHeaders, headerDesign: context.headerDesign, @@ -112,51 +109,54 @@ export class TreeMapChart extends AbstractChart { getContextCreation(): ChartCreationContext { const definition = this.getDefinition(); - const leafRange = definition.dataSets.at(-1)?.dataRange; + const leafRange = definition.dataSource.dataSets.at(-1)?.dataRange; return { ...definition, treemapColoringOptions: definition.coloringOptions, - range: definition.labelRange ? [{ dataRange: definition.labelRange }] : [], + dataSource: definition.labelRange + ? { dataSets: [{ dataRange: definition.labelRange, id: "0" }] } + : { dataSets: [] }, auxiliaryRange: leafRange, - hierarchicalRanges: definition.dataSets, + hierarchicalDataSource: definition.dataSource, }; } duplicateInDuplicatedSheet(newSheetId: UID): TreeMapChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new TreeMapChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): TreeMapChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); return new TreeMapChart(definition, sheetId, this.getters); } getDefinition(): TreeMapChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): TreeMapChartDefinition { - const ranges: CustomizedDataSet[] = dataSets.map((dataSet) => ({ - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - })); return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -168,16 +168,17 @@ export class TreeMapChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): TreeMapChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new TreeMapChart(definition, this.sheetId, this.getters); } } diff --git a/src/helpers/figures/charts/waterfall_chart.ts b/src/helpers/figures/charts/waterfall_chart.ts index 17278be0fc..8021240198 100644 --- a/src/helpers/figures/charts/waterfall_chart.ts +++ b/src/helpers/figures/charts/waterfall_chart.ts @@ -5,7 +5,7 @@ import { checkDataset, checkLabelRange, createDataSets, - duplicateDataSetsInDuplicatedSheet, + duplicateDataSourceInDuplicatedSheet, duplicateLabelRangeInDuplicatedSheet, transformChartDefinitionWithDataSetsWithZone, updateChartRangesWithDataSets, @@ -15,7 +15,7 @@ import { createValidRange } from "@odoo/o-spreadsheet-engine/helpers/range"; import { ChartCreationContext, ChartData, - CustomizedDataSet, + ChartRangeDataSource, DataSet, ExcelChartDefinition, } from "@odoo/o-spreadsheet-engine/types/chart/chart"; @@ -43,6 +43,7 @@ export class WaterfallChart extends AbstractChart { static allowedDefinitionKeys: readonly (keyof WaterfallChartDefinition)[] = [ ...AbstractChart.commonKeys, + "dataSource", "legendPosition", "dataSets", "dataSetsHaveTitle", @@ -62,12 +63,7 @@ export class WaterfallChart extends AbstractChart { constructor(private definition: WaterfallChartDefinition, sheetId: UID, getters: CoreGetters) { super(definition, sheetId, getters); - this.dataSets = createDataSets( - getters, - definition.dataSets, - sheetId, - definition.dataSetsHaveTitle - ); + this.dataSets = createDataSets(getters, sheetId, definition); this.labelRange = createValidRange(getters, sheetId, definition.labelRange); } @@ -89,7 +85,8 @@ export class WaterfallChart extends AbstractChart { static getDefinitionFromContextCreation(context: ChartCreationContext): WaterfallChartDefinition { return { background: context.background, - dataSets: context.range ? context.range : [], + dataSource: context.dataSource ?? { dataSets: [] }, + dataSets: context.dataSets ? context.dataSets : {}, dataSetsHaveTitle: context.dataSetsHaveTitle ?? false, aggregated: context.aggregated ?? false, legendPosition: context.legendPosition ?? "top", @@ -111,25 +108,30 @@ export class WaterfallChart extends AbstractChart { const definition = this.getDefinition(); return { ...definition, - range: definition.dataSets, + dataSets: definition.dataSets, auxiliaryRange: definition.labelRange, }; } duplicateInDuplicatedSheet(newSheetId: UID): WaterfallChart { - const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets); + const dataSource = duplicateDataSourceInDuplicatedSheet( + this.getters, + this.sheetId, + newSheetId, + this.definition.dataSource + ); const labelRange = duplicateLabelRangeInDuplicatedSheet( this.sheetId, newSheetId, this.labelRange ); - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange, newSheetId); return new WaterfallChart(definition, newSheetId, this.getters); } copyInSheetId(sheetId: UID): WaterfallChart { const definition = this.getDefinitionWithSpecificDataSets( - this.dataSets, + this.definition.dataSource, this.labelRange, sheetId ); @@ -137,25 +139,17 @@ export class WaterfallChart extends AbstractChart { } getDefinition(): WaterfallChartDefinition { - return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange); + return this.getDefinitionWithSpecificDataSets(this.definition.dataSource, this.labelRange); } private getDefinitionWithSpecificDataSets( - dataSets: DataSet[], + dataSource: ChartRangeDataSource, labelRange: Range | undefined, targetSheetId?: UID ): WaterfallChartDefinition { - const ranges: CustomizedDataSet[] = []; - for (const [i, dataSet] of dataSets.entries()) { - ranges.push({ - ...this.definition.dataSets?.[i], - dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId), - }); - } return { ...this.definition, - dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false, - dataSets: ranges, + dataSource, labelRange: labelRange ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, @@ -168,16 +162,17 @@ export class WaterfallChart extends AbstractChart { } updateRanges(applyChange: ApplyRangeChange): WaterfallChart { - const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets( + const { dataSource, labelRange, isStale } = updateChartRangesWithDataSets( this.getters, + this.sheetId, applyChange, - this.dataSets, + this.definition.dataSource, this.labelRange ); if (!isStale) { return this; } - const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange); + const definition = this.getDefinitionWithSpecificDataSets(dataSource, labelRange); return new WaterfallChart(definition, this.sheetId, this.getters); } } diff --git a/tests/figures/chart/combo_chart_plugin.test.ts b/tests/figures/chart/combo_chart_plugin.test.ts index 6dd36d1155..1204c001ed 100644 --- a/tests/figures/chart/combo_chart_plugin.test.ts +++ b/tests/figures/chart/combo_chart_plugin.test.ts @@ -1,6 +1,6 @@ import { BarChartRuntime } from "@odoo/o-spreadsheet-engine/types/chart"; import { - ComboChartDataSet, + ComboChartDataSetStyling, ComboChartRuntime, } from "@odoo/o-spreadsheet-engine/types/chart/combo_chart"; import { ChartConfiguration } from "chart.js"; @@ -153,19 +153,32 @@ describe("combo chart", () => { test("Bar spacing is adapted to the number of bar datasets", () => { const model = createModelFromGrid({ A2: "2", B2: "3", C2: "4" }); - let dataSets: ComboChartDataSet[] = [ - { dataRange: "A1:A3", type: "bar" }, - { dataRange: "B1:B3", type: "line" }, - ]; - createChart(model, { type: "combo", dataSets }, "chartId"); + let dataSets: ComboChartDataSetStyling = { + bar: { type: "bar" }, + line: { type: "line" }, + }; + const dataSource = { + dataSets: [ + { dataRange: "A1:A3", id: "bar" }, + { dataRange: "B1:B3", id: "line" }, + ], + }; + createChart(model, { type: "combo", dataSets, dataSource }, "chartId"); let runtime = model.getters.getChartRuntime("chartId") as BarChartRuntime; let config = runtime.chartJsConfig as ChartConfiguration<"bar">; expect(config.data.datasets[0].barPercentage).toEqual(0.9); expect(config.data.datasets[0].categoryPercentage).toEqual(1); - dataSets = [...dataSets, { dataRange: "C1:C3", type: "bar" }]; - updateChart(model, "chartId", { dataSets }); + dataSets = { ...dataSets, bar2: { type: "bar" } }; + const newDataSource = { + dataSets: [ + { dataRange: "A1:A3", id: "bar" }, + { dataRange: "B1:B3", id: "line" }, + { dataRange: "C1:C3", id: "bar2" }, + ], + }; + updateChart(model, "chartId", { dataSets, dataSource: newDataSource }); runtime = model.getters.getChartRuntime("chartId") as BarChartRuntime; config = runtime.chartJsConfig as ChartConfiguration<"bar">; expect(config.data.datasets.map((ds) => ds.barPercentage)).toEqual([0.9, undefined, 0.9]); // undefined for line dataset diff --git a/tests/figures/chart/menu_item_insert_chart.test.ts b/tests/figures/chart/menu_item_insert_chart.test.ts index bde7e7430f..a0c4fdf890 100644 --- a/tests/figures/chart/menu_item_insert_chart.test.ts +++ b/tests/figures/chart/menu_item_insert_chart.test.ts @@ -5,7 +5,7 @@ import { DEFAULT_FIGURE_WIDTH, } from "@odoo/o-spreadsheet-engine/constants"; import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env"; -import { ChartDefinition, CustomizedDataSet, Model } from "../../../src"; +import { ChartDefinition, Model } from "../../../src"; import { toXC, zoneToXc } from "../../../src/helpers"; import { addColumns, @@ -537,7 +537,7 @@ describe("Smart chart type detection", () => { doAction(["insert", "insert_chart"], env); const datasetLastCol = datasetPattern.findIndex((p) => !p.includes("text")); - const expectedDatasets: CustomizedDataSet[] = []; + const expectedDatasets: DataSetStyling = []; for (let i = 0; i < datasetLastCol; i++) { expectedDatasets.push({ dataRange: toXC(i, 0) + ":" + toXC(i, 5) }); } @@ -567,7 +567,7 @@ describe("Smart chart type detection", () => { createDatasetFromDescription(datasetPattern); doAction(["insert", "insert_chart"], env); - const expectedDatasets: CustomizedDataSet[] = []; + const expectedDatasets: DataSetStyling = []; for (let i = 1; i < datasetPattern.length; i++) { expectedDatasets.push({ dataRange: toXC(i, 0) + ":" + toXC(i, 5) }); } diff --git a/tests/xlsx/xlsx_export.test.ts b/tests/xlsx/xlsx_export.test.ts index 5274b69e2e..8a14969d05 100644 --- a/tests/xlsx/xlsx_export.test.ts +++ b/tests/xlsx/xlsx_export.test.ts @@ -7,7 +7,7 @@ import { hexaToInt } from "@odoo/o-spreadsheet-engine/xlsx/conversion"; import { adaptFormulaToExcel } from "@odoo/o-spreadsheet-engine/xlsx/functions/cells"; import { escapeXml, parseXML } from "@odoo/o-spreadsheet-engine/xlsx/helpers/xml_helpers"; import { buildSheetLink, toXC } from "../../src/helpers"; -import { CustomizedDataSet, Dimension } from "../../src/types"; +import { Dimension } from "../../src/types"; import { arg } from "@odoo/o-spreadsheet-engine/functions/arguments"; import { functionRegistry } from "@odoo/o-spreadsheet-engine/functions/function_registry"; @@ -1130,22 +1130,19 @@ describe("Test XLSX export", () => { ["combo", [{ dataRange: "Sheet1!B1:B4" }]], ["pie", [{ dataRange: "Sheet1!B1:B4" }]], ["radar", [{ dataRange: "Sheet1!B1:B4" }]], - ])( - "simple %s chart with dataset %s", - async (chartType: string, dataSets: CustomizedDataSet[]) => { - const model = new Model(chartData); - createChart( - model, - { - dataSets, - labelRange: "Sheet1!A2:A4", - type: chartType as "line" | "bar" | "pie" | "combo", - }, - "1" - ); - expect(await exportPrettifiedXlsx(model)).toMatchSnapshot(); - } - ); + ])("simple %s chart with dataset %s", async (chartType: string, dataSets: DataSetStyling) => { + const model = new Model(chartData); + createChart( + model, + { + dataSets, + labelRange: "Sheet1!A2:A4", + type: chartType as "line" | "bar" | "pie" | "combo", + }, + "1" + ); + expect(await exportPrettifiedXlsx(model)).toMatchSnapshot(); + }); test.each(["line", "scatter", "bar", "combo", "radar"])( "simple %s chart with customized dataset",