Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { compile } from "../../../formulas/compiler";

import { createEvaluatedCell, evaluateLiteral } from "../../../helpers/cells/cell_evaluation";

import { CellValueType, EvaluatedCell, FormulaCell } from "../../../types/cells";
import { CellValueType, EmptyCell, EvaluatedCell, FormulaCell } from "../../../types/cells";
import {
BadExpressionError,
CellErrorType,
Expand Down Expand Up @@ -45,7 +45,9 @@ const MAX_ITERATION = 30;
const ERROR_CYCLE_CELL = Object.freeze(
createEvaluatedCell({ ...new CircularDependencyError(), origin: undefined })
);
const EMPTY_CELL = Object.freeze(createEvaluatedCell({ value: null }));
export const EMPTY_CELL: EmptyCell = Object.freeze(
createEvaluatedCell({ value: null })
) as EmptyCell;

export class Evaluator {
private readonly getters: Getters;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { range } from "../../helpers/misc";
import { CellPosition, Dimension, HeaderIndex, UID } from "../../types/misc";
import { ExcelWorkbookData } from "../../types/workbook_data";
import { UIPlugin } from "../ui_plugin";
Expand Down Expand Up @@ -78,10 +77,12 @@ export class HeaderVisibilityUIPlugin extends UIPlugin {
dimension: Dimension,
{ last, first }: { first: HeaderIndex; last: HeaderIndex }
): HeaderIndex {
const lastVisibleIndex = range(last, first, -1).find(
(index) => !this.isHeaderHidden(sheetId, dimension, index)
);
return lastVisibleIndex || first;
for (let header = last; header >= first; header--) {
if (!this.isHeaderHidden(sheetId, dimension, header)) {
return header;
}
}
return first;
}

findFirstVisibleColRowIndex(sheetId: UID, dimension: Dimension) {
Expand Down
3 changes: 2 additions & 1 deletion packages/o-spreadsheet-engine/src/types/chart/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "./tree_map_chart";
import { WaterfallChartDefinition, WaterfallChartRuntime } from "./waterfall_chart";

import { EvaluatedCell } from "../cells";
import { Format } from "../format";
import { Locale } from "../locale";
import { Range } from "../range";
Expand Down Expand Up @@ -93,7 +94,7 @@ export interface LabelValues {

export interface DatasetValues {
readonly label?: string;
readonly data: any[];
readonly data: EvaluatedCell[];
readonly hidden?: boolean;
}

Expand Down
9 changes: 8 additions & 1 deletion src/helpers/figures/charts/pyramid_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { toXlsxHexColor } from "@odoo/o-spreadsheet-engine/xlsx/helpers/colors";
import { ChartConfiguration } from "chart.js";
import {
ApplyRangeChange,
CellValueType,
Color,
CommandResult,
Getters,
Expand Down Expand Up @@ -197,7 +198,13 @@ 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) =>
cell.type === CellValueType.number ? Math.abs(cell.value) : -Infinity
)
)
)
);
return {
...definition,
Expand Down
86 changes: 54 additions & 32 deletions src/helpers/figures/charts/runtime/chart_data_extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ 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 { createEvaluatedCell } from "@odoo/o-spreadsheet-engine/helpers/cells/cell_evaluation";
import { shouldRemoveFirstLabel } from "@odoo/o-spreadsheet-engine/helpers/figures/charts/chart_common";
import { isDateTimeFormat } from "@odoo/o-spreadsheet-engine/helpers/format/format";
import { deepCopy, findNextDefinedValue, range } from "@odoo/o-spreadsheet-engine/helpers/misc";
import { isNumber } from "@odoo/o-spreadsheet-engine/helpers/numbers";
import { recomputeZones } from "@odoo/o-spreadsheet-engine/helpers/recompute_zones";
import { positions } from "@odoo/o-spreadsheet-engine/helpers/zones";
import { EMPTY_CELL } from "@odoo/o-spreadsheet-engine/plugins/ui_core_views/cell_evaluation/evaluator";
import {
AxisType,
BarChartDefinition,
Expand All @@ -37,15 +39,22 @@ import { TreeMapChartDefinition } from "@odoo/o-spreadsheet-engine/types/chart/t
import { Point } from "chart.js";
import {
CellValue,
CellValueType,
DEFAULT_LOCALE,
EmptyCell,
EvaluatedCell,
Format,
GenericDefinition,
Getters,
Locale,
NumberCell,
Range,
} from "../../../../types";
import { timeFormatLuxonCompatible } from "../../../chart_date";

const ZERO = Object.freeze(createEvaluatedCell({ value: 0 }));
const ONE = Object.freeze(createEvaluatedCell({ value: 1 }));

export function getBarChartData(
definition: GenericDefinition<BarChartDefinition>,
dataSets: DataSet[],
Expand Down Expand Up @@ -105,11 +114,17 @@ 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) =>
cell.type === CellValueType.number && 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) =>
cell.type === CellValueType.number && cell.value > 0
? createEvaluatedCell({ value: -cell.value })
: ZERO
);
pyramidDatasetValues.push({ ...barDataset[1], data: pyramidData });
}

Expand Down Expand Up @@ -632,7 +647,7 @@ function keepOnlyPositiveValues(
dataSetsValues: datasets.map((ds) => ({
...ds,
data: filteredIndexes.map((i) =>
typeof ds.data[i] === "number" && ds.data[i] > 0 ? ds.data[i] : null
ds.data[i].type === CellValueType.number && ds.data[i].value > 0 ? ds.data[i] : EMPTY_CELL
),
})),
};
Expand All @@ -651,7 +666,7 @@ function fixEmptyLabelsForDateCharts(
if (!newLabels[i]) {
newLabels[i] = findNextDefinedValue(newLabels, i);
for (const ds of newDatasets) {
ds.data[i] = undefined;
ds.data[i] = EMPTY_CELL;
}
}
}
Expand All @@ -661,15 +676,17 @@ function fixEmptyLabelsForDateCharts(
/**
* Get the data from a dataSet
*/
export function getData(getters: Getters, ds: DataSet): (CellValue | undefined)[] {
export function getData(getters: Getters, ds: DataSet): EvaluatedCell[] {
if (ds.dataRange) {
const labelCellZone = ds.labelCell ? [ds.labelCell.zone] : [];
const dataZone = recomputeZones([ds.dataRange.zone], labelCellZone)[0];
if (dataZone === undefined) {
return [];
}
const dataRange = getters.getRangeFromZone(ds.dataRange.sheetId, dataZone);
return getters.getRangeValues(dataRange).map((value) => (value === "" ? undefined : value));
const { sheetId, zone } = getters.getRangeFromZone(ds.dataRange.sheetId, dataZone);
return getters
.getEvaluatedCellsInZone(sheetId, zone)
.map((cell) => (cell.value === "" ? EMPTY_CELL : cell));
}
return [];
}
Expand Down Expand Up @@ -697,7 +714,7 @@ function filterInvalidDataPoints(
dataSetsValues: datasets.map((dataset) => ({
...dataset,
data: dataPointsIndexes.map((i) =>
typeof dataset.data[i] === "number" ? dataset.data[i] : null
dataset.data[i].type === CellValueType.number ? dataset.data[i] : EMPTY_CELL
),
})),
};
Expand All @@ -714,17 +731,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;
}
}
Expand Down Expand Up @@ -783,15 +800,19 @@ 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
);
}
}

return {
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) =>
createEvaluatedCell({ value: labelMap[label][indexOfDataset] })
),
})),
};
}
Expand Down Expand Up @@ -890,14 +911,14 @@ 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 || cell.type === CellValueType.text) &&
data.filter((cell) => cell.type === CellValueType.text).length > 1
) {
// Convert categorical data into counts
data = data.map((e) => (e && !isEvaluationError(e) ? 1 : null));
data = data.map((cell) => (cell && cell.type !== CellValueType.error ? ONE : EMPTY_CELL));
} else if (
data.every(
(cell) => cell === undefined || cell === null || !isNumber(cell.toString(), DEFAULT_LOCALE)
(cell) => cell.value === null || !isNumber(cell.value.toString(), DEFAULT_LOCALE) // bizarre
)
) {
hidden = true;
Expand Down Expand Up @@ -931,20 +952,20 @@ function getHierarchicalDatasetValues(getters: Getters, dataSets: DataSet[]): Da
}
const minLength = Math.min(...dataSetsData.map((ds) => ds.length));

let currentValues: (CellValue | undefined)[] = [];
let currentValues: EvaluatedCell[] = [];
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.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);
}
}

Expand All @@ -956,16 +977,17 @@ export function makeDatasetsCumulative(
order: "asc" | "desc"
): DatasetValues[] {
return datasets.map((dataset) => {
const data: number[] = [];
const data: (NumberCell | EmptyCell)[] = [];
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 (cell.type === CellValueType.number) {
accumulator += cell.value;
data[i] = { ...cell, value: accumulator };
} else {
data[i] = dataset.data[i];
data[i] = EMPTY_CELL;
}
}
return { ...dataset, data };
Expand Down
Loading