Skip to content

Commit 5e593d6

Browse files
committed
[IMP] add date picker for date scale axis
This commit extends the axis customization to date type x scale Task: 5159370
1 parent 6c90592 commit 5e593d6

File tree

4 files changed

+143
-29
lines changed

4 files changed

+143
-29
lines changed

src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.ts

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { _t } from "@odoo/o-spreadsheet-engine";
22
import { CHART_AXIS_TITLE_FONT_SIZE } from "@odoo/o-spreadsheet-engine/constants";
3-
import { LineChartRuntime } from "@odoo/o-spreadsheet-engine/types/chart";
3+
import { AxisType, LineChartRuntime } from "@odoo/o-spreadsheet-engine/types/chart";
44
import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env";
55
import { Component, useState } from "@odoo/owl";
66
import { deepCopy } from "../../../../../helpers";
@@ -77,18 +77,28 @@ export class AxisDesignEditor extends Component<Props, SpreadsheetChildEnv> {
7777
this.props.updateChart(this.props.chartId, { axesDesign });
7878
}
7979

80-
get axisMin(): number | undefined {
81-
return this.currentAxisDesign?.min;
80+
get axisMin(): string | number | undefined {
81+
const min = this.currentAxisDesign?.min;
82+
return this.isTimeAxis ? this.formatAxisBoundary(min) : min;
8283
}
8384

84-
get axisMax(): number | undefined {
85-
return this.currentAxisDesign?.max;
85+
get axisMax(): string | number | undefined {
86+
const max = this.currentAxisDesign?.max;
87+
return this.isTimeAxis ? this.formatAxisBoundary(max) : max;
8688
}
8789

8890
get axisScaleType(): AxisScaleType {
8991
return this.currentAxisDesign?.scaleType ?? "linear";
9092
}
9193

94+
get axisBoundsInputType(): "number" | "date" {
95+
return this.isTimeAxis ? "date" : "number";
96+
}
97+
98+
get axisBoundsInputStep(): string | null {
99+
return this.isTimeAxis ? "1" : null;
100+
}
101+
92102
get isMajorGridEnabled(): boolean {
93103
const designValue = this.currentAxisDesign?.grid?.major;
94104
if (designValue !== undefined) {
@@ -114,29 +124,29 @@ export class AxisDesignEditor extends Component<Props, SpreadsheetChildEnv> {
114124
}
115125

116126
updateAxisMin(ev: InputEvent) {
117-
const value = (ev.target as HTMLInputElement).value.trim();
118-
const parsed = value === "" ? undefined : Number(value);
119-
if (parsed === undefined || !isNaN(parsed)) {
120-
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
121-
axesDesign[this.state.currentAxis] = {
122-
...axesDesign[this.state.currentAxis],
123-
min: parsed,
124-
};
125-
this.props.updateChart(this.props.chartId, { axesDesign });
127+
const parsed = this.parseAxisBoundaryValue(ev);
128+
if (parsed === null) {
129+
return;
126130
}
131+
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
132+
axesDesign[this.state.currentAxis] = {
133+
...axesDesign[this.state.currentAxis],
134+
min: parsed,
135+
};
136+
this.props.updateChart(this.props.chartId, { axesDesign });
127137
}
128138

129139
updateAxisMax(ev: InputEvent) {
130-
const value = (ev.target as HTMLInputElement).value.trim();
131-
const parsed = value === "" ? undefined : Number(value);
132-
if (parsed === undefined || !isNaN(parsed)) {
133-
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
134-
axesDesign[this.state.currentAxis] = {
135-
...axesDesign[this.state.currentAxis],
136-
max: parsed,
137-
};
138-
this.props.updateChart(this.props.chartId, { axesDesign });
140+
const parsed = this.parseAxisBoundaryValue(ev);
141+
if (parsed === null) {
142+
return;
139143
}
144+
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
145+
axesDesign[this.state.currentAxis] = {
146+
...axesDesign[this.state.currentAxis],
147+
max: parsed,
148+
};
149+
this.props.updateChart(this.props.chartId, { axesDesign });
140150
}
141151

142152
updateAxisScaleType(ev: InputEvent) {
@@ -198,9 +208,12 @@ export class AxisDesignEditor extends Component<Props, SpreadsheetChildEnv> {
198208
if (this.state.currentAxis !== "x") {
199209
return false;
200210
}
201-
const runtime = this.env.model.getters.getChartRuntime(this.props.chartId) as LineChartRuntime;
202-
const axisType = runtime?.chartJsConfig.options?.scales?.x?.type;
203-
return axisType === undefined || axisType === "time";
211+
const axisType = this.getXAxisType();
212+
return axisType === undefined || axisType === "category";
213+
}
214+
215+
get isTimeAxis(): boolean {
216+
return this.state.currentAxis === "x" && this.getXAxisType() === "time";
204217
}
205218

206219
get canChangeMinorGridVisibility(): boolean {
@@ -213,4 +226,43 @@ export class AxisDesignEditor extends Component<Props, SpreadsheetChildEnv> {
213226
const type = this.props.definition.type;
214227
return type === "line" || type === "scatter";
215228
}
229+
230+
private parseAxisBoundaryValue(ev: InputEvent): number | undefined | null {
231+
const input = ev.target as HTMLInputElement;
232+
const value = input.value.trim();
233+
if (value === "") {
234+
return undefined;
235+
}
236+
if (this.isTimeAxis) {
237+
const timestamp = this.getTimestampFromInput(input);
238+
return Number.isNaN(timestamp) ? null : timestamp;
239+
}
240+
const parsed = Number(value);
241+
return Number.isNaN(parsed) ? null : parsed;
242+
}
243+
244+
private formatAxisBoundary(value: number | string | undefined): string | undefined {
245+
if (value === undefined) {
246+
return undefined;
247+
}
248+
const timestamp = typeof value === "number" ? value : Date.parse(value);
249+
if (Number.isNaN(timestamp)) {
250+
return typeof value === "string" ? value : undefined;
251+
}
252+
const date = new Date(timestamp);
253+
return date.toISOString().split("T")[0];
254+
}
255+
256+
private getTimestampFromInput(input: HTMLInputElement): number {
257+
const valueAsNumber = (input as any).valueAsNumber as number | undefined;
258+
if (typeof valueAsNumber === "number" && !Number.isNaN(valueAsNumber)) {
259+
return valueAsNumber;
260+
}
261+
return Date.parse(input.value);
262+
}
263+
264+
private getXAxisType(): AxisType | undefined {
265+
const runtime = this.env.model.getters.getChartRuntime(this.props.chartId) as LineChartRuntime;
266+
return runtime?.chartJsConfig.options?.scales?.x?.type as AxisType | undefined;
267+
}
216268
}

src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.xml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,29 @@
2424
<div class="w-50">
2525
<span class="o-section-subtitle my-0">Minimum</span>
2626
<input
27-
type="number"
27+
t-att-type="axisBoundsInputType"
2828
class="o-input w-100"
2929
data-testid="axis-min-input"
3030
t-att-value="axisMin"
31+
t-att-step="axisBoundsInputStep"
3132
t-on-change="updateAxisMin"
3233
/>
3334
</div>
3435
<div class="w-50 ms-3">
3536
<span class="o-section-subtitle my-0">Maximum</span>
3637
<input
37-
type="number"
38+
t-att-type="axisBoundsInputType"
3839
class="o-input w-100"
3940
data-testid="axis-max-input"
4041
t-att-value="axisMax"
42+
t-att-step="axisBoundsInputStep"
4143
t-on-change="updateAxisMax"
4244
/>
4345
</div>
4446
</div>
4547
</Section>
4648
<Section
47-
t-if="!isCategoricalAxis"
49+
t-if="!isCategoricalAxis and !isTimeAxis"
4850
class="'pt-0 o-axis-scale-section'"
4951
title.translate="Scale type">
5052
<select

src/components/spreadsheet/spreadsheet.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
}
126126

127127
.o-input[type="number"],
128+
.o-input[type="date"],
128129
.o-number-input {
129130
border-width: 0 0 1px 0;
130131
/* Remove number input arrows */

tests/figures/chart/charts_component.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,65 @@ describe("charts", () => {
714714
}
715715
);
716716

717+
test("can edit chart time axis limits", async () => {
718+
const model = createModelFromGrid({
719+
A2: "=DATE(2022,1,1)",
720+
A3: "=DATE(2022,1,2)",
721+
A4: "=DATE(2022,1,3)",
722+
A5: "=DATE(2022,1,4)",
723+
});
724+
setFormat(model, "A2:A5", "m/d/yyyy");
725+
createChart(
726+
model,
727+
{
728+
dataSets: [{ dataRange: "B2:B5" }],
729+
labelRange: "A2:A5",
730+
type: "line",
731+
labelsAsText: false,
732+
},
733+
chartId
734+
);
735+
await mountChartSidePanel(chartId, model);
736+
await openChartDesignSidePanel(model, env, fixture, chartId);
737+
738+
const minInput = fixture.querySelector('[data-testid="axis-min-input"]') as HTMLInputElement;
739+
expect(minInput.type).toBe("date");
740+
741+
await setInputValueAndTrigger(minInput, "2022-01-02");
742+
let definition = model.getters.getChartDefinition(chartId) as LineChartDefinition;
743+
expect(definition.axesDesign?.x?.min).toEqual(Date.parse("2022-01-02"));
744+
745+
const maxInput = fixture.querySelector('[data-testid="axis-max-input"]') as HTMLInputElement;
746+
await setInputValueAndTrigger(maxInput, "2022-01-04");
747+
definition = model.getters.getChartDefinition(chartId) as LineChartDefinition;
748+
expect(definition.axesDesign?.x?.max).toEqual(Date.parse("2022-01-04"));
749+
});
750+
751+
test("Axis scale type is not editable for time axis", async () => {
752+
const model = createModelFromGrid({
753+
A2: "=DATE(2022,1,1)",
754+
A3: "=DATE(2022,1,2)",
755+
A4: "=DATE(2022,1,3)",
756+
A5: "=DATE(2022,1,4)",
757+
});
758+
setFormat(model, "A2:A5", "m/d/yyyy");
759+
createChart(
760+
model,
761+
{
762+
dataSets: [{ dataRange: "B2:B5" }],
763+
labelRange: "A2:A5",
764+
type: "line",
765+
labelsAsText: false,
766+
},
767+
chartId
768+
);
769+
await mountChartSidePanel(chartId, model);
770+
await openChartDesignSidePanel(model, env, fixture, chartId);
771+
772+
const scaleSelect = fixture.querySelector('[data-testid="axis-scale-select"]');
773+
expect(scaleSelect).toBeNull();
774+
});
775+
717776
test.each(["min", "max"])("can edit chart vertical axis %s limit", async (boundarie: string) => {
718777
createChart(
719778
model,

0 commit comments

Comments
 (0)