diff --git a/packages/o-spreadsheet-engine/src/types/chart/chart.ts b/packages/o-spreadsheet-engine/src/types/chart/chart.ts index 028063eff2..4412a3932b 100644 --- a/packages/o-spreadsheet-engine/src/types/chart/chart.ts +++ b/packages/o-spreadsheet-engine/src/types/chart/chart.ts @@ -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; } export interface AxesDesign { 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..6d76d5fcf7 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/src/components/figures/chart/chartJs/chartjs.ts b/src/components/figures/chart/chartJs/chartjs.ts index cc0d47eb24..04ed3d11be 100644 --- a/src/components/figures/chart/chartJs/chartjs.ts +++ b/src/components/figures/chart/chartJs/chartjs.ts @@ -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"; @@ -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), diff --git a/src/components/figures/chart/chartJs/chartjs_minor_grid_plugin.ts b/src/components/figures/chart/chartJs/chartjs_minor_grid_plugin.ts new file mode 100644 index 0000000000..38bda0dd6b --- /dev/null +++ b/src/components/figures/chart/chartJs/chartjs_minor_grid_plugin.ts @@ -0,0 +1,69 @@ +import { Chart } from "chart.js"; +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(); + } + }, +}; diff --git a/src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.ts b/src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.ts index bf4ca7ba2c..49aa4bcea8 100644 --- a/src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.ts +++ b/src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.ts @@ -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"; @@ -21,7 +32,7 @@ interface Props { export class AxisDesignEditor extends Component { 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" }); @@ -42,7 +53,7 @@ export class AxisDesignEditor extends Component { getAxisTitle() { const axesDesign = this.props.definition.axesDesign ?? {}; - return axesDesign[this.state.currentAxis]?.title.text || ""; + return axesDesign[this.state.currentAxis]?.title?.text || ""; } updateAxisTitle(text: string) { @@ -65,4 +76,193 @@ export class AxisDesignEditor extends Component { }; 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); + 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; + } } diff --git a/src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.xml b/src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.xml index 6692bdf993..116606d8a1 100644 --- a/src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.xml +++ b/src/components/side_panel/chart/building_blocks/axis_design/axis_design_editor.xml @@ -16,5 +16,62 @@ style="axisTitleStyle" defaultStyle="{align: 'center', color: '', fontSize: defaultFontSize}" /> +
+
+
+ Minimum + +
+
+ Maximum + +
+
+
+
+ +
+
+ + +
diff --git a/src/components/side_panel/chart/index.ts b/src/components/side_panel/chart/index.ts index 35b2c735f7..e56cc7e6e0 100644 --- a/src/components/side_panel/chart/index.ts +++ b/src/components/side_panel/chart/index.ts @@ -46,7 +46,7 @@ chartSidePanelComponentRegistry }) .add("scatter", { configuration: ScatterConfigPanel, - design: GenericZoomableChartDesignPanel, + design: ChartWithAxisDesignPanel, }) .add("bar", { configuration: BarConfigPanel, diff --git a/src/components/spreadsheet/spreadsheet.css b/src/components/spreadsheet/spreadsheet.css index 6b8b29f61e..9bac27099e 100644 --- a/src/components/spreadsheet/spreadsheet.css +++ b/src/components/spreadsheet/spreadsheet.css @@ -125,6 +125,7 @@ } .o-input[type="number"], + .o-input[type="date"], .o-number-input { border-width: 0 0 1px 0; /* Remove number input arrows */ diff --git a/src/helpers/chart_date.ts b/src/helpers/chart_date.ts index e22c2097bf..3b314ee756 100644 --- a/src/helpers/chart_date.ts +++ b/src/helpers/chart_date.ts @@ -1,6 +1,7 @@ import type { TimeScaleOptions } from "chart.js"; import { DeepPartial } from "chart.js/dist/types/utils"; -import { largeMax, largeMin, parseDateTime } from "."; +import { DateTime } from "luxon"; +import { largeMax, largeMin } from "."; import { Alias, Format, Locale } from "../types"; // ----------------------------------------------------------------------------- @@ -44,7 +45,7 @@ const Milliseconds = { * Regex to test if a format string is a date format that can be translated into a luxon time format */ export const timeFormatLuxonCompatible = - /^((d|dd|m|mm|yyyy|yy|hh|h|ss|a)(-|:|\s|\/))*(d|dd|m|mm|yyyy|yy|hh|h|ss|a)$/i; + /^((d|dd|m|mm|mmm|yyyy|yy|hh|h|ss|a)(-|:|\s|\/))*(d|dd|m|mm|mmm|yyyy|yy|hh|h|ss|a)$/i; /** Get the time options for the XAxis of ChartJS */ export function getChartTimeOptions( @@ -122,11 +123,11 @@ function getBestTimeUnitForScale( format: LuxonFormat, locale: Locale ): TimeUnit | undefined { - const labelDates = labels.map((label) => parseDateTime(label, locale)?.jsDate); + const labelDates = labels.map((label) => DateTime.fromFormat(label, format)); if (labelDates.some((date) => date === undefined) || labels.length < 2) { return undefined; } - const labelsTimestamps = labelDates.map((date) => date!.getTime()); + const labelsTimestamps = labelDates.map((date) => date!.ts); const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps); const minUnit = getFormatMinDisplayUnit(format); diff --git a/src/helpers/figures/charts/runtime/chartjs_scales.ts b/src/helpers/figures/charts/runtime/chartjs_scales.ts index 76d342d402..1a524281ec 100644 --- a/src/helpers/figures/charts/runtime/chartjs_scales.ts +++ b/src/helpers/figures/charts/runtime/chartjs_scales.ts @@ -3,9 +3,10 @@ import { CHART_PADDING, CHART_PADDING_BOTTOM, CHART_PADDING_TOP, + GRAY_200, GRAY_300, } from "@odoo/o-spreadsheet-engine/constants"; -import { getColorScale } from "@odoo/o-spreadsheet-engine/helpers/color"; +import { getColorScale, relativeLuminance } from "@odoo/o-spreadsheet-engine/helpers/color"; import { MOVING_AVERAGE_TREND_LINE_XAXIS_ID, TREND_LINE_XAXIS_ID, @@ -17,6 +18,7 @@ import { import { formatValue, humanizeNumber } from "@odoo/o-spreadsheet-engine/helpers/format/format"; import { AxisDesign, + AxisType, BarChartDefinition, ChartRuntimeGenerationArgs, ChartWithAxisDefinition, @@ -52,17 +54,20 @@ export function getBarChartScales( args: ChartRuntimeGenerationArgs ): ChartScales { let scales: ChartScales = {}; - const { trendDataSetsValues: trendDatasets, locale, axisFormats } = args; + const { trendDataSetsValues: trendDatasets, locale, axisFormats, axisType } = args; const options = { stacked: definition.stacked, locale: locale }; if (definition.horizontal) { - scales.x = getChartAxis(definition, "bottom", "values", { ...options, format: axisFormats?.x }); - scales.y = getChartAxis(definition, "left", "labels", options); + scales.x = getChartAxis(definition, "bottom", "values", axisType, { + ...options, + format: axisFormats?.x, + }); + scales.y = getChartAxis(definition, "left", "labels", "linear", options); } else { - scales.x = getChartAxis(definition, "bottom", "labels", options); + scales.x = getChartAxis(definition, "bottom", "labels", axisType, options); const leftAxisOptions = { ...options, format: axisFormats?.y }; - scales.y = getChartAxis(definition, "left", "values", leftAxisOptions); + scales.y = getChartAxis(definition, "left", "values", "linear", leftAxisOptions); const rightAxisOptions = { ...options, format: axisFormats?.y1 }; - scales.y1 = getChartAxis(definition, "right", "values", rightAxisOptions); + scales.y1 = getChartAxis(definition, "right", "values", "linear", rightAxisOptions); } scales = removeFalsyAttributes(scales); @@ -96,9 +101,17 @@ export function getLineChartScales( const stacked = definition.stacked; let scales: ChartScales = { - x: getChartAxis(definition, "bottom", "labels", { locale }), - y: getChartAxis(definition, "left", "values", { locale, stacked, format: axisFormats?.y }), - y1: getChartAxis(definition, "right", "values", { locale, stacked, format: axisFormats?.y1 }), + x: getChartAxis(definition, "bottom", "labels", axisType, { locale }), + y: getChartAxis(definition, "left", "values", "linear", { + locale, + stacked, + format: axisFormats?.y, + }), + y1: getChartAxis(definition, "right", "values", "linear", { + locale, + stacked, + format: axisFormats?.y1, + }), }; scales = removeFalsyAttributes(scales); @@ -111,7 +124,8 @@ export function getLineChartScales( scales!.x!.ticks!.maxTicksLimit = 15; delete scales?.x?.ticks?.callback; } else if (axisType === "linear") { - scales!.x!.type = "linear"; + scales!.x!.type = + definition.axesDesign?.x?.scaleType === "logarithmic" ? "logarithmic" : "linear"; scales!.x!.ticks!.callback = definition.humanize ? (value) => humanizeNumber({ value, format: labelFormat }, locale) : (value) => formatValue(value, { format: labelFormat, locale }); @@ -145,16 +159,27 @@ export function getLineChartScales( return scales; } +function getGridColor(background?: string) { + return relativeLuminance(background || "#ffffff") > 0.5 ? GRAY_200 : "#383838ff"; +} + export function getScatterChartScales( definition: GenericDefinition, args: ChartRuntimeGenerationArgs ) { const lineScales = getLineChartScales(definition, args); + const xScale = lineScales?.x ?? {}; + const axisDesign = definition.axesDesign?.x; + const scatterGridDisplay = axisDesign?.grid?.major ?? true; return { ...lineScales, x: { - ...lineScales!.x, - grid: { display: true }, + ...xScale, + grid: { + ...xScale.grid, + color: getGridColor(definition.background), + display: scatterGridDisplay, + }, }, }; } @@ -163,12 +188,12 @@ export function getWaterfallChartScales( definition: WaterfallChartDefinition, args: ChartRuntimeGenerationArgs ): ChartScales { - const { locale, axisFormats } = args; + const { locale, axisFormats, axisType } = args; const format = axisFormats?.y || axisFormats?.y1; definition.dataSets; const scales: ChartScales = { x: { - ...getChartAxis(definition, "bottom", "labels", { locale }), + ...getChartAxis(definition, "bottom", "labels", axisType, { locale }), grid: { display: false }, }, y: { @@ -181,6 +206,7 @@ export function getWaterfallChartScales( color: chartFontColor(definition.background), callback: formatTickValue({ locale, format }, definition.humanize), }, + //TODO: handle grid here grid: { lineWidth: (context) => (context.tick.value === 0 ? 2 : 1), }, @@ -353,6 +379,7 @@ function getChartAxis( definition: GenericDefinition, position: "left" | "right" | "bottom", type: "values" | "labels", + axisType: AxisType | undefined, options: LocaleFormat & { stacked?: boolean } ): DeepPartial | undefined { const { useLeftAxis, useRightAxis } = getDefinedAxis(definition); @@ -372,22 +399,45 @@ function getChartAxis( if (type === "values") { const displayGridLines = !(position === "right" && useLeftAxis); + const majorGridEnabled = design?.grid?.major ?? displayGridLines; + const isCategoricalAxis = axisType === "category" || axisType === undefined; + const minorGridEnabled = !isCategoricalAxis && (design?.grid?.minor ?? false); + const isLogarithmic = design?.scaleType === "logarithmic"; - return { + const scale: DeepPartial = { position: position, title: getChartAxisTitleRuntime(design), - grid: { - display: displayGridLines, - }, - beginAtZero: true, + beginAtZero: isLogarithmic ? false : design?.min === undefined, stacked: options?.stacked, ticks: { color: fontColor, callback: formatTickValue(options, definition.humanize), }, }; + if (majorGridEnabled !== undefined || minorGridEnabled !== undefined) { + scale.grid = { + display: majorGridEnabled, + color: getGridColor(definition.background), + }; + } + if (minorGridEnabled) { + scale.grid!["minor"] = { display: true }; + } + if (design?.min !== undefined) { + scale["min"] = design.min; + } + if (design?.max !== undefined) { + scale["max"] = design.max; + } + if (isLogarithmic) { + scale["type"] = "logarithmic"; + } + return scale; } else { - return { + const majorGridEnabled = design?.grid?.major ?? false; + const minorGridEnabled = design?.grid?.minor ?? false; + + const scale: DeepPartial = { ticks: { padding: 5, color: fontColor, @@ -398,11 +448,22 @@ function getChartAxis( }, }, grid: { - display: false, + display: majorGridEnabled, + color: getGridColor(definition.background), }, stacked: options?.stacked, title: getChartAxisTitleRuntime(design), }; + if (minorGridEnabled) { + scale.grid!["minor"] = { display: true }; + } + if (design?.min !== undefined) { + scale["min"] = design.min; + } + if (design?.max !== undefined) { + scale["max"] = design.max; + } + return scale; } } diff --git a/src/helpers/figures/charts/scatter_chart.ts b/src/helpers/figures/charts/scatter_chart.ts index c299fdcd9e..1cffdeb017 100644 --- a/src/helpers/figures/charts/scatter_chart.ts +++ b/src/helpers/figures/charts/scatter_chart.ts @@ -84,7 +84,6 @@ export class ScatterChart extends AbstractChart { this.dataSetDesign = definition.dataSets; this.axesDesign = definition.axesDesign; this.showValues = definition.showValues; - this.zoomable = definition.zoomable; } static validateChartDefinition( @@ -115,7 +114,6 @@ export class ScatterChart extends AbstractChart { aggregated: context.aggregated ?? false, axesDesign: context.axesDesign, showValues: context.showValues, - zoomable: context.zoomable, humanize: context.humanize, }; } @@ -150,7 +148,6 @@ export class ScatterChart extends AbstractChart { aggregated: this.aggregated, axesDesign: this.axesDesign, showValues: this.showValues, - zoomable: this.zoomable, humanize: this.humanize, }; } diff --git a/src/registries/chart_component_registry.ts b/src/registries/chart_component_registry.ts index f5ab39165c..e6b3a0cc32 100644 --- a/src/registries/chart_component_registry.ts +++ b/src/registries/chart_component_registry.ts @@ -12,7 +12,7 @@ chartComponentRegistry.add("bar", ZoomableChartJsComponent); chartComponentRegistry.add("combo", ZoomableChartJsComponent); chartComponentRegistry.add("pie", ChartJsComponent); chartComponentRegistry.add("gauge", GaugeChartComponent); -chartComponentRegistry.add("scatter", ZoomableChartJsComponent); +chartComponentRegistry.add("scatter", ChartJsComponent); chartComponentRegistry.add("scorecard", ScorecardChartComponent); chartComponentRegistry.add("waterfall", ZoomableChartJsComponent); chartComponentRegistry.add("pyramid", ChartJsComponent); diff --git a/tests/figures/chart/__snapshots__/chart_plugin.test.ts.snap b/tests/figures/chart/__snapshots__/chart_plugin.test.ts.snap index 9d96562cc9..f1a65eb562 100644 --- a/tests/figures/chart/__snapshots__/chart_plugin.test.ts.snap +++ b/tests/figures/chart/__snapshots__/chart_plugin.test.ts.snap @@ -113,6 +113,7 @@ exports[`Linear/Time charts font color is white with a dark background color 1`] "scales": { "x": { "grid": { + "color": "#383838ff", "display": false, }, "stacked": undefined, @@ -127,6 +128,7 @@ exports[`Linear/Time charts font color is white with a dark background color 1`] "y": { "beginAtZero": true, "grid": { + "color": "#383838ff", "display": true, }, "position": "left", @@ -223,6 +225,7 @@ exports[`Linear/Time charts font color is white with a dark background color 1`] "scales": { "x": { "grid": { + "color": "#383838ff", "display": false, }, "stacked": undefined, @@ -241,6 +244,7 @@ exports[`Linear/Time charts font color is white with a dark background color 1`] "beginAtZero": true, "display": false, "grid": { + "color": "#383838ff", "display": true, }, "position": "left", @@ -379,6 +383,7 @@ exports[`Linear/Time charts snapshot test of chartJS configuration for date char "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -401,6 +406,7 @@ exports[`Linear/Time charts snapshot test of chartJS configuration for date char "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -502,6 +508,7 @@ exports[`Linear/Time charts snapshot test of chartJS configuration for date char "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -529,6 +536,7 @@ exports[`Linear/Time charts snapshot test of chartJS configuration for date char "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -667,6 +675,7 @@ exports[`Linear/Time charts snapshot test of chartJS configuration for linear ch "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -681,6 +690,7 @@ exports[`Linear/Time charts snapshot test of chartJS configuration for linear ch "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -782,6 +792,7 @@ exports[`Linear/Time charts snapshot test of chartJS configuration for linear ch "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -800,6 +811,7 @@ exports[`Linear/Time charts snapshot test of chartJS configuration for linear ch "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -919,6 +931,7 @@ exports[`datasource tests create a chart with stacked bar 1`] = ` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": true, @@ -932,6 +945,7 @@ exports[`datasource tests create a chart with stacked bar 1`] = ` "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -1015,6 +1029,7 @@ exports[`datasource tests create a chart with stacked bar 1`] = ` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": true, @@ -1032,6 +1047,7 @@ exports[`datasource tests create a chart with stacked bar 1`] = ` "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -1152,6 +1168,7 @@ exports[`datasource tests create chart with a dataset of one cell (no title) 1`] "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -1165,6 +1182,7 @@ exports[`datasource tests create chart with a dataset of one cell (no title) 1`] "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -1248,6 +1266,7 @@ exports[`datasource tests create chart with a dataset of one cell (no title) 1`] "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -1265,6 +1284,7 @@ exports[`datasource tests create chart with a dataset of one cell (no title) 1`] "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -1405,6 +1425,7 @@ exports[`datasource tests create chart with column datasets 1`] = ` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -1418,6 +1439,7 @@ exports[`datasource tests create chart with column datasets 1`] = ` "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -1522,6 +1544,7 @@ exports[`datasource tests create chart with column datasets 1`] = ` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -1539,6 +1562,7 @@ exports[`datasource tests create chart with column datasets 1`] = ` "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -1679,6 +1703,7 @@ exports[`datasource tests create chart with column datasets with category title "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -1692,6 +1717,7 @@ exports[`datasource tests create chart with column datasets with category title "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -1796,6 +1822,7 @@ exports[`datasource tests create chart with column datasets with category title "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -1813,6 +1840,7 @@ exports[`datasource tests create chart with column datasets with category title "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -1953,6 +1981,7 @@ exports[`datasource tests create chart with column datasets without series title "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -1966,6 +1995,7 @@ exports[`datasource tests create chart with column datasets without series title "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -2070,6 +2100,7 @@ exports[`datasource tests create chart with column datasets without series title "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -2087,6 +2118,7 @@ exports[`datasource tests create chart with column datasets without series title "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -2209,6 +2241,7 @@ exports[`datasource tests create chart with only the dataset title (no data) 1`] "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -2222,6 +2255,7 @@ exports[`datasource tests create chart with only the dataset title (no data) 1`] "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -2307,6 +2341,7 @@ exports[`datasource tests create chart with only the dataset title (no data) 1`] "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -2324,6 +2359,7 @@ exports[`datasource tests create chart with only the dataset title (no data) 1`] "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -2464,6 +2500,7 @@ exports[`datasource tests create chart with rectangle dataset 1`] = ` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -2477,6 +2514,7 @@ exports[`datasource tests create chart with rectangle dataset 1`] = ` "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -2581,6 +2619,7 @@ exports[`datasource tests create chart with rectangle dataset 1`] = ` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -2598,6 +2637,7 @@ exports[`datasource tests create chart with rectangle dataset 1`] = ` "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -2738,6 +2778,7 @@ exports[`datasource tests create chart with row datasets 1`] = ` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -2751,6 +2792,7 @@ exports[`datasource tests create chart with row datasets 1`] = ` "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -2855,6 +2897,7 @@ exports[`datasource tests create chart with row datasets 1`] = ` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -2872,6 +2915,7 @@ exports[`datasource tests create chart with row datasets 1`] = ` "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -3012,6 +3056,7 @@ exports[`datasource tests create chart with row datasets with category title 1`] "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -3025,6 +3070,7 @@ exports[`datasource tests create chart with row datasets with category title 1`] "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -3129,6 +3175,7 @@ exports[`datasource tests create chart with row datasets with category title 1`] "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -3146,6 +3193,7 @@ exports[`datasource tests create chart with row datasets with category title 1`] "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -3286,6 +3334,7 @@ exports[`datasource tests create chart with row datasets without series title 1` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -3299,6 +3348,7 @@ exports[`datasource tests create chart with row datasets without series title 1` "y": { "beginAtZero": true, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", @@ -3403,6 +3453,7 @@ exports[`datasource tests create chart with row datasets without series title 1` "scales": { "x": { "grid": { + "color": "#E7E9ED", "display": false, }, "stacked": undefined, @@ -3420,6 +3471,7 @@ exports[`datasource tests create chart with row datasets without series title 1` "beginAtZero": true, "display": false, "grid": { + "color": "#E7E9ED", "display": true, }, "position": "left", diff --git a/tests/figures/chart/chart_plugin.test.ts b/tests/figures/chart/chart_plugin.test.ts index f211e249d4..0d237b6d5c 100644 --- a/tests/figures/chart/chart_plugin.test.ts +++ b/tests/figures/chart/chart_plugin.test.ts @@ -1492,6 +1492,181 @@ describe("title", function () { expect(scales.x!["title"].font.style).toEqual("italic"); } ); + + test("line chart runtime reflects axis bounds, grids and scale type", () => { + setGrid(model, { + A1: "Month", + A2: "Jan", + A3: "Feb", + A4: "Mar", + B1: "Series A", + B2: "5", + B3: "15", + B4: "25", + }); + + createChart( + model, + { + type: "line", + labelRange: "A2:A4", + dataSets: [{ dataRange: "B2:B4" }], + }, + "line-axis" + ); + + updateChart(model, "line-axis", { + axesDesign: { + x: { min: 0, max: 2, grid: { major: true, minor: true } }, + y: { min: 5, max: 25, scaleType: "logarithmic", grid: { major: false, minor: true } }, + }, + }); + + const scales = getChartConfiguration(model, "line-axis").options?.scales; + expect(scales.x?.min).toBe(0); + expect(scales.x?.max).toBe(2); + expect(scales.x?.grid?.display).toBe(true); + expect(scales.x?.grid?.minor?.display).toBe(true); + expect(scales.y?.min).toBe(5); + expect(scales.y?.max).toBe(25); + expect(scales.y?.type).toBe("logarithmic"); + expect(scales.y?.grid?.display).toBe(false); + expect(scales.y?.grid?.minor?.display).toBe(true); + }); + + test("bar chart runtime reflects axis bounds and grids", () => { + setGrid(model, { + A1: "Month", + A2: "Jan", + A3: "Feb", + A4: "Mar", + B1: "Series A", + B2: "5", + B3: "15", + B4: "25", + }); + + createChart( + model, + { + type: "bar", + labelRange: "A2:A4", + dataSets: [{ dataRange: "B2:B4" }], + }, + "bar-axis" + ); + + updateChart(model, "bar-axis", { + axesDesign: { + x: { min: 0, max: 2, grid: { major: true, minor: true } }, + y: { min: 0, max: 30, grid: { major: false, minor: true } }, + }, + }); + + const scales = getChartConfiguration(model, "bar-axis").options?.scales; + expect(scales.x?.min).toBe(0); + expect(scales.x?.max).toBe(2); + expect(scales.x?.grid?.display).toBe(true); + expect(scales.x?.grid?.minor?.display).toBe(true); + expect(scales.y?.min).toBe(0); + expect(scales.y?.max).toBe(30); + expect(scales.y?.grid?.display).toBe(false); + expect(scales.y?.grid?.minor?.display).toBe(true); + }); + + test("combo chart runtime reflects axis bounds on both vertical axes", () => { + setGrid(model, { + A1: "Month", + A2: "Jan", + A3: "Feb", + A4: "Mar", + B1: "Series A", + B2: "5", + B3: "15", + B4: "25", + C1: "Series B", + C2: "50", + C3: "60", + C4: "70", + }); + + createChart( + model, + { + type: "combo", + labelRange: "A2:A4", + dataSets: [ + { dataRange: "B2:B4", yAxisId: "y" }, + { dataRange: "C2:C4", yAxisId: "y1" }, + ], + dataSetsHaveTitle: false, + }, + "combo-axis" + ); + + updateChart(model, "combo-axis", { + axesDesign: { + x: { min: 0, max: 2, grid: { major: true, minor: true } }, + y: { min: 5, max: 30, grid: { major: false, minor: true } }, + y1: { min: 40, max: 80, scaleType: "logarithmic", grid: { major: true, minor: true } }, + }, + }); + + const scales = getChartConfiguration(model, "combo-axis").options?.scales; + expect(scales.x?.min).toBe(0); + expect(scales.x?.grid?.minor?.display).toBe(true); + expect(scales.y?.min).toBe(5); + expect(scales.y?.max).toBe(30); + expect(scales.y?.grid?.display).toBe(false); + expect(scales.y?.grid?.minor?.display).toBe(true); + expect(scales.y1?.min).toBe(40); + expect(scales.y1?.max).toBe(80); + expect(scales.y1?.type).toBe("logarithmic"); + expect(scales.y1?.grid?.display).toBe(true); + expect(scales.y1?.grid?.minor?.display).toBe(true); + }); + + test("scatter chart runtime reflects axis bounds, scale type and grids", () => { + setGrid(model, { + A1: "x", + A2: "1", + A3: "2", + A4: "3", + B1: "Series A", + B2: "5", + B3: "15", + B4: "25", + }); + + createChart( + model, + { + type: "scatter", + labelRange: "A2:A4", + dataSets: [{ dataRange: "B2:B4", yAxisId: "y" }], + labelsAsText: false, + }, + "scatter-axis" + ); + + updateChart(model, "scatter-axis", { + axesDesign: { + x: { min: 1, max: 3, scaleType: "logarithmic", grid: { major: false, minor: true } }, + y: { min: -10, max: 10, grid: { major: true, minor: true } }, + }, + }); + + const scales = getChartConfiguration(model, "scatter-axis").options?.scales; + expect(scales.x?.min).toBe(1); + expect(scales.x?.max).toBe(3); + expect(scales.x?.type).toBe("logarithmic"); + expect(scales.x?.grid?.display).toBe(false); + expect(scales.x?.grid?.minor?.display).toBe(true); + expect(scales.y?.min).toBe(-10); + expect(scales.y?.max).toBe(10); + expect(scales.y?.grid?.display).toBe(true); + expect(scales.y?.grid?.minor?.display).toBe(true); + }); }); describe("multiple sheets", function () { @@ -2288,7 +2463,6 @@ describe("Chart design configuration", () => { }); expect(config.options?.scales?.y1).toMatchObject({ position: "right", - grid: { display: false }, }); updateChart(model, "43", { dataSets: [ @@ -2360,6 +2534,34 @@ describe("Chart design configuration", () => { expect(config.options?.scales?.y?.grid?.display).toBe(true); }); + test.each([ + ["line", true, "45"], + ["scatter", false, "46"], + ] as const)( + "%s chart allows toggling horizontal gridlines without a title", + (chartType, majorEnabled, chartId) => { + setCellContent(model, "A1", "1"); + setCellContent(model, "A2", "2"); + + createChart( + model, + { + dataSets: [{ dataRange: "A1:A2", yAxisId: "y" }], + type: chartType, + }, + chartId + ); + + updateChart(model, chartId, { + axesDesign: { x: { grid: { major: majorEnabled, minor: true } } }, + }); + + const config = getChartConfiguration(model, chartId); + expect(config.options?.scales?.x?.grid?.display).toBe(majorEnabled); + expect(config.options?.scales?.x?.grid?.minor?.display).toBe(true); + } + ); + test("horizontal bar chart displays vertical but not horizontal grid lines", () => { setCellContent(model, "A1", "1"); setCellContent(model, "A2", "2"); diff --git a/tests/figures/chart/charts_component.test.ts b/tests/figures/chart/charts_component.test.ts index 30d30ea641..7abccb578d 100644 --- a/tests/figures/chart/charts_component.test.ts +++ b/tests/figures/chart/charts_component.test.ts @@ -54,12 +54,14 @@ import { doubleClick, focusAndKeyDown, keyDown, + setCheckboxValueAndTrigger, setInputValueAndTrigger, simulateClick, triggerMouseEvent, } from "../../test_helpers/dom_helper"; import { getCellContent } from "../../test_helpers/getters_helpers"; import { + createModelFromGrid, mockChart, mockGeoJsonService, mountComponentWithPortalTarget, @@ -114,9 +116,9 @@ async function changeChartType(type: string) { await click(fixture, `.o-chart-type-item[data-id="${type}"]`); } -async function mountChartSidePanel(id = chartId) { +async function mountChartSidePanel(id: UID = chartId, _model: Model = model) { const props = { chartId: id, onCloseSidePanel: () => {} }; - ({ fixture, env } = await mountComponentWithPortalTarget(ChartPanel, { props, model })); + ({ fixture, env } = await mountComponentWithPortalTarget(ChartPanel, { props, model: _model })); } async function mountSpreadsheet(partialEnv?: Partial) { @@ -647,6 +649,284 @@ describe("charts", () => { }); }); + test.each(["min", "max"])( + "can edit chart horizontal axis %s limit", + async (boundarie: string) => { + const model = createModelFromGrid({ + A1: "0", + B1: "1", + A2: "1", + B2: "2", + A3: "3", + B3: "3", + A4: "2", + B4: "4", + }); + createChart( + model, + { + dataSets: [{ dataRange: "B1:B4" }], + labelRange: "A2:A4", + type: "line", + }, + chartId + ); + await mountChartSidePanel(chartId, model); + await openChartDesignSidePanel(model, env, fixture, chartId); + + const input = fixture.querySelector( + `[data-testid="axis-${boundarie}-input"]` + ) as HTMLInputElement; + await setInputValueAndTrigger(input, "5"); + + const definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.x?.[boundarie]).toEqual(5); + } + ); + + test.each(["min", "max"])( + "Axis %s limit are hidden for categorical horizontal axis", + async (boundarie: string) => { + const model = createModelFromGrid({ + A1: "A", + B1: "1", + A2: "B", + B2: "2", + A3: "C", + B3: "3", + A4: "D", + B4: "4", + }); + createChart( + model, + { + dataSets: [{ dataRange: "B1:B4" }], + labelRange: "A2:A4", + type: "line", + }, + chartId + ); + await mountChartSidePanel(chartId, model); + await openChartDesignSidePanel(model, env, fixture, chartId); + + const element = fixture.querySelector(`[data-testid="axis-${boundarie}-input"]`); + expect(element).toBeNull(); + } + ); + + test("can edit chart time axis limits", async () => { + const model = createModelFromGrid({ + A2: "=DATE(2022,1,1)", + A3: "=DATE(2022,1,2)", + A4: "=DATE(2022,1,3)", + A5: "=DATE(2022,1,4)", + }); + setFormat(model, "A2:A5", "m/d/yyyy"); + createChart( + model, + { + dataSets: [{ dataRange: "B2:B5" }], + labelRange: "A2:A5", + type: "line", + labelsAsText: false, + }, + chartId + ); + await mountChartSidePanel(chartId, model); + await openChartDesignSidePanel(model, env, fixture, chartId); + + const minInput = fixture.querySelector('[data-testid="axis-min-input"]') as HTMLInputElement; + expect(minInput.type).toBe("date"); + + await setInputValueAndTrigger(minInput, "2022-01-02"); + let definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.x?.min).toEqual(Date.parse("2022-01-02")); + + const maxInput = fixture.querySelector('[data-testid="axis-max-input"]') as HTMLInputElement; + await setInputValueAndTrigger(maxInput, "2022-01-04"); + definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.x?.max).toEqual(Date.parse("2022-01-04")); + }); + + test("Axis scale type is not editable for time axis", async () => { + const model = createModelFromGrid({ + A2: "=DATE(2022,1,1)", + A3: "=DATE(2022,1,2)", + A4: "=DATE(2022,1,3)", + A5: "=DATE(2022,1,4)", + }); + setFormat(model, "A2:A5", "m/d/yyyy"); + createChart( + model, + { + dataSets: [{ dataRange: "B2:B5" }], + labelRange: "A2:A5", + type: "line", + labelsAsText: false, + }, + chartId + ); + await mountChartSidePanel(chartId, model); + await openChartDesignSidePanel(model, env, fixture, chartId); + + const scaleSelect = fixture.querySelector('[data-testid="axis-scale-select"]'); + expect(scaleSelect).toBeNull(); + }); + + test.each(["min", "max"])("can edit chart vertical axis %s limit", async (boundarie: string) => { + createChart( + model, + { + dataSets: [{ dataRange: "C1:C4" }], + labelRange: "A2:A4", + type: "line", + }, + chartId + ); + await mountChartSidePanel(); + await openChartDesignSidePanel(model, env, fixture, chartId); + await click(fixture, ".o-badge-selection button[data-id=y]"); + + const input = fixture.querySelector( + `[data-testid="axis-${boundarie}-input"]` + ) as HTMLInputElement; + await setInputValueAndTrigger(input, "5"); + + const definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.y?.[boundarie]).toEqual(5); + }); + + test.each(["x", "y"] as const)("can edit chart %s axis scale type", async (axis: string) => { + const model = createModelFromGrid({ + A1: "0", + B1: "1", + A2: "1", + B2: "2", + A3: "3", + B3: "3", + A4: "2", + B4: "4", + }); + createChart( + model, + { + dataSets: [{ dataRange: "B1:B4" }], + labelRange: "A2:A4", + type: "line", + }, + chartId + ); + await mountChartSidePanel(chartId, model); + await openChartDesignSidePanel(model, env, fixture, chartId); + await click(fixture, `.o-badge-selection button[data-id=${axis}]`); + await setInputValueAndTrigger('[data-testid="axis-scale-select"]', "logarithmic"); + + const definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.[axis]?.scaleType).toEqual("logarithmic"); + }); + + test("Axis scale type is not editable for categorical axis", async () => { + const model = createModelFromGrid({ + A1: "A", + B1: "1", + A2: "B", + B2: "2", + A3: "C", + B3: "3", + A4: "D", + B4: "4", + }); + createChart( + model, + { + dataSets: [{ dataRange: "B1:B4" }], + labelRange: "A2:A4", + type: "line", + }, + chartId + ); + await mountChartSidePanel(chartId, model); + await openChartDesignSidePanel(model, env, fixture, chartId); + const element = fixture.querySelector('[data-testid="axis-scale-select"]'); + expect(element).toBeNull(); + }); + + test.each(["x", "y"] as const)("can toggle chart %s axis gridlines", async (axis: string) => { + const model = createModelFromGrid({ + A1: "0", + B1: "1", + A2: "1", + B2: "2", + A3: "3", + B3: "3", + A4: "2", + B4: "4", + }); + createChart( + model, + { + dataSets: [{ dataRange: "B1:B4" }], + labelRange: "A2:A4", + type: "line", + axesDesign: { + [axis]: { grid: { major: false, minor: false } }, + }, + }, + chartId + ); + await mountChartSidePanel(chartId, model); + await openChartDesignSidePanel(model, env, fixture, chartId); + await click(fixture, `.o-badge-selection button[data-id=${axis}]`); + let definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.[axis]?.grid).toEqual({ major: false, minor: false }); + setCheckboxValueAndTrigger(".o-axis-grid-major input", true, "change"); + await nextTick(); + definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.[axis]?.grid).toEqual({ major: true, minor: false }); + setCheckboxValueAndTrigger(".o-axis-grid-minor input", true, "change"); + await nextTick(); + definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.[axis]?.grid).toEqual({ + major: true, + minor: true, + }); + }); + + test("can only toggle chart major gridline for categorical axis", async () => { + const model = createModelFromGrid({ + A1: "A", + B1: "1", + A2: "B", + B2: "2", + A3: "C", + B3: "3", + A4: "D", + B4: "4", + }); + createChart( + model, + { + dataSets: [{ dataRange: "B1:B4" }], + labelRange: "A2:A4", + type: "line", + axesDesign: { + x: { grid: { major: false, minor: false } }, + }, + }, + chartId + ); + await mountChartSidePanel(chartId, model); + await openChartDesignSidePanel(model, env, fixture, chartId); + let definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.x?.grid).toEqual({ major: false, minor: false }); + setCheckboxValueAndTrigger(".o-axis-grid-major input", true, "change"); + await nextTick(); + definition = model.getters.getChartDefinition(chartId) as LineChartDefinition; + expect(definition.axesDesign?.x?.grid).toEqual({ major: true, minor: false }); + const element = fixture.querySelector(".o-axis-grid-minor input"); + expect(element).toBeNull(); + }); + test("can edit chart data series color", async () => { createChart( model, @@ -2421,9 +2701,10 @@ test("ChartJS charts extensions are loaded when mounting a spreadsheet, are only const spyUnregister = jest.spyOn(window.Chart, "unregister"); createChart(model, { type: "bar" }, chartId); await mountSpreadsheet(); - expect(spyRegister).toHaveBeenCalledTimes(7); + expect(spyRegister).toHaveBeenCalledTimes(8); expect(window.Chart.registry.plugins["items"].map((i) => i.id)).toMatchObject([ "chartShowValuesPlugin", + "o-spreadsheet-minor-gridlines", "waterfallLinesPlugin", "funnel", // Funnel controller "funnel", // Funnel element @@ -2434,11 +2715,11 @@ test("ChartJS charts extensions are loaded when mounting a spreadsheet, are only createChart(model, { type: "line" }, "chart2"); await nextTick(); - expect(spyRegister).toHaveBeenCalledTimes(7); + expect(spyRegister).toHaveBeenCalledTimes(8); app.destroy(); await nextTick(); - expect(spyUnregister).toHaveBeenCalledTimes(7); + expect(spyUnregister).toHaveBeenCalledTimes(8); expect(window.Chart.registry.plugins["items"]).toEqual([]); });