Skip to content

Commit

Permalink
[IMP] chart: multiple Y axis value formatting
Browse files Browse the repository at this point in the history
Some chart support multiple Y axis, but the format of the values in
the ticks/tooltips was the same for both charts.

With this commit, each axis can have a different format, and this
format is applied to the ticks, the tooltips and the "show values"
option.

Task: 4153935
Part-of: #5013
Signed-off-by: Rémi Rahir (rar) <[email protected]>
  • Loading branch information
hokolomopo committed Oct 11, 2024
1 parent 9eae62c commit 398d495
Show file tree
Hide file tree
Showing 16 changed files with 182 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface ChartShowValuesPluginOptions {
showValues: boolean;
background?: Color;
horizontal?: boolean;
callback: (value: number | string) => string;
callback: (value: number | string, axisId?: string) => string;
}

declare module "chart.js" {
Expand Down Expand Up @@ -64,10 +64,14 @@ export const chartShowValuesPlugin: Plugin = {
case "bar":
case "line": {
const yOffset = dataset.type === "bar" && !options.horizontal ? 0 : 3;

const horizontalChart = dataset.type === "bar" && options.horizontal;
const axisId = horizontalChart ? dataset.xAxisID : dataset.yAxisID;

for (let i = 0; i < dataset._parsed.length; i++) {
const point = dataset.data[i];
const value = options.horizontal ? dataset._parsed[i].x : dataset._parsed[i].y;
const displayedValue = options.callback(value - 0);
const displayedValue = options.callback(value - 0, axisId);
let xPosition = 0,
yPosition = 0;
if (options.horizontal) {
Expand Down
38 changes: 21 additions & 17 deletions src/helpers/figures/charts/bar_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
copyDataSetsWithNewSheetId,
copyLabelRangeWithNewSheetId,
createDataSets,
formatTickValue,
formatChartDatasetValue,
getChartAxis,
getChartColorsGenerator,
getDefinedAxis,
Expand Down Expand Up @@ -243,12 +243,16 @@ export function createBarChartRuntime(chart: BarChart, getters: Getters): BarCha
({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
}

const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
const locale = getters.getLocale();
const localeFormat = { format: dataSetFormat, locale };
const fontColor = chartFontColor(chart.background);
const axisFormats = chart.horizontal
? { x: leftAxisFormat || rightAxisFormat }
: { y: leftAxisFormat, y1: rightAxisFormat };
const config = getDefaultChartJsRuntime(chart, labels, fontColor, {
...localeFormat,
locale,
axisFormats,
horizontalChart: chart.horizontal,
});
const legend: DeepPartial<LegendOptions<"bar">> = {
Expand All @@ -270,25 +274,25 @@ export function createBarChartRuntime(chart: BarChart, getters: Getters): BarCha

config.options.scales = {};
const definition = chart.getDefinition();
const stacked = chart.stacked;

const valueAxisOptions = { ...localeFormat, stacked };
const labelAxisOptions = { stacked, locale };
const options = { stacked: chart.stacked, locale };
if (chart.horizontal) {
config.options.scales.x = getChartAxis(definition, "bottom", "values", valueAxisOptions);
config.options.scales.y = getChartAxis(definition, "left", "labels", labelAxisOptions);
const format = leftAxisFormat || rightAxisFormat;
config.options.scales.x = getChartAxis(definition, "bottom", "values", { ...options, format });
config.options.scales.y = getChartAxis(definition, "left", "labels", options);
} else {
config.options.scales.x = getChartAxis(definition, "bottom", "labels", labelAxisOptions);
config.options.scales.y = getChartAxis(definition, "left", "values", valueAxisOptions);
config.options.scales.y1 = getChartAxis(definition, "right", "values", valueAxisOptions);
config.options.scales.x = getChartAxis(definition, "bottom", "labels", options);
const leftAxisOptions = { ...options, format: leftAxisFormat };
config.options.scales.y = getChartAxis(definition, "left", "values", leftAxisOptions);
const rightAxisOptions = { ...options, format: rightAxisFormat };
config.options.scales.y1 = getChartAxis(definition, "right", "values", rightAxisOptions);
}
config.options.scales = removeFalsyAttributes(config.options.scales);

config.options.plugins!.chartShowValuesPlugin = {
showValues: chart.showValues,
background: chart.background,
horizontal: chart.horizontal,
callback: formatTickValue(localeFormat),
callback: formatChartDatasetValue(axisFormats, locale),
};

const colors = getChartColorsGenerator(definition, dataSetsValues.length);
Expand All @@ -309,9 +313,9 @@ export function createBarChartRuntime(chart: BarChart, getters: Getters): BarCha
const label = definition.dataSets[index].label;
dataset.label = label;
}
if (definition.dataSets?.[index]?.yAxisId && !chart.horizontal) {
dataset["yAxisID"] = definition.dataSets[index].yAxisId;
}

dataset.yAxisID = chart.horizontal ? "y" : definition.dataSets[index].yAxisId || "y";
dataset.xAxisID = "x";

const trend = definition.dataSets?.[index].trend;
if (!trend?.display || chart.horizontal) {
Expand Down
9 changes: 9 additions & 0 deletions src/helpers/figures/charts/chart_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DOMCoordinates,
DOMDimension,
Getters,
Locale,
LocaleFormat,
Range,
RemoveColumnsRowsCommand,
Expand All @@ -28,6 +29,7 @@ import {
} from "../../../types";
import {
AxisDesign,
ChartAxisFormats,
ChartWithAxisDefinition,
CustomizedDataSet,
DataSet,
Expand Down Expand Up @@ -614,6 +616,13 @@ export function interpolateData(
}
}

export function formatChartDatasetValue(axisFormats: ChartAxisFormats, locale: Locale) {
return (value: any, axisId: string | undefined) => {
const format = axisId ? axisFormats?.[axisId] : undefined;
return formatTickValue({ format, locale })(value);
};
}

export function formatTickValue(localeFormat: LocaleFormat) {
return (value: any) => {
value = Number(value);
Expand Down
22 changes: 11 additions & 11 deletions src/helpers/figures/charts/chart_common_line_scatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
TREND_LINE_XAXIS_ID,
chartFontColor,
computeChartPadding,
formatTickValue,
formatChartDatasetValue,
getChartAxis,
getChartColorsGenerator,
getFullTrendingLineDataSet,
Expand Down Expand Up @@ -223,8 +223,10 @@ export function createLineOrScatterChartRuntime(

const locale = getters.getLocale();
const truncateLabels = axisType === "category";
const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
const options = { format: dataSetFormat, locale, truncateLabels };
const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat };
const options = { locale, truncateLabels, axisFormats };
const fontColor = chartFontColor(chart.background);
const config = getDefaultChartJsRuntime(chart, labels, fontColor, options);

Expand Down Expand Up @@ -259,15 +261,15 @@ export function createLineOrScatterChartRuntime(
const stacked = "stacked" in chart && chart.stacked;
config.options.scales = {
x: getChartAxis(definition, "bottom", "labels", { locale }),
y: getChartAxis(definition, "left", "values", { ...options, stacked }),
y1: getChartAxis(definition, "right", "values", { ...options, stacked }),
y: getChartAxis(definition, "left", "values", { locale, stacked, format: leftAxisFormat }),
y1: getChartAxis(definition, "right", "values", { locale, stacked, format: rightAxisFormat }),
};
config.options.scales = removeFalsyAttributes(config.options.scales);

config.options.plugins!.chartShowValuesPlugin = {
showValues: chart.showValues,
background: chart.background,
callback: formatTickValue(options),
callback: formatChartDatasetValue(axisFormats, locale),
};

if (
Expand Down Expand Up @@ -298,7 +300,7 @@ export function createLineOrScatterChartRuntime(
label = toNumber(label, locale);
}
const formattedX = formatValue(label, { locale, format: labelFormat });
const formattedY = formatValue(dataSetPoint, { locale, format: dataSetFormat });
const formattedY = formatValue(dataSetPoint, { locale, format: leftAxisFormat });
const dataSetTitle = tooltipItem.dataset.label;
return formattedX
? `${dataSetTitle}: (${formattedX}, ${formattedY})`
Expand Down Expand Up @@ -354,9 +356,7 @@ export function createLineOrScatterChartRuntime(
const label = definition.dataSets[index].label;
dataset.label = label;
}
if (definition.dataSets?.[index]?.yAxisId) {
dataset["yAxisID"] = definition.dataSets[index].yAxisId;
}
dataset["yAxisID"] = definition.dataSets[index].yAxisId || "y";

const trend = definition.dataSets?.[index].trend;
if (!trend?.display) {
Expand Down Expand Up @@ -401,7 +401,7 @@ export function createLineOrScatterChartRuntime(
background: chart.background || BACKGROUND_CHART_COLOR,
dataSetsValues,
labelValues,
dataSetFormat,
dataSetFormat: leftAxisFormat,
labelFormat,
};
}
45 changes: 31 additions & 14 deletions src/helpers/figures/charts/chart_ui_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ import { ChartTerms } from "../../../components/translations_terms";
import { DEFAULT_CHART_FONT_SIZE, DEFAULT_CHART_PADDING, MAX_CHAR_LABEL } from "../../../constants";
import { isEvaluationError } from "../../../functions/helpers";
import { _t } from "../../../translation";
import { CellValue, Color, Figure, Format, Getters, LocaleFormat, Range } from "../../../types";
import { CellValue, Color, Figure, Format, Getters, Locale, Range } from "../../../types";
import { GaugeChartRuntime, ScorecardChartRuntime } from "../../../types/chart";
import { ChartRuntime, DataSet, DatasetValues, LabelValues } from "../../../types/chart/chart";
import { formatValue, isDateTimeFormat } from "../../format/format";
import {
ChartAxisFormats,
ChartRuntime,
DataSet,
DatasetValues,
LabelValues,
} from "../../../types/chart/chart";
import { isDateTimeFormat } from "../../format/format";
import { deepCopy, range } from "../../misc";
import { isNumber } from "../../numbers";
import { recomputeZones } from "../../recompute_zones";
import { AbstractChart } from "./abstract_chart";
import { formatChartDatasetValue } from "./chart_common";
import { drawGaugeChart } from "./gauge_chart_rendering";
import { drawScoreChart } from "./scorecard_chart";
import { getScorecardConfiguration } from "./scorecard_chart_config_builder";
Expand Down Expand Up @@ -97,22 +104,24 @@ export function truncateLabel(label: string | undefined): string {
return label;
}

interface GetDefaultChartJsRuntimeOptions {
axisFormats?: ChartAxisFormats;
locale: Locale;
truncateLabels?: boolean;
horizontalChart?: boolean;
}

/**
* Get a default chart js configuration
*/
export function getDefaultChartJsRuntime(
chart: AbstractChart,
labels: string[],
fontColor: Color,
{
format,
locale,
truncateLabels = true,
horizontalChart,
}: LocaleFormat & { truncateLabels?: boolean; horizontalChart?: boolean }
{ axisFormats, locale, truncateLabels = true, horizontalChart }: GetDefaultChartJsRuntimeOptions
): Required<ChartConfiguration> {
const chartTitle = chart.title.text ? chart.title : { ...chart.title, content: "" };
const options: ChartOptions = {
const chartOptions: ChartOptions = {
// https://www.chartjs.org/docs/latest/general/responsive.html
responsive: true, // will resize when its container is resized
maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
Expand Down Expand Up @@ -160,8 +169,11 @@ export function getDefaultChartJsRuntime(
if (!yLabel) {
yLabel = tooltipItem.parsed;
}
const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });

const axisId = horizontalChart
? tooltipItem.dataset.xAxisID
: tooltipItem.dataset.yAxisID;
const yLabelStr = formatChartDatasetValue(axisFormats, locale)(yLabel, axisId);
return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
},
},
Expand All @@ -170,7 +182,7 @@ export function getDefaultChartJsRuntime(
};
return {
type: chart.type as ChartType,
options,
options: chartOptions,
data: {
labels: truncateLabels ? labels.map(truncateLabel) : labels,
datasets: [],
Expand Down Expand Up @@ -237,7 +249,12 @@ export function getChartLabelValues(
* Get the format to apply to the the dataset values. This format is defined as the first format
* found in the dataset ranges that isn't a date format.
*/
export function getChartDatasetFormat(getters: Getters, dataSets: DataSet[]): Format | undefined {
export function getChartDatasetFormat(
getters: Getters,
allDataSets: DataSet[],
axis: "left" | "right"
): Format | undefined {
const dataSets = allDataSets.filter((ds) => (axis === "right") === !!ds.rightYAxis);
for (const ds of dataSets) {
const formatsInDataset = getters.getRangeFormats(ds.dataRange);
const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));
Expand Down
15 changes: 6 additions & 9 deletions src/helpers/figures/charts/combo_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
copyDataSetsWithNewSheetId,
copyLabelRangeWithNewSheetId,
createDataSets,
formatTickValue,
formatChartDatasetValue,
getChartAxis,
getChartColorsGenerator,
getDefinedAxis,
Expand Down Expand Up @@ -230,10 +230,8 @@ export class ComboChart extends AbstractChart {
}

export function createComboChartRuntime(chart: ComboChart, getters: Getters): ComboChartRuntime {
const mainDataSetFormat = chart.dataSets.length
? getChartDatasetFormat(getters, [chart.dataSets[0]])
: undefined;
const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets.slice(1));
const mainDataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
const locale = getters.getLocale();

const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
Expand All @@ -252,10 +250,9 @@ export function createComboChartRuntime(chart: ComboChart, getters: Getters): Co
({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
}

const localeFormat = { format: mainDataSetFormat, locale };

const fontColor = chartFontColor(chart.background);
const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
const axisFormats = { y: mainDataSetFormat, y1: lineDataSetsFormat };
const config = getDefaultChartJsRuntime(chart, labels, fontColor, { locale, axisFormats });
const legend: DeepPartial<LegendOptions<"bar">> = {
labels: { color: fontColor },
};
Expand Down Expand Up @@ -283,7 +280,7 @@ export function createComboChartRuntime(chart: ComboChart, getters: Getters): Co
config.options.plugins!.chartShowValuesPlugin = {
showValues: chart.showValues,
background: chart.background,
callback: formatTickValue({ format: mainDataSetFormat, locale }),
callback: formatChartDatasetValue(axisFormats, locale),
};

const colors = getChartColorsGenerator(definition, dataSetsValues.length);
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/figures/charts/pie_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export function createPieChartRuntime(chart: PieChart, getters: Getters): PieCha

({ dataSetsValues, labels } = filterNegativeValues(labels, dataSetsValues));

const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
const locale = getters.getLocale();
const config = getPieConfiguration(chart, labels, { format: dataSetFormat, locale });
const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/figures/charts/pyramid_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ export function createPyramidChartRuntime(
return tooltipLabelCallback(tooltipItem);
};
const callback = config.options!.plugins!.chartShowValuesPlugin!.callback;
config.options!.plugins!.chartShowValuesPlugin!.callback = (x) =>
callback!(Math.abs(x as number));
config.options!.plugins!.chartShowValuesPlugin!.callback = (x, axisId) =>
callback!(Math.abs(x as number), axisId);

return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
}
4 changes: 3 additions & 1 deletion src/helpers/figures/charts/waterfall_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,9 @@ export function createWaterfallChartRuntime(
labels.push(_t("Subtotal"));
}

const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
const dataSetFormat =
getChartDatasetFormat(getters, chart.dataSets, "left") ||
getChartDatasetFormat(getters, chart.dataSets, "right");
const locale = getters.getLocale();
const dataSeriesLabels = dataSetsValues.map((dataSet) => dataSet.label);
const config = getWaterfallConfiguration(chart, labels, dataSeriesLabels, {
Expand Down
4 changes: 3 additions & 1 deletion src/types/chart/chart.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Align, Color, Range } from "../../types";
import { Align, Color, Format, Range } from "../../types";
import { XlsxHexColor } from "../xlsx";
import { BarChartDefinition, BarChartRuntime } from "./bar_chart";
import { ComboChartDefinition, ComboChartRuntime } from "./combo_chart";
Expand Down Expand Up @@ -151,3 +151,5 @@ export interface ChartCreationContext {
readonly fillArea?: boolean;
readonly showValues?: boolean;
}

export type ChartAxisFormats = { [axisId: string]: Format | undefined } | undefined;
Loading

0 comments on commit 398d495

Please sign in to comment.