Skip to content

Commit ca74c01

Browse files
anhe-odoorrahir
authored andcommitted
[IMP] charts: add calendar chart
Task Description This task aims to add the possibility to analyze temporal records by grouping the value associated to each date into time periods (e.g. year, date, month, hour of day, ...). This analysis is added as a new chart which could be further adapted if we want to make heat map based on a matrix data. Related Task Task: 4816317 Part-of: #6773 Signed-off-by: Rémi Rahir (rar) <rar@odoo.com>
1 parent 41e8d5c commit ca74c01

30 files changed

+1651
-53
lines changed

packages/o-spreadsheet-engine/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const CHART_PADDING_TOP = 15;
4040
export const CHART_TITLE_FONT_SIZE = 16;
4141
export const CHART_AXIS_TITLE_FONT_SIZE = 12;
4242
export const MASTER_CHART_HEIGHT = 60;
43+
export const CHART_COLORSCALE_WIDTH = 70;
4344

4445
export const SCORECARD_CHART_TITLE_FONT_SIZE = 14;
4546

packages/o-spreadsheet-engine/src/helpers/pivot/spreadsheet_pivot/date_spreadsheet_pivot.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { toNormalizedPivotValue } from "../pivot_helpers";
66

77
const NULL_SYMBOL = Symbol("NULL");
88

9-
export function createDate(dimension: PivotDimension, value: CellValue, locale: Locale): CellValue {
9+
export function createDate(
10+
dimension: Pick<PivotDimension, "type" | "displayName" | "granularity">,
11+
value: CellValue,
12+
locale: Locale
13+
): CellValue {
1014
const granularity = dimension.granularity || "month";
1115
if (!(granularity in MAP_VALUE_DIMENSION_DATE)) {
1216
throw new Error(`Unknown date granularity: ${granularity}`);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ChartConfiguration } from "chart.js";
2+
import { ChartColorScale, CommonChartDefinition } from ".";
3+
import { Color } from "../misc";
4+
import { Granularity } from "../pivot";
5+
6+
export const CALENDAR_CHART_GRANULARITIES: Granularity[] = [
7+
"year",
8+
"quarter_number",
9+
"month_number",
10+
"iso_week_number",
11+
"day_of_month",
12+
"day_of_week",
13+
"hour_number",
14+
"minute_number",
15+
"second_number",
16+
];
17+
18+
export type CalendarChartGranularity = (typeof CALENDAR_CHART_GRANULARITIES)[number];
19+
20+
export interface CalendarChartDefinition extends CommonChartDefinition {
21+
readonly type: "calendar";
22+
readonly colorScale?: ChartColorScale;
23+
readonly missingValueColor?: Color;
24+
readonly horizontalGroupBy?: CalendarChartGranularity;
25+
readonly verticalGroupBy?: CalendarChartGranularity;
26+
}
27+
28+
export type CalendarChartRuntime = {
29+
chartJsConfig: ChartConfiguration;
30+
background: Color;
31+
};

packages/o-spreadsheet-engine/src/types/chart/chart.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Point } from "chart.js";
22
import { XlsxHexColor } from "../xlsx";
33
import { BarChartDefinition, BarChartRuntime } from "./bar_chart";
4+
import { CalendarChartDefinition } from "./calendar_chart";
45
import { ComboChartDefinition, ComboChartRuntime } from "./combo_chart";
56
import { LegendPosition } from "./common_chart";
67
import { FunnelChartColors, FunnelChartDefinition, FunnelChartRuntime } from "./funnel_chart";
@@ -40,6 +41,7 @@ export const CHART_TYPES = [
4041
"funnel",
4142
"sunburst",
4243
"treemap",
44+
"calendar",
4345
] as const;
4446
export type ChartType = (typeof CHART_TYPES)[number];
4547

@@ -57,13 +59,21 @@ export type ChartDefinition =
5759
| GeoChartDefinition
5860
| FunnelChartDefinition
5961
| SunburstChartDefinition
60-
| TreeMapChartDefinition;
62+
| TreeMapChartDefinition
63+
| CalendarChartDefinition;
6164

6265
export type ChartWithDataSetDefinition = Extract<
6366
ChartDefinition,
6467
{ dataSets: CustomizedDataSet[]; labelRange?: string; humanize?: boolean }
6568
>;
6669

70+
export type ChartWithColorScaleDefinition = Extract<
71+
ChartDefinition,
72+
{ colorScale?: ChartColorScale }
73+
>;
74+
75+
export type ChartWithTitleDefinition = Extract<ChartDefinition, { title?: TitleDesign }>;
76+
6777
export type ChartWithAxisDefinition = Extract<
6878
ChartWithDataSetDefinition,
6979
{ axesDesign?: AxesDesign }

packages/o-spreadsheet-engine/src/types/chart/geo_chart.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,14 @@
11
import { ChartConfiguration } from "chart.js";
22
import { Color } from "../misc";
3-
import {
4-
ChartColorScale,
5-
ChartRuntimeGenerationArgs,
6-
CustomizedDataSet,
7-
TitleDesign,
8-
} from "./chart";
9-
import { LegendPosition } from "./common_chart";
3+
import { ChartColorScale, ChartRuntimeGenerationArgs } from "./chart";
4+
import { CommonChartDefinition } from "./common_chart";
105

11-
export interface GeoChartDefinition {
6+
export interface GeoChartDefinition extends CommonChartDefinition {
127
readonly type: "geo";
13-
readonly dataSets: CustomizedDataSet[];
14-
readonly dataSetsHaveTitle: boolean;
15-
readonly labelRange?: string;
16-
readonly title: TitleDesign;
17-
readonly background?: Color;
18-
readonly legendPosition: LegendPosition;
198
readonly colorScale?: ChartColorScale;
209
readonly missingValueColor?: Color;
2110
readonly region?: string;
22-
readonly humanize?: boolean;
11+
readonly showColorBar?: boolean;
2312
}
2413

2514
export type GeoChartRuntime = {

src/components/figures/chart/chartJs/chartjs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Store, useStore } from "../../../../store_engine";
77
import { UID } from "../../../../types";
88
import { chartJsExtensionRegistry, registerChartJSExtensions } from "./chart_js_extension";
99
import { ChartAnimationStore } from "./chartjs_animation_store";
10+
import { chartColorScalePlugin } from "./chartjs_colorscale_plugin";
1011
import {
1112
funnelTooltipPositioner,
1213
getFunnelChartController,
@@ -52,6 +53,10 @@ chartJsExtensionRegistry.add("sunburstHoverPlugin", {
5253
register: (Chart) => Chart.register(sunburstHoverPlugin),
5354
unregister: (Chart) => Chart.unregister(sunburstHoverPlugin),
5455
});
56+
chartJsExtensionRegistry.add("chartColorScalePlugin", {
57+
register: (Chart) => Chart.register(chartColorScalePlugin),
58+
unregister: (Chart) => Chart.unregister(chartColorScalePlugin),
59+
});
5560

5661
export class ChartJsComponent extends Component<Props, SpreadsheetChildEnv> {
5762
static template = "o-spreadsheet-ChartJsComponent";
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
CHART_AXIS_TITLE_FONT_SIZE,
3+
CHART_COLORSCALE_WIDTH,
4+
CHART_PADDING,
5+
} from "@odoo/o-spreadsheet-engine/constants";
6+
import { ChartType, Plugin } from "chart.js";
7+
import { Color, Locale } from "../../../..";
8+
import { getDefaultContextFont, humanizeNumber } from "../../../../helpers";
9+
10+
export interface ChartColorScalePluginOptions {
11+
position: "left" | "right" | "none";
12+
colorScale: Color[];
13+
fontColor?: Color;
14+
minValue: number;
15+
maxValue: number;
16+
locale: Locale;
17+
}
18+
19+
declare module "chart.js" {
20+
interface PluginOptionsByType<TType extends ChartType> {
21+
chartColorScalePlugin?: ChartColorScalePluginOptions;
22+
}
23+
}
24+
25+
/** This is a chartJS plugin that will draw the heatmap colorscale at the chart legend position */
26+
export const chartColorScalePlugin: Plugin = {
27+
id: "chartColorScalePlugin",
28+
afterDatasetsDraw(chart: any, args, options: ChartColorScalePluginOptions) {
29+
if (!options.position || options.position === "none" || !options.colorScale.length) {
30+
return;
31+
}
32+
const ctx = chart.ctx as CanvasRenderingContext2D;
33+
ctx.save();
34+
35+
ctx.textAlign = "center";
36+
ctx.textBaseline = "middle";
37+
ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
38+
39+
const gradientHeight = (chart.chartArea.bottom - chart.chartArea.top) / 2;
40+
const gradientWidth = 10;
41+
const gradientX =
42+
options.position === "left" ? CHART_PADDING : ctx.canvas.width - CHART_COLORSCALE_WIDTH;
43+
const gradientY = chart.chartArea.top;
44+
45+
// Create gradient
46+
const gradient = ctx.createLinearGradient(0, gradientY + gradientHeight, 0, gradientY);
47+
const step = 1 / (options.colorScale.length - 1);
48+
options.colorScale.forEach((color, index) => {
49+
gradient.addColorStop(index * step, color);
50+
});
51+
52+
// Draw gradient rectangle
53+
ctx.fillStyle = gradient;
54+
ctx.fillRect(gradientX, gradientY, gradientWidth, gradientHeight);
55+
56+
// Draw min and max labels
57+
ctx.fillStyle = options.fontColor ?? "black";
58+
ctx.font = getDefaultContextFont(CHART_AXIS_TITLE_FONT_SIZE);
59+
ctx.textAlign = "left";
60+
let minValue = Math.round(options.minValue * 100) / 100;
61+
let maxValue = Math.round(options.maxValue * 100) / 100;
62+
if (options.minValue === options.maxValue) {
63+
minValue -= 1;
64+
maxValue += 1;
65+
}
66+
const formattedMaxValue = humanizeNumber(
67+
{ value: maxValue, format: undefined },
68+
options.locale
69+
);
70+
const formattedMinValue = humanizeNumber(
71+
{ value: minValue, format: undefined },
72+
options.locale
73+
);
74+
ctx.fillText(formattedMinValue, gradientX + gradientWidth + 5, gradientY + gradientHeight - 6);
75+
ctx.fillText(formattedMaxValue, gradientX + gradientWidth + 5, gradientY + 6);
76+
77+
ctx.restore();
78+
},
79+
};

0 commit comments

Comments
 (0)