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
11 changes: 11 additions & 0 deletions packages/o-spreadsheet-engine/src/types/chart/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,19 @@ export interface DatasetDesign {
readonly label?: string;
}

export type AxisScaleType = "linear" | "logarithmic";

export interface AxisGridDesign {
readonly major?: boolean;
readonly minor?: boolean;
}

export interface AxisDesign {
readonly title?: TitleDesign;
readonly min?: number;
readonly max?: number;
readonly scaleType?: AxisScaleType;
readonly grid?: AxisGridDesign;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: probably more clear to have a boolean showMajor/minorGridLine rather than design.grid.major

}

export interface AxesDesign {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LineChartDefinition, LineChartRuntime } from "./line_chart";

export interface ScatterChartDefinition
extends Omit<LineChartDefinition, "type" | "stacked" | "cumulative"> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh ? scatter cahrts are not zoomable ? I did a fix and all for zoomable scatter charts https://www.odoo.com/odoo/2328/tasks/5143146

Why change that? And why in this task ?

extends Omit<LineChartDefinition, "type" | "stacked" | "cumulative" | "zoomable"> {
readonly type: "scatter";
}

Expand Down
5 changes: 5 additions & 0 deletions src/components/figures/chart/chartJs/chartjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getFunnelChartController,
getFunnelChartElement,
} from "./chartjs_funnel_chart";
import { chartMinorGridPlugin } from "./chartjs_minor_grid_plugin";
import { chartShowValuesPlugin } from "./chartjs_show_values_plugin";
import { sunburstHoverPlugin } from "./chartjs_sunburst_hover_plugin";
import { sunburstLabelsPlugin } from "./chartjs_sunburst_labels_plugin";
Expand All @@ -26,6 +27,10 @@ chartJsExtensionRegistry.add("chartShowValuesPlugin", {
register: (Chart) => Chart.register(chartShowValuesPlugin),
unregister: (Chart) => Chart.unregister(chartShowValuesPlugin),
});
chartJsExtensionRegistry.add("chartMinorGridPlugin", {
register: (Chart) => Chart.register(chartMinorGridPlugin),
unregister: (Chart) => Chart.unregister(chartMinorGridPlugin),
});
chartJsExtensionRegistry.add("waterfallLinesPlugin", {
register: (Chart) => Chart.register(waterfallLinesPlugin),
unregister: (Chart) => Chart.unregister(waterfallLinesPlugin),
Expand Down
69 changes: 69 additions & 0 deletions src/components/figures/chart/chartJs/chartjs_minor_grid_plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Chart } from "chart.js";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are minor grid line really not a default chartjs option ? Idk if it's worth doing a plugin for that, IMO minor grid lines are kinda useless

import { Color } from "../../../..";

interface MinorGridOptions {
display?: boolean;
color?: Color;
}

export const chartMinorGridPlugin = {
id: "o-spreadsheet-minor-gridlines",
beforeDatasetsDraw(chart: Chart) {
const ctx = chart.ctx;
const chartArea = chart.chartArea;
if (!chartArea) {
return;
}

for (const scaleId in chart.scales) {
const scale = chart.scales[scaleId];
const options: any = scale.options;
const minor: MinorGridOptions | undefined = options?.grid?.minor;
if (!minor?.display) {
continue;
}
const ticks = scale.ticks;
if (!ticks || ticks.length < 2) {
continue;
}

ctx.save();
ctx.lineWidth = 1 / 2;
ctx.strokeStyle = minor.color ?? options?.grid?.color ?? "#bdbdbd";
ctx.setLineDash([]);
ctx.globalAlpha = 0.6;

const drawVerticalLine = (x: number) => {
ctx.beginPath();
ctx.moveTo(x, chartArea.top);
ctx.lineTo(x, chartArea.bottom);
ctx.stroke();
};
const drawHorizontalLine = (y: number) => {
ctx.beginPath();
ctx.moveTo(chartArea.left, y);
ctx.lineTo(chartArea.right, y);
ctx.stroke();
};

for (let i = 0; i < ticks.length - 1; i++) {
const start = scale.getPixelForTick(i);
const end = scale.getPixelForTick(i + 1);
if (!isFinite(start) || !isFinite(end)) {
continue;
}
for (let j = 1; j < 4; j++) {
const ratio = j / 4;
const position = start + (end - start) * ratio;
if (scale.isHorizontal()) {
drawVerticalLine(position);
} else {
drawHorizontalLine(position);
}
}
}

ctx.restore();
}
},
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { _t } from "@odoo/o-spreadsheet-engine";
import { CHART_AXIS_TITLE_FONT_SIZE } from "@odoo/o-spreadsheet-engine/constants";
import { AxisType, LineChartRuntime } from "@odoo/o-spreadsheet-engine/types/chart";
import { SpreadsheetChildEnv } from "@odoo/o-spreadsheet-engine/types/spreadsheet_env";
import { Component, useState } from "@odoo/owl";
import { deepCopy } from "../../../../../helpers";
import { ChartWithAxisDefinition, DispatchResult, TitleDesign, UID } from "../../../../../types";
import { getDefinedAxis } from "../../../../../helpers/figures/charts";
import {
AxisDesign,
AxisScaleType,
ChartWithAxisDefinition,
DispatchResult,
TitleDesign,
UID,
} from "../../../../../types";
import { BadgeSelection } from "../../../components/badge_selection/badge_selection";
import { Checkbox } from "../../../components/checkbox/checkbox";
import { Section } from "../../../components/section/section";
import { ChartTitle } from "../chart_title/chart_title";

Expand All @@ -21,7 +32,7 @@ interface Props {

export class AxisDesignEditor extends Component<Props, SpreadsheetChildEnv> {
static template = "o-spreadsheet-AxisDesignEditor";
static components = { Section, ChartTitle, BadgeSelection };
static components = { Section, ChartTitle, BadgeSelection, Checkbox };
static props = { chartId: String, definition: Object, updateChart: Function, axesList: Array };

state = useState({ currentAxis: "x" });
Expand All @@ -42,7 +53,7 @@ export class AxisDesignEditor extends Component<Props, SpreadsheetChildEnv> {

getAxisTitle() {
const axesDesign = this.props.definition.axesDesign ?? {};
return axesDesign[this.state.currentAxis]?.title.text || "";
return axesDesign[this.state.currentAxis]?.title?.text || "";
}

updateAxisTitle(text: string) {
Expand All @@ -65,4 +76,193 @@ export class AxisDesignEditor extends Component<Props, SpreadsheetChildEnv> {
};
this.props.updateChart(this.props.chartId, { axesDesign });
}

get axisMin(): string | number | undefined {
const min = this.currentAxisDesign?.min;
return this.isTimeAxis ? this.formatAxisBoundary(min) : min;
}

get axisMax(): string | number | undefined {
const max = this.currentAxisDesign?.max;
return this.isTimeAxis ? this.formatAxisBoundary(max) : max;
}

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

get axisBoundsInputType(): "number" | "date" {
return this.isTimeAxis ? "date" : "number";
}

get axisBoundsInputStep(): string | null {
return this.isTimeAxis ? "1" : null;
}

get isMajorGridEnabled(): boolean {
const designValue = this.currentAxisDesign?.grid?.major;
if (designValue !== undefined) {
return designValue;
}
return this.getDefaultMajorGridValue(this.state.currentAxis);
}

get isMinorGridEnabled(): boolean {
return !!this.currentAxisDesign?.grid?.minor;
}

get majorGridLabel() {
return this.state.currentAxis === "x"
? _t("Vertical major gridlines")
: _t("Horizontal major gridlines");
}

get minorGridLabel() {
return this.state.currentAxis === "x"
? _t("Vertical minor gridlines")
: _t("Horizontal minor gridlines");
}

updateAxisMin(ev: InputEvent) {
const parsed = this.parseAxisBoundaryValue(ev);
if (parsed === null) {
return;
}
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
axesDesign[this.state.currentAxis] = {
...axesDesign[this.state.currentAxis],
min: parsed,
};
this.props.updateChart(this.props.chartId, { axesDesign });
}

updateAxisMax(ev: InputEvent) {
const parsed = this.parseAxisBoundaryValue(ev);
if (parsed === null) {
return;
}
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
axesDesign[this.state.currentAxis] = {
...axesDesign[this.state.currentAxis],
max: parsed,
};
this.props.updateChart(this.props.chartId, { axesDesign });
}

updateAxisScaleType(ev: InputEvent) {
const type = (ev.target as HTMLSelectElement).value as AxisScaleType;
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
axesDesign[this.state.currentAxis] = {
...axesDesign[this.state.currentAxis],
scaleType: type === "linear" ? undefined : type,
};
this.props.updateChart(this.props.chartId, { axesDesign });
}

toggleMajorGrid(major: boolean) {
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
axesDesign[this.state.currentAxis] = {
...axesDesign[this.state.currentAxis],
grid: {
...axesDesign[this.state.currentAxis]?.grid,
major,
},
};
this.props.updateChart(this.props.chartId, { axesDesign });
}

toggleMinorGrid(minor: boolean) {
const axesDesign = deepCopy(this.props.definition.axesDesign) ?? {};
axesDesign[this.state.currentAxis] = {
...axesDesign[this.state.currentAxis],
grid: {
...axesDesign[this.state.currentAxis]?.grid,
minor,
},
};
this.props.updateChart(this.props.chartId, { axesDesign });
}

private get currentAxisDesign(): AxisDesign | undefined {
return this.props.definition.axesDesign?.[this.state.currentAxis];
}

private getDefaultMajorGridValue(axisId: string): boolean {
const { useLeftAxis, useRightAxis } = getDefinedAxis(this.props.definition);
if (axisId === "x") {
if ("horizontal" in this.props.definition && this.props.definition.horizontal) {
return true;
}
if (this.props.definition.type === "scatter") {
return true;
}
} else if (axisId === "y") {
return true;
} else if (axisId === "y1") {
return !useLeftAxis && useRightAxis;
}
return false;
}

get isCategoricalAxis(): boolean {
if (this.state.currentAxis !== "x") {
return false;
}
const axisType = this.getXAxisType();
return axisType === undefined || axisType === "category";
}

get isTimeAxis(): boolean {
return this.state.currentAxis === "x" && this.getXAxisType() === "time";
}

get canChangeMinorGridVisibility(): boolean {
if (this.state.currentAxis !== "x") {
return true;
}
if (this.isCategoricalAxis) {
return false;
}
const type = this.props.definition.type;
return type === "line" || type === "scatter";
}

private parseAxisBoundaryValue(ev: InputEvent): number | undefined | null {
const input = ev.target as HTMLInputElement;
const value = input.value.trim();
if (value === "") {
return undefined;
}
if (this.isTimeAxis) {
const timestamp = this.getTimestampFromInput(input);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should really convert that to a spreadsheet date number (1 is 1/1/0900) - not a js timestamp. If I add a limit then remove the date format, I get an huge value that doesn't make sense (eg. 1756425600000). If you use spreadsheet date numbers, it would make sense even after removing the format.

return Number.isNaN(timestamp) ? null : timestamp;
}
const parsed = Number(value);
return Number.isNaN(parsed) ? null : parsed;
}

private formatAxisBoundary(value: number | string | undefined): string | undefined {
if (value === undefined) {
return undefined;
}
const timestamp = typeof value === "number" ? value : Date.parse(value);
if (Number.isNaN(timestamp)) {
return typeof value === "string" ? value : undefined;
}
const date = new Date(timestamp);
return date.toISOString().split("T")[0];
}

private getTimestampFromInput(input: HTMLInputElement): number {
const valueAsNumber = (input as any).valueAsNumber as number | undefined;
if (typeof valueAsNumber === "number" && !Number.isNaN(valueAsNumber)) {
return valueAsNumber;
}
return Date.parse(input.value);
}

private getXAxisType(): AxisType | undefined {
const runtime = this.env.model.getters.getChartRuntime(this.props.chartId) as LineChartRuntime;
return runtime?.chartJsConfig.options?.scales?.x?.type as AxisType | undefined;
}
}
Loading