created piechart

This commit is contained in:
stfrigerio 2025-02-03 15:19:49 +01:00
parent 479f1ad8a5
commit 7d255212cd
7 changed files with 261 additions and 59 deletions

View file

@ -0,0 +1,97 @@
import { getColorForIndex } from "../helpers/getColorForIndex";
interface PieChartOptions {
showLegend?: boolean;
isDoughnut?: boolean;
cutout?: number; // percentage of the chart radius (0-100)
tooltips?: boolean;
animations?: boolean;
rotation?: number; // starting angle in degrees
}
export function createPieChart(labels: string[], datasets: any[], options: PieChartOptions = {}) {
const defaultOptions: PieChartOptions = {
showLegend: true,
isDoughnut: false,
cutout: 50, // only used if isDoughnut is true
tooltips: true,
animations: true,
rotation: -90 // start from top (-90 degrees)
};
const chartOptions = { ...defaultOptions, ...options };
return {
type: chartOptions.isDoughnut ? 'doughnut' : 'pie',
data: {
labels,
datasets: datasets.map(dataset => ({
...dataset,
backgroundColor: labels.map((_, index) =>
getColorForIndex(index, 0.6)
),
hoverBackgroundColor: labels.map((_, index) =>
getColorForIndex(index, 0.8)
),
borderWidth: 2,
borderColor: 'white',
hoverBorderWidth: 3,
hoverBorderColor: 'white',
}))
},
options: {
responsive: true,
maintainAspectRatio: false,
rotation: chartOptions.rotation,
cutout: chartOptions.isDoughnut ? `${chartOptions.cutout}%` : 0,
animations: chartOptions.animations ? {
animateRotate: true,
animateScale: true
} : false,
plugins: {
legend: {
display: chartOptions.showLegend,
position: 'right' as const,
labels: {
padding: 15,
font: {
size: 12
},
generateLabels: (chart: any) => {
const data = chart.data;
return data.labels.map((label: string, index: number) => ({
text: `${label}: ${data.datasets[0].data[index]}`,
fillStyle: data.datasets[0].backgroundColor[index],
strokeStyle: data.datasets[0].borderColor,
lineWidth: 1,
hidden: isNaN(data.datasets[0].data[index]),
index
}));
}
}
},
tooltip: {
enabled: chartOptions.tooltips,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleFont: {
size: 13
},
bodyFont: {
size: 12
},
padding: 10,
cornerRadius: 4,
callbacks: {
label: function(context: any) {
const label = context.label || '';
const value = context.parsed;
const total = context.dataset.data.reduce((a: number, b: number) => a + b, 0);
const percentage = ((value * 100) / total).toFixed(1);
return `${label}: ${value} (${percentage}%)`;
}
}
}
}
}
};
}

View file

@ -1,5 +1,6 @@
import { createLineChart } from './LineChart';
import { createBarChart } from './BarChart';
import { createPieChart } from './PieChart';
export type ChartType = 'line' | 'bar' | 'scatter' | 'pie';
@ -9,6 +10,8 @@ export function createChart(type: ChartType, labels: string[], datasets: any[],
return createLineChart(labels, datasets, options);
case 'bar':
return createBarChart(labels, datasets, options);
case 'pie':
return createPieChart(labels, datasets, options);
//todo add pie and others
default:
return createLineChart(labels, datasets); // fallback to line chart

View file

@ -3,31 +3,53 @@ import { ChartConfig } from "./parseChartParams";
export function buildSqlQuery(config: ChartConfig) {
let query, params = [];
if (config.groupBy) {
query = `
SELECT
${config.xColumn},
${config.groupBy},
${config.yColumns.join(', ')}
FROM "${config.table}"
`;
} else {
query = `
SELECT
${config.xColumn},
${config.yColumns.join(', ')}
FROM "${config.table}"
`;
}
switch (config.chartType) {
case 'pie':
query = `
SELECT
${config.categoryColumn},
SUM(${config.valueColumn}) as value
FROM "${config.table}"
`;
if (config.startDate && config.endDate) {
query += ` WHERE date BETWEEN ? AND ?`;
params.push(config.startDate, config.endDate);
}
query += ` GROUP BY ${config.categoryColumn}`;
query += ` ORDER BY value DESC`;
break;
if (config.startDate && config.endDate) {
query += ` WHERE ${config.xColumn} BETWEEN ? AND ?`;
params.push(config.startDate, config.endDate);
}
case 'line':
case 'bar':
if (config.categoryColumn) {
query = `
SELECT
${config.xColumn},
${config.categoryColumn},
${config.yColumns.join(', ')}
FROM "${config.table}"
`;
} else {
query = `
SELECT
${config.xColumn},
${config.yColumns.join(', ')}
FROM "${config.table}"
`;
}
query += ` ORDER BY ${config.xColumn}`;
if (config.groupBy) {
query += `, ${config.groupBy}`;
if (config.startDate && config.endDate) {
query += ` WHERE ${config.xColumn} BETWEEN ? AND ?`;
params.push(config.startDate, config.endDate);
}
query += ` ORDER BY ${config.xColumn}`;
if (config.categoryColumn) {
query += `, ${config.categoryColumn}`;
}
break;
}
return { query, params };

View file

@ -1,26 +1,28 @@
export interface ChartOptions {
fill?: boolean;
tension?: number;
pointRadius?: number;
pointHoverRadius?: number;
showLegend?: boolean;
yAxisMin?: number;
yAxisMax?: number;
tooltips?: boolean;
animations?: boolean;
}
export interface ChartConfig {
export interface BaseChartConfig {
table: string;
xColumn: string;
yColumns: string[];
groupBy?: string;
chartType: 'line' | 'bar' | 'pie';
startDate?: string;
endDate?: string;
chartType: string;
chartOptions?: ChartOptions;
chartOptions?: any;
}
// For line and bar charts
export interface TimeSeriesChartConfig extends BaseChartConfig {
chartType: 'line' | 'bar';
xColumn: string;
yColumns: string[];
categoryColumn?: string;
}
// For pie charts
export interface PieChartConfig extends BaseChartConfig {
chartType: 'pie';
categoryColumn: string;
valueColumn: string;
}
export type ChartConfig = TimeSeriesChartConfig | PieChartConfig;
export function parseChartParams(source: string): ChartConfig | null {
const lines = source.split("\n");
const params: any = {};
@ -42,11 +44,9 @@ export function parseChartParams(source: string): ChartConfig | null {
}
if (inChartOptions) {
// Parse chartOptions parameters
const optionMatch = trimmed.match(/(\w+):\s*([^,]+),?$/);
if (optionMatch) {
const [, key, valueStr] = optionMatch;
// Convert string values to appropriate types
let value: any = valueStr.trim();
if (value === 'true') value = true;
else if (value === 'false') value = false;
@ -61,22 +61,52 @@ export function parseChartParams(source: string): ChartConfig | null {
if (parts.length >= 2) {
const key = parts[0];
const val = parts.slice(1).join(":").trim();
params[key] = val;
if (key === 'yColumns') {
params[key] = val.split(',').map((c: string) => c.trim());
} else {
params[key] = val;
}
}
}
if (!params.table || !params.xColumn || !params.yColumns) {
if (!params.table || !params.chartType) {
return null;
}
return {
const baseConfig = {
table: params.table,
xColumn: params.xColumn,
yColumns: params.yColumns.split(',').map((c: string) => c.trim()),
groupBy: params.groupBy?.trim(),
chartType: params.chartType,
startDate: params.startDate,
endDate: params.endDate,
chartType: params.chartType || 'line',
chartOptions: Object.keys(chartOptions).length > 0 ? chartOptions : undefined
};
switch (params.chartType) {
case 'pie':
if (!params.categoryColumn || !params.valueColumn) {
return null;
}
return {
...baseConfig,
chartType: 'pie',
categoryColumn: params.categoryColumn,
valueColumn: params.valueColumn
};
case 'line':
case 'bar':
if (!params.xColumn || !params.yColumns) {
return null;
}
return {
...baseConfig,
chartType: params.chartType,
xColumn: params.xColumn,
yColumns: Array.isArray(params.yColumns) ? params.yColumns : [params.yColumns],
categoryColumn: params.categoryColumn
};
default:
return null;
}
}

View file

@ -1,11 +1,28 @@
import { getColorForIndex } from './getColorForIndex';
import { ChartConfig } from './parseChartParams';
import { ChartConfig, TimeSeriesChartConfig, PieChartConfig } from './parseChartParams';
function isTimeSeriesConfig(config: ChartConfig): config is TimeSeriesChartConfig {
return config.chartType === 'line' || config.chartType === 'bar';
}
function isPieConfig(config: ChartConfig): config is PieChartConfig {
return config.chartType === 'pie';
}
export function processChartData(rowObj: any, config: ChartConfig) {
if (isPieConfig(config)) {
const labels = rowObj.values.map((row: any[]) => row[0]);
const datasets = [{
data: rowObj.values.map((row: any[]) => row[1]),
label: config.valueColumn
}];
return { labels, datasets };
}
let labels: any[];
let datasets: any[];
if (config.groupBy) {
if (config.categoryColumn) {
const { labels: groupLabels, datasets: groupDatasets } = processGroupedData(rowObj, config);
labels = groupLabels;
datasets = groupDatasets;
@ -17,7 +34,8 @@ export function processChartData(rowObj: any, config: ChartConfig) {
return { labels, datasets };
}
function processGroupedData(rowObj: any, config: ChartConfig) {
function processGroupedData(rowObj: any, config: TimeSeriesChartConfig) {
const groupedData = new Map();
rowObj.values.forEach((row: any[]) => {
@ -46,8 +64,30 @@ function processGroupedData(rowObj: any, config: ChartConfig) {
return { labels, datasets };
}
function processUngroupedData(rowObj: any, config: ChartConfig) {
return config.yColumns.map((col, index) => ({
function processUngroupedData(rowObj: any, config: TimeSeriesChartConfig) {
if (config.chartType === 'line') {
return [{
label: config.yColumns[0],
data: rowObj.values.map((row: any[]) => row[1]),
borderColor: getColorForIndex(0),
backgroundColor: getColorForIndex(0, 0.2),
fill: false,
tension: 0.1
}];
}
if (config.chartType === 'bar') {
return [{
label: config.yColumns[0],
data: rowObj.values.map((row: any[]) => row[1]),
backgroundColor: getColorForIndex(0, 0.6),
hoverBackgroundColor: getColorForIndex(0, 0.8),
borderColor: getColorForIndex(0),
borderWidth: 1
}];
}
return config.yColumns.map((col: string, index: number) => ({
label: col,
data: rowObj.values.map((row: any[]) => row[index + 1]),
borderColor: getColorForIndex(index),

View file

@ -4,9 +4,15 @@ export async function validateColumns(db: any, config: ChartConfig) {
const tableInfo = db.exec(`PRAGMA table_info("${config.table}");`);
const availableColumns = tableInfo[0].values.map((row: any[]) => row[1]);
const requestedColumns = [config.xColumn, ...config.yColumns];
if (config.groupBy) {
requestedColumns.push(config.groupBy);
let requestedColumns = [];
if (config.chartType === 'pie') {
requestedColumns = [config.categoryColumn, config.valueColumn];
} else {
requestedColumns = [config.xColumn, ...config.yColumns];
if (config.categoryColumn) {
requestedColumns.push(config.categoryColumn);
}
}
const invalidColumns = requestedColumns.filter(col => !availableColumns.includes(col));

View file

@ -13,7 +13,11 @@ export async function processSqlChartBlock(dbService: DBService, source: string,
const config = parseChartParams(source);
if (!config) {
el.createEl("p", { text: "Required parameters are: table, xColumn, yColumns" });
// @ts-ignore
const message = config?.chartType === 'pie'
? "Required parameters for pie chart are: table, categoryColumn, valueColumn"
: "Required parameters are: table, xColumn, yColumns";
el.createEl("p", { text: message });
return;
}