Skip to content

Commit

Permalink
[IMP] charts: handles multiple bars in combo chart
Browse files Browse the repository at this point in the history
Task Description

This task adds the possibility to choose which series are bar
and line in a combo chart, make the user able to change this
for each data series in the design pannel.
When there is no bar series in the set, the first one is always
considered as a bar chart, to stay consistent with the previous
behavior.

Related Task

closes #4957

Task: 4164614
Signed-off-by: Lucas Lefèvre (lul) <[email protected]>
  • Loading branch information
anhe-odoo committed Sep 30, 2024
1 parent d89d673 commit 449782a
Show file tree
Hide file tree
Showing 12 changed files with 400 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ interface Props {
updateChart: (figureId: UID, definition: Partial<ChartWithAxisDefinition>) => DispatchResult;
}

export class ChartWithAxisDesignPanel extends Component<Props, SpreadsheetChildEnv> {
export class ChartWithAxisDesignPanel<P extends Props = Props> extends Component<
P,
SpreadsheetChildEnv
> {
static template = "o-spreadsheet-ChartWithAxisDesignPanel";
static components = {
GeneralDesignEditor,
Expand All @@ -49,7 +52,7 @@ export class ChartWithAxisDesignPanel extends Component<Props, SpreadsheetChildE

axisChoices = CHART_AXIS_CHOICES;

private state = useState({ index: 0 });
protected state = useState({ index: 0 });

get axesList(): AxisDefinition[] {
const { useLeftAxis, useRightAxis } = getDefinedAxis(this.props.definition);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { _t } from "../../../../translation";
import { ComboChartDefinition } from "../../../../types/chart/combo_chart";
import { DispatchResult, UID } from "../../../../types/index";
import { ChartWithAxisDesignPanel } from "../chart_with_axis/design_panel";

interface Props {
figureId: UID;
definition: ComboChartDefinition;
canUpdateChart: (figureID: UID, definition: Partial<ComboChartDefinition>) => DispatchResult;
updateChart: (figureId: UID, definition: Partial<ComboChartDefinition>) => DispatchResult;
}

export class ComboChartDesignPanel extends ChartWithAxisDesignPanel<Props> {
static template = "o-spreadsheet-ComboChartDesignPanel";
seriesTypeChoices = [
{ value: "bar", label: _t("Bar") },
{ value: "line", label: _t("Line") },
];

updateDataSeriesType(type: "bar" | "line") {
const dataSets = [...this.props.definition.dataSets];
if (!dataSets?.[this.state.index]) {
return;
}
dataSets[this.state.index] = {
...dataSets[this.state.index],
type,
};
this.props.updateChart(this.props.figureId, { dataSets });
}

getDataSeriesType() {
const dataSets = this.props.definition.dataSets;
if (!dataSets?.[this.state.index]) {
return "bar";
}
return dataSets[this.state.index].type ?? "line";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<templates>
<t t-name="o-spreadsheet-ComboChartDesignPanel">
<GeneralDesignEditor
figureId="props.figureId"
definition="props.definition"
updateChart="props.updateChart">
<t t-set-slot="general-extension">
<Section class="'pt-0'">
<t t-set-slot="title">Legend position</t>
<select
t-att-value="props.definition.legendPosition ?? 'top'"
class="o-input"
t-on-change="this.updateLegendPosition">
<option value="none">None</option>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</Section>
<Section class="'pt-0'">
<t t-set-slot="title">Values</t>
<Checkbox
name="'showValues'"
label="showValuesLabel"
value="props.definition.showValues"
onChange="showValues => props.updateChart(this.props.figureId, {showValues})"
/>
</Section>
</t>
</GeneralDesignEditor>
<SidePanelCollapsible collapsedAtInit="true">
<t t-set-slot="title">Data series</t>
<t t-set-slot="content">
<Section class="'pt-0 pb-0'">
<select
class="o-input data-series-selector"
t-model="state.label"
t-on-change="(ev) => this.updateSerieEditor(ev)">
<t t-foreach="getDataSeries()" t-as="serie" t-key="serie_index">
<option
t-att-value="serie"
t-att-selected="state.index === serie_index"
t-esc="serie"
/>
</t>
</select>
<Section class="'px-0'">
<div class="d-flex align-items-center">
<t t-set-slot="title">Series color</t>
<RoundColorPicker
currentColor="getDataSerieColor()"
onColorPicked.bind="updateDataSeriesColor"
/>
</div>
</Section>
<Section class="'pt-0 px-0 o-vertical-axis-selection'" t-if="canHaveTwoVerticalAxis">
<t t-set-slot="title">Vertical axis</t>
<RadioSelection
choices="axisChoices"
selectedValue="getDataSerieAxis()"
name="'axis'"
onChange.bind="updateDataSeriesAxis"
/>
</Section>
<Section class="'pt-0 px-0 o-series-type-selection'">
<t t-set-slot="title">Serie type</t>
<RadioSelection
choices="seriesTypeChoices"
selectedValue="getDataSeriesType()"
name="'seriesType'"
onChange.bind="updateDataSeriesType"
/>
</Section>
<Section class="'pt-0 px-0'">
<t t-set-slot="title">Series name</t>
<input
class="o-input o-serie-label-editor"
type="text"
t-att-value="getDataSerieLabel()"
t-on-change="(ev) => this.updateDataSeriesLabel(ev)"
/>
</Section>
<Section class="'pt-0 px-0 o-show-trend-line'" t-if="!props.definition.horizontal">
<t t-set-slot="title">Trend line</t>
<t t-set="showTrendLineLabel">Show trend line</t>
<t t-set="trend" t-value="getTrendLineConfiguration()"/>
<t t-set="trendType" t-value="getTrendType(trend)"/>
<Checkbox
name="'showTrendLine'"
label="showTrendLineLabel"
value="trend !== undefined and trend.display"
onChange.bind="toggleDataTrend"
/>
<div t-if="trend !== undefined and trend.display">
<div class="d-flex py-2">
<div class="w-100">
<span class="o-section-subtitle">Type</span>
<select class="o-input trend-type-selector" t-on-change="this.onChangeTrendType">
<option value="linear" t-att-selected="trendType === 'linear'">Linear</option>
<option value="exponential" t-att-selected="trendType === 'exponential'">
Exponential
</option>
<option value="polynomial" t-att-selected="trendType === 'polynomial'">
Polynomial
</option>
<option value="logarithmic" t-att-selected="trendType === 'logarithmic'">
Logarithmic
</option>
</select>
</div>
<div class="w-50 ms-3" t-if="trendType === 'polynomial'">
<span class="o-section-subtitle">Degree</span>
<input
t-att-value="trend.order"
type="number"
class="w-100 o-input trend-order-input"
t-on-change="this.onChangePolynomialDegree"
min="1"
/>
</div>
</div>
<div class="d-flex align-items-center">
<span class="o-section-subtitle my-0 pe-2">Trend line color</span>
<RoundColorPicker
currentColor="getTrendLineColor()"
onColorPicked.bind="updateTrendLineColor"
/>
</div>
</div>
</Section>
</Section>
</t>
</SidePanelCollapsible>
<SidePanelCollapsible collapsedAtInit="true">
<t t-set-slot="title">Axes</t>
<t t-set-slot="content">
<AxisDesignEditor
axesList="axesList"
figureId="props.figureId"
definition="props.definition"
updateChart="props.updateChart"
/>
</t>
</SidePanelCollapsible>
</t>
</templates>
3 changes: 2 additions & 1 deletion src/components/side_panel/chart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Registry } from "../../../registries/registry";
import { BarConfigPanel } from "./bar_chart/bar_chart_config_panel";
import { GenericChartConfigPanel } from "./building_blocks/generic_side_panel/config_panel";
import { ChartWithAxisDesignPanel } from "./chart_with_axis/design_panel";
import { ComboChartDesignPanel } from "./combo_chart/combo_chart_design_panel";
import { GaugeChartConfigPanel } from "./gauge_chart_panel/gauge_chart_config_panel";
import { GaugeChartDesignPanel } from "./gauge_chart_panel/gauge_chart_design_panel";
import { LineConfigPanel } from "./line_chart/line_chart_config_panel";
Expand Down Expand Up @@ -43,7 +44,7 @@ chartSidePanelComponentRegistry
})
.add("combo", {
configuration: GenericChartConfigPanel,
design: ChartWithAxisDesignPanel,
design: ComboChartDesignPanel,
})
.add("pie", {
configuration: GenericChartConfigPanel,
Expand Down
24 changes: 16 additions & 8 deletions src/helpers/figures/charts/combo_chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import {
import {
AxesDesign,
CustomizedDataSet,
DatasetDesign,
ExcelChartDataset,
LegendPosition,
} from "../../../types/chart";
import { ComboChartDefinition, ComboChartRuntime } from "../../../types/chart/combo_chart";
import {
ComboChartDataSet,
ComboChartDefinition,
ComboChartRuntime,
} from "../../../types/chart/combo_chart";
import { CellErrorType } from "../../../types/errors";
import { Validator } from "../../../types/validator";
import { toXlsxHexColor } from "../../../xlsx/helpers/colors";
Expand Down Expand Up @@ -64,7 +67,7 @@ export class ComboChart extends AbstractChart {
readonly legendPosition: LegendPosition;
readonly aggregated?: boolean;
readonly dataSetsHaveTitle: boolean;
readonly dataSetDesign?: DatasetDesign[];
readonly dataSetDesign?: ComboChartDataSet[];
readonly axesDesign?: AxesDesign;
readonly type = "combo";
readonly showValues?: boolean;
Expand Down Expand Up @@ -127,11 +130,12 @@ export class ComboChart extends AbstractChart {
labelRange: Range | undefined,
targetSheetId?: UID
): ComboChartDefinition {
const ranges: CustomizedDataSet[] = [];
const ranges: ComboChartDataSet[] = [];
for (const [i, dataSet] of dataSets.entries()) {
ranges.push({
...this.dataSetDesign?.[i],
dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"),
});
}
return {
Expand Down Expand Up @@ -189,9 +193,13 @@ export class ComboChart extends AbstractChart {
}

static getDefinitionFromContextCreation(context: ChartCreationContext): ComboChartDefinition {
const dataSets: ComboChartDataSet[] = (context.range ?? []).map((ds, index) => ({
...ds,
type: index ? "line" : "bar",
}));
return {
background: context.background,
dataSets: context.range ?? [],
dataSets,
dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
aggregated: context.aggregated,
legendPosition: context.legendPosition ?? "top",
Expand Down Expand Up @@ -249,7 +257,6 @@ export function createComboChartRuntime(chart: ComboChart, getters: Getters): Co
const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
const legend: DeepPartial<LegendOptions<"bar">> = {
labels: { color: fontColor },
reverse: true,
};
if (chart.legendPosition === "none") {
legend.display = false;
Expand Down Expand Up @@ -320,14 +327,15 @@ export function createComboChartRuntime(chart: ComboChart, getters: Getters): Co
for (let [index, { label, data }] of dataSetsValues.entries()) {
const design = definition.dataSets[index];
const color = colors.next();
const type = design?.type ?? "line";
const dataset: ChartDataset<"bar" | "line", number[]> = {
label: design?.label ?? label,
data,
borderColor: color,
backgroundColor: color,
yAxisID: design?.yAxisId ?? "y",
type: index === 0 ? "bar" : "line",
order: -index,
type,
order: type === "bar" ? dataSetsValues.length + index : index,
};
config.data.datasets.push(dataset);

Expand Down
4 changes: 4 additions & 0 deletions src/types/chart/combo_chart.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { ChartConfiguration } from "chart.js";
import { Color } from "../misc";
import { CustomizedDataSet } from "./chart";
import { ComboBarChartDefinition } from "./common_bar_combo";

export interface ComboChartDefinition extends ComboBarChartDefinition {
readonly dataSets: ComboChartDataSet[];
readonly type: "combo";
}

export type ComboChartDataSet = CustomizedDataSet & { type?: "bar" | "line" };

export type ComboChartRuntime = {
chartJsConfig: ChartConfiguration;
background: Color;
Expand Down
10 changes: 8 additions & 2 deletions tests/figures/chart/chart_plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1644,7 +1644,7 @@ describe("Chart without labels", () => {
expect(getChartConfiguration(model, "44").data?.labels).toEqual(["B1", "B2"]);
});

test("Combo chart has both line and bar", () => {
test("Combo chart has bar if the type is set to bar and line else", () => {
setCellContent(model, "A1", "1");
setCellContent(model, "A2", "2");
setCellContent(model, "A3", "3");
Expand All @@ -1654,7 +1654,13 @@ describe("Chart without labels", () => {

createComboChart(
model,
{ dataSets: [{ dataRange: "A1:A2" }, { dataRange: "A3:A4" }, { dataRange: "A5:A6" }] },
{
dataSets: [
{ dataRange: "A1:A2", type: "bar" },
{ dataRange: "A3:A4" },
{ dataRange: "A5:A6" },
],
},
"43"
);
const dataSets = getChartConfiguration(model, "43").data.datasets;
Expand Down
Loading

0 comments on commit 449782a

Please sign in to comment.