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
8 changes: 8 additions & 0 deletions src/actions/edit_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ export const pasteSpecialFormat: ActionSpec = {
execute: ACTIONS.PASTE_FORMAT_ACTION,
};

export const dataAnalysis: ActionSpec = {
name: _t("Data analysis"),
execute: (env) =>
env.openSidePanel("DataAnalysisPanel", { zones: env.model.getters.getSelectedZones() }),
icon: "o-spreadsheet-Icon.COLUMN_STATS",
isEnabled: (env) => !env.isSmall,
};

export const findAndReplace: ActionSpec = {
name: _t("Find and replace"),
shortcut: "Ctrl+H",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
.o-spreadsheet {
.o-selection-statistic {
margin-right: 20px;
padding: 4px 4px 4px 8px;
cursor: pointer;
&:hover {
background-color: var(--os-background-gray-color-hover) !important;
.o-bottom-bar-statistic {
.o-selection-statistic {
margin-right: 10px;
padding: 4px 4px 4px 8px;
cursor: pointer;
&:hover {
background-color: var(--os-background-gray-color-hover) !important;
}
}
.o-icon {
width: 30px;
height: 30px;
}
.o-data-analysis-button {
cursor: pointer;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,9 @@ export class BottomBarStatistic extends Component<SpreadsheetChildEnv> {
(fnValue?.value !== undefined ? formatValue(fnValue.value(), { locale }) : "__")
);
}

showDataAnalysis() {
const zones = this.env.model.getters.getSelectedZones();
this.env.openSidePanel("DataAnalysisPanel", { zones });
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
<templates>
<t t-name="o-spreadsheet-BottomBarStatistic">
<t t-set="selectedStatistic" t-value="this.getSelectedStatistic()"/>
<Ripple class="'ms-auto bottom-statistics'" t-if="selectedStatistic !== undefined">
<div
class="o-selection-statistic text-truncate user-select-none me-4 bg-white rounded shadow d-flex align-items-center"
t-on-click="this.listSelectionStatistics">
<t t-out="selectedStatistic"/>
<span class="ms-2">
<t t-call="o-spreadsheet-Icon.CARET_DOWN"/>
</span>
</div>
</Ripple>
<div class="w-100 d-flex o-bottom-bar-statistic" t-if="selectedStatistic !== undefined">
<Ripple class="'ms-auto bottom-statistics'" t-if="selectedStatistic !== undefined">
<div
class="o-selection-statistic text-truncate user-select-none me-4 bg-white rounded shadow d-flex align-items-center"
t-on-click="this.listSelectionStatistics">
<t t-out="selectedStatistic"/>
<span class="ms-2">
<t t-call="o-spreadsheet-Icon.CARET_DOWN"/>
</span>
</div>
</Ripple>
<span class="o-data-analysis-button m-2 me-4" t-on-click="this.showDataAnalysis">
<t t-call="o-spreadsheet-Icon.COLUMN_STATS"/>
</span>
</div>
</t>
</templates>
11 changes: 10 additions & 1 deletion src/components/icons/icons.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,7 @@
</div>
</t>
<t t-name="o-spreadsheet-Icon.COLUMN_STATS">
<svg width="18px" height="18px" viewBox="0 0 512 512">
<svg class="o-icon" viewBox="0 0 512 512">
<path
d="M319 174c71 0 129 58 129 129 0 27-8 52-23 73l66 66-33 33-66-66c-21 14-46 23-73 23-71 0-129-58-129-129s57-129 129-129m0 47c-45 0-82 37-82 82s36 82 82 82 82-37 82-82-37-82-82-82m-152 70a155 155 0 0 0 0 12c0 12 1 24 4 35H25v-47zM119 33v235H49V33zm94 94v66c-21 20-36 46-43 75h-28V127zm94-47v71a152 152 0 0 0-70 24V80zm94 23v71a152 152 0 0 0-70-24v-47z"
fill="currentColor"
Expand Down Expand Up @@ -1200,4 +1200,13 @@
/>
</svg>
</t>
<t t-name="o-spreadsheet-Icon.DATA_ANALYSIS">
<svg fill="currentColor" class="o-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.71,7.29a1,1,0,0,0-1.42,0L14,13.59,9.71,9.29a1,1,0,0,0-1.42,0l-6,6a1,1,0,0,0,0,1.42,1,1,0,0,0,1.42,0L9,11.41l4.29,4.3a1,1,0,0,0,1.42,0l7-7A1,1,0,0,0,21.71,7.29Z"
transform="scale(.8,.8) translate(2,2)"
/>
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor"/>
</svg>
</t>
</templates>
226 changes: 226 additions & 0 deletions src/components/side_panel/data_analysis/chart_suggestion_preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { onWillUnmount, props, signal } from "@odoo/owl";
import { Chart, ChartConfiguration } from "chart.js/auto";
import { DEFAULT_FIGURE_HEIGHT, DEFAULT_FIGURE_WIDTH } from "../../../constants";
import { SpreadsheetChart } from "../../../helpers/figures/chart";
import { drawGaugeChart } from "../../../helpers/figures/charts/gauge_chart_rendering";
import { drawScoreChart } from "../../../helpers/figures/charts/scorecard_chart";
import { getScorecardConfiguration } from "../../../helpers/figures/charts/scorecard_chart_config_builder";
import { UuidGenerator } from "../../../helpers/uuid";
import { Component, useLayoutEffect } from "../../../owl3_compatibility_layer";
import { GaugeChartRuntime } from "../../../types/chart/gauge_chart";
import { ScorecardChartRuntime } from "../../../types/chart/scorecard_chart";
import { SpreadsheetChildEnv } from "../../../types/spreadsheet_env";
import { types } from "../../props_validation";

export class ChartSuggestionPreview extends Component<SpreadsheetChildEnv> {
static template = "o-spreadsheet-ChartSuggestionPreview";
protected props = props({
definition: types.ChartDefinition(),
title: types.string(),
rationale: types.string(),
isRecommended: types.boolean(),
});

private chartCanvasRef = signal<HTMLCanvasElement | null>(null);
private chartDivRef = signal<HTMLDivElement | null>(null);
private chart?: Chart;
private renderedChartType?: string;

setup() {
onWillUnmount(() => this.destroyChart());
useLayoutEffect(
() => {
this.updateChart();
},
() => [this.props.definition]
);
}

private getChartConfiguration(): ChartConfiguration | null {
const getters = this.env.model.getters;
const activeSheetId = getters.getActiveSheetId();
const chart = SpreadsheetChart.fromStrDefinition(getters, activeSheetId, this.props.definition);
const runtime = chart.getRuntime(getters, "myChart");
if (!("chartJsConfig" in runtime)) {
return null;
}
let config = runtime.chartJsConfig;
const existingScales = config.options?.scales ?? {};
config = {
...config,
options: {
...config.options,
plugins: {
...config.options?.plugins,
legend: { display: false },
title: { display: false },
tooltip: { enabled: false },
},
events: [],
animation: false,
scales: {
...existingScales,
...("x" in existingScales
? {
x: {
...existingScales.x,
ticks: { display: false },
border: { display: false },
},
}
: {}),
...("y" in existingScales
? {
y: {
...existingScales.y,
ticks: { display: false },
border: { display: false },
},
}
: {}),
...("r" in existingScales
? {
r: {
...(existingScales as any).r,
ticks: { display: false },
pointLabels: { display: false },
},
}
: {}),
},
},
};
return config;
}

startDragAndDrop(ev: MouseEvent) {
const canvas = this.chartCanvasRef();
const div = this.chartDivRef();
if (!div) {
return;
}
const target = canvas ?? div;
const rect = target.getBoundingClientRect();
const { position, left, top } = getComputedStyle(div);
const offsetX = ev.clientX - rect.left;
const offsetY = ev.clientY - rect.top;
const onMouseMove = (moveEvent: MouseEvent) => {
div.style.position = "absolute";
div.style.left = `${moveEvent.clientX - offsetX}px`;
div.style.top = `${moveEvent.clientY - offsetY}px`;
};
const onMouseUp = (mouseEvent: MouseEvent) => {
const gridOverlay = document.querySelector(".o-grid-overlay") as HTMLElement | null;
if (!gridOverlay) {
return;
}
const gridRect = gridOverlay.getBoundingClientRect();
if (
mouseEvent.clientX > gridRect.left &&
mouseEvent.clientX < gridRect.right &&
mouseEvent.clientY > gridRect.top &&
mouseEvent.clientY < gridRect.bottom
) {
const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
const x = mouseEvent.clientX - gridRect.left - offsetX + scrollX;
const y = mouseEvent.clientY - gridRect.top - offsetY + scrollY;
const { col, row, offset } = this.env.model.getters.getPositionAnchorOffset({ x, y });
this.env.model.dispatch("CREATE_CHART", {
chartId: UuidGenerator.smallUuid(),
figureId: UuidGenerator.smallUuid(),
sheetId: this.env.model.getters.getActiveSheetId(),
size: { width: DEFAULT_FIGURE_WIDTH, height: DEFAULT_FIGURE_HEIGHT },
definition: this.props.definition,
col,
row,
offset,
});
}
div.style.position = position;
div.style.left = left;
div.style.top = top;
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
};
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
}

private createChart() {
if (!globalThis.Chart) {
throw new Error("Chart.js library is not loaded");
}
const canvas = this.chartCanvasRef();
if (!canvas) {
return;
}
const ctx = canvas.getContext("2d");
if (!ctx) {
return;
}
const config = this.getChartConfiguration();
if (!config) {
return;
}
//@ts-ignore
this.chart = new globalThis.Chart!(ctx, config);
this.renderedChartType = this.props.definition.type;
}

private updateChart() {
const config = this.getChartConfiguration();
if (!config) {
this.destroyChart();
this.drawNativeChart();
return;
}
if (this.chart && this.renderedChartType !== this.props.definition.type) {
this.destroyChart();
}
if (!this.chart) {
this.createChart();
return;
}
this.chart.config.options = config.options;
this.chart.data = config.data;
this.chart.update();
}

private drawNativeChart() {
const canvas = this.chartCanvasRef();
if (!canvas) {
return;
}
const getters = this.env.model.getters;
const chart = SpreadsheetChart.fromStrDefinition(
getters,
getters.getActiveSheetId(),
this.props.definition
);
const runtime = chart.getRuntime(getters, "myChart");
const { type } = this.props.definition;
if (type === "scorecard") {
const rect = canvas.getBoundingClientRect();
const config = getScorecardConfiguration(
{ width: rect.width || 130, height: rect.height || 120 },
runtime as ScorecardChartRuntime
);
drawScoreChart(config, canvas);
} else if (type === "gauge") {
let gaugeRuntime = runtime as GaugeChartRuntime;
if (gaugeRuntime.title) {
gaugeRuntime = {
...gaugeRuntime,
title: { text: "" },
};
}
drawGaugeChart(canvas, gaugeRuntime, 1, undefined, { labelFontSize: 8 });
}
}

private destroyChart() {
this.chart?.destroy();
this.chart = undefined;
this.renderedChartType = undefined;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<templates>
<t t-name="o-spreadsheet-ChartSuggestionPreview">
<div
class="o-chart-suggestion-thumb d-flex flex-column"
t-att-class="{ 'o-chart-suggestion-recommended': this.props.isRecommended }"
t-att-title="this.props.rationale">
<div
class="o-suggestion-canvas-wrap"
t-ref="this.chartDivRef"
t-on-pointerdown="(ev) => this.startDragAndDrop(ev)">
<canvas class="w-100 h-100" t-ref="this.chartCanvasRef"/>
</div>
<div
class="o-suggestion-thumb-title text-truncate small text-center px-1 mt-1"
t-out="this.props.title"
/>
</div>
</t>
</templates>
Loading