Skip to content

Commit

Permalink
[IMP] gauge chart: allow formulas in gauge config
Browse files Browse the repository at this point in the history
This commit adds the possibility to use formulas to configure the gauge
chart min/max ranges and the threshold values.

This make it so we cannot rely on allowDispatch now to check that the
values are number, because the only implemented allowDispatch of charts
is core (so no evaluation). The panel will try to filter the non-number
values, but they could change to non-number afterwards. In that case:

- the non-number thresholds will be dropped
- the chart will display an error if the range min/max is not a number

Task: 4236214
  • Loading branch information
hokolomopo committed Nov 8, 2024
1 parent c79b70d commit f76c9cb
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component, useState } from "@odoo/owl";
import { tryToNumber } from "../../../../functions/helpers";
import { deepCopy } from "../../../../helpers/index";
import { GaugeChartDefinition, SectionRule } from "../../../../types/chart/gauge_chart";
import {
Expand All @@ -7,7 +8,9 @@ import {
DispatchResult,
SpreadsheetChildEnv,
UID,
isMatrix,
} from "../../../../types/index";
import { StandaloneComposer } from "../../../composer/standalone_composer/standalone_composer";
import { css } from "../../../helpers/css";
import { ChartTerms } from "../../../translations_terms";
import { SidePanelCollapsible } from "../../components/collapsible/side_panel_collapsible";
Expand Down Expand Up @@ -78,6 +81,7 @@ export class GaugeChartDesignPanel extends Component<Props, SpreadsheetChildEnv>
RoundColorPicker,
GeneralDesignEditor,
ChartErrorSection,
StandaloneComposer,
};
static props = {
figureId: String,
Expand All @@ -90,7 +94,9 @@ export class GaugeChartDesignPanel extends Component<Props, SpreadsheetChildEnv>

setup() {
this.state = useState<PanelState>({
sectionRuleDispatchResult: undefined,
sectionRuleDispatchResult: new DispatchResult(
this.checkSectionRuleFormulasAreValid(this.props.definition.sectionRule)
),
sectionRule: deepCopy(this.props.definition.sectionRule),
});
}
Expand All @@ -102,14 +108,14 @@ export class GaugeChartDesignPanel extends Component<Props, SpreadsheetChildEnv>
);
}

isRangeMinInvalid() {
get isRangeMinInvalid() {
return !!(
this.state.sectionRuleDispatchResult?.isCancelledBecause(CommandResult.EmptyGaugeRangeMin) ||
this.state.sectionRuleDispatchResult?.isCancelledBecause(CommandResult.GaugeRangeMinNaN)
);
}

isRangeMaxInvalid() {
get isRangeMaxInvalid() {
return !!(
this.state.sectionRuleDispatchResult?.isCancelledBecause(CommandResult.EmptyGaugeRangeMax) ||
this.state.sectionRuleDispatchResult?.isCancelledBecause(CommandResult.GaugeRangeMaxNaN)
Expand Down Expand Up @@ -139,6 +145,12 @@ export class GaugeChartDesignPanel extends Component<Props, SpreadsheetChildEnv>
}

updateSectionRule(sectionRule: SectionRule) {
const invalidValueReasons = this.checkSectionRuleFormulasAreValid(this.state.sectionRule);
if (invalidValueReasons.length > 0) {
this.state.sectionRuleDispatchResult = new DispatchResult(invalidValueReasons);
return;
}

this.state.sectionRuleDispatchResult = this.props.updateChart(this.props.figureId, {
sectionRule,
});
Expand All @@ -147,9 +159,69 @@ export class GaugeChartDesignPanel extends Component<Props, SpreadsheetChildEnv>
}
}

canUpdateSectionRule(sectionRule: SectionRule) {
this.state.sectionRuleDispatchResult = this.props.canUpdateChart(this.props.figureId, {
sectionRule,
});
onConfirmGaugeRange(editedRange: "rangeMin" | "rangeMax", content: string) {
this.state.sectionRule = { ...this.state.sectionRule, [editedRange]: content };
this.updateSectionRule(this.state.sectionRule);
}

getGaugeInflectionComposerProps(
sectionType: "lowerColor" | "middleColor"
): StandaloneComposer["props"] {
const inflectionPointName =
sectionType === "lowerColor" ? "lowerInflectionPoint" : "upperInflectionPoint";
const inflectionPoint = this.state.sectionRule[inflectionPointName];
return {
onConfirm: (str: string) => {
this.state.sectionRule = {
...this.state.sectionRule,
[inflectionPointName]: { ...inflectionPoint, value: str },
};
this.updateSectionRule(this.state.sectionRule);
},
composerContent: inflectionPoint.value,
invalid:
sectionType === "lowerColor"
? this.isLowerInflectionPointInvalid
: this.isUpperInflectionPointInvalid,
defaultRangeSheetId: this.sheetId,
class: inflectionPointName,
};
}

private checkSectionRuleFormulasAreValid(sectionRule: SectionRule): CommandResult[] {
const reasons: CommandResult[] = [];
if (!this.valueIsValidNumber(sectionRule.rangeMin)) {
reasons.push(CommandResult.GaugeRangeMinNaN);
}
if (!this.valueIsValidNumber(sectionRule.rangeMax)) {
reasons.push(CommandResult.GaugeRangeMaxNaN);
}
if (!this.valueIsValidNumber(sectionRule.lowerInflectionPoint.value)) {
reasons.push(CommandResult.GaugeLowerInflectionPointNaN);
}
if (!this.valueIsValidNumber(sectionRule.upperInflectionPoint.value)) {
reasons.push(CommandResult.GaugeUpperInflectionPointNaN);
}
return reasons;
}

private valueIsValidNumber(value: string): boolean {
const locale = this.env.model.getters.getLocale();
if (!value.startsWith("=")) {
return tryToNumber(value, locale) !== undefined;
}
const evaluatedValue = this.env.model.getters.evaluateFormula(this.sheetId, value);
if (isMatrix(evaluatedValue)) {
return false;
}
return tryToNumber(evaluatedValue, locale) !== undefined;
}

get sheetId() {
const chart = this.env.model.getters.getChart(this.props.figureId);
if (!chart) {
throw new Error("Chart not found with id " + this.props.figureId);
}
return chart.sheetId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,21 @@
<t t-set-slot="content">
<Section class="'pt-0'" title.translate="Range">
<div class="o-subsection-left">
<input
type="text"
t-model="state.sectionRule.rangeMin"
t-on-change="() => this.updateSectionRule(state.sectionRule)"
t-on-input="() => this.canUpdateSectionRule(state.sectionRule)"
class="o-input o-data-range-min"
t-att-class="{ 'o-invalid': isRangeMinInvalid() }"
<StandaloneComposer
class="'o-data-range-min'"
invalid="isRangeMinInvalid"
composerContent="state.sectionRule.rangeMin"
defaultRangeSheetId="sheetId"
onConfirm="(str) => this.onConfirmGaugeRange('rangeMin', str)"
/>
</div>
<div class="o-subsection-right">
<input
type="text"
t-model="state.sectionRule.rangeMax"
t-on-change="() => this.updateSectionRule(state.sectionRule)"
t-on-input="() => this.canUpdateSectionRule(state.sectionRule)"
class="o-input o-data-range-max"
t-att-class="{ 'o-invalid': isRangeMaxInvalid() }"
<StandaloneComposer
class="'o-data-range-max'"
invalid="isRangeMaxInvalid"
composerContent="state.sectionRule.rangeMax"
defaultRangeSheetId="sheetId"
onConfirm="(str) => this.onConfirmGaugeRange('rangeMax', str)"
/>
</div>
</Section>
Expand Down Expand Up @@ -107,15 +105,7 @@
</select>
</td>
<td class="pe-2">
<input
type="text"
class="o-input"
t-model="inflectionPoint.value"
t-on-input="() => this.canUpdateSectionRule(state.sectionRule)"
t-on-change="() => this.updateSectionRule(state.sectionRule)"
t-attf-class="o-input-{{inflectionPointName}}"
t-att-class="{ 'o-invalid': isInvalid }"
/>
<StandaloneComposer t-props="getGaugeInflectionComposerProps(sectionType)"/>
</td>
<td>
<select
Expand Down
63 changes: 53 additions & 10 deletions src/helpers/figures/charts/gauge_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DEFAULT_GAUGE_MIDDLE_COLOR,
DEFAULT_GAUGE_UPPER_COLOR,
} from "../../../constants";
import { tryToNumber } from "../../../functions/helpers";
import { BasePlugin } from "../../../plugins/base_plugin";
import {
AddColumnsRowsCommand,
Expand All @@ -19,6 +20,7 @@ import {
UID,
UnboundedZone,
Validation,
isMatrix,
} from "../../../types";
import { ChartCreationContext } from "../../../types/chart/chart";
import {
Expand All @@ -28,6 +30,7 @@ import {
SectionRule,
SectionThreshold,
} from "../../../types/chart/gauge_chart";
import { CellErrorType } from "../../../types/errors";
import { Validator } from "../../../types/validator";
import { clip, formatValue } from "../../index";
import { createValidRange } from "../../range";
Expand Down Expand Up @@ -106,7 +109,10 @@ function checkEmpty(value: string, valueName: string) {
return CommandResult.Success;
}

function checkNaN(value: string, valueName: string) {
function checkValueIsNumberOrFormula(value: string, valueName: string) {
if (value.startsWith("=")) {
return CommandResult.Success;
}
if (isNaN(value as any)) {
switch (valueName) {
case "rangeMin":
Expand Down Expand Up @@ -144,9 +150,11 @@ export class GaugeChart extends AbstractChart {
isDataRangeValid,
validator.chainValidations(
checkRangeLimits(checkEmpty, validator.batchValidations),
checkRangeLimits(checkNaN, validator.batchValidations)
checkRangeLimits(checkValueIsNumberOrFormula, validator.batchValidations)
),
validator.chainValidations(checkInflectionPointsValue(checkNaN, validator.batchValidations))
validator.chainValidations(
checkInflectionPointsValue(checkValueIsNumberOrFormula, validator.batchValidations)
)
);
}

Expand Down Expand Up @@ -269,15 +277,31 @@ export function createGaugeChartRuntime(chart: GaugeChart, getters: Getters): Ga
}
}

let minValue = Number(chart.sectionRule.rangeMin);
let maxValue = Number(chart.sectionRule.rangeMax);
let minValue = getFormulaNumberValue(chart.sheetId, chart.sectionRule.rangeMin, getters);
let maxValue = getFormulaNumberValue(chart.sheetId, chart.sectionRule.rangeMax, getters);
if (minValue === undefined || maxValue === undefined) {
return getInvalidGaugeRuntime(chart, getters);
}
if (maxValue < minValue) {
[minValue, maxValue] = [maxValue, minValue];
}

const lowerPoint = chart.sectionRule.lowerInflectionPoint;
const upperPoint = chart.sectionRule.upperInflectionPoint;
const lowerPointValue = getSectionThresholdValue(lowerPoint, minValue, maxValue);
const upperPointValue = getSectionThresholdValue(upperPoint, minValue, maxValue);
const lowerPointValue = getSectionThresholdValue(
chart.sheetId,
chart.sectionRule.lowerInflectionPoint,
minValue,
maxValue,
getters
);
const upperPointValue = getSectionThresholdValue(
chart.sheetId,
chart.sectionRule.upperInflectionPoint,
minValue,
maxValue,
getters
);

const inflectionValues: GaugeInflectionValue[] = [];
const colors: Color[] = [];
Expand Down Expand Up @@ -332,17 +356,36 @@ export function createGaugeChartRuntime(chart: GaugeChart, getters: Getters): Ga
}

function getSectionThresholdValue(
sheetId: UID,
threshold: SectionThreshold,
minValue: number,
maxValue: number
maxValue: number,
getters: Getters
): number | undefined {
if (threshold.value === "" || isNaN(Number(threshold.value))) {
const numberValue = getFormulaNumberValue(sheetId, threshold.value, getters);
if (numberValue === undefined) {
return undefined;
}
const numberValue = Number(threshold.value);
const value =
threshold.type === "number"
? numberValue
: minValue + ((maxValue - minValue) * numberValue) / 100;
return clip(value, minValue, maxValue);
}

function getFormulaNumberValue(sheetId: UID, formula: string, getters: Getters) {
const value = getters.evaluateFormula(sheetId, formula);
return isMatrix(value) ? undefined : tryToNumber(value, getters.getLocale());
}

function getInvalidGaugeRuntime(chart: GaugeChart, getters: Getters): GaugeChartRuntime {
return {
background: getters.getStyleOfSingleCellChart(chart.background, chart.dataRange).background,
title: chart.title ?? { text: "" },
minValue: { value: 0, label: "" },
maxValue: { value: 100, label: "" },
gaugeValue: { value: 0, label: CellErrorType.GenericError },
inflectionValues: [],
colors: [],
};
}
Loading

0 comments on commit f76c9cb

Please sign in to comment.