diff --git a/src/helpers/processSqlChartBlock/charts/PieChart.ts b/src/helpers/processSqlChartBlock/charts/PieChart.ts new file mode 100644 index 0000000..80ad2e2 --- /dev/null +++ b/src/helpers/processSqlChartBlock/charts/PieChart.ts @@ -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}%)`; + } + } + } + } + } + }; +} \ No newline at end of file diff --git a/src/helpers/processSqlChartBlock/charts/index.ts b/src/helpers/processSqlChartBlock/charts/index.ts index 0e712d1..c21e693 100644 --- a/src/helpers/processSqlChartBlock/charts/index.ts +++ b/src/helpers/processSqlChartBlock/charts/index.ts @@ -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 diff --git a/src/helpers/processSqlChartBlock/helpers/buildSqlQuery.ts b/src/helpers/processSqlChartBlock/helpers/buildSqlQuery.ts index 91aae34..5d4e8e9 100644 --- a/src/helpers/processSqlChartBlock/helpers/buildSqlQuery.ts +++ b/src/helpers/processSqlChartBlock/helpers/buildSqlQuery.ts @@ -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 }; diff --git a/src/helpers/processSqlChartBlock/helpers/parseChartParams.ts b/src/helpers/processSqlChartBlock/helpers/parseChartParams.ts index 3d66fea..7102d18 100644 --- a/src/helpers/processSqlChartBlock/helpers/parseChartParams.ts +++ b/src/helpers/processSqlChartBlock/helpers/parseChartParams.ts @@ -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; + } } \ No newline at end of file diff --git a/src/helpers/processSqlChartBlock/helpers/processChartData.ts b/src/helpers/processSqlChartBlock/helpers/processChartData.ts index 32150b5..47eeae1 100644 --- a/src/helpers/processSqlChartBlock/helpers/processChartData.ts +++ b/src/helpers/processSqlChartBlock/helpers/processChartData.ts @@ -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), diff --git a/src/helpers/processSqlChartBlock/helpers/validateColumns.ts b/src/helpers/processSqlChartBlock/helpers/validateColumns.ts index 7461495..f54c7cc 100644 --- a/src/helpers/processSqlChartBlock/helpers/validateColumns.ts +++ b/src/helpers/processSqlChartBlock/helpers/validateColumns.ts @@ -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)); diff --git a/src/helpers/processSqlChartBlock/index.ts b/src/helpers/processSqlChartBlock/index.ts index 384cbc2..f5487ee 100644 --- a/src/helpers/processSqlChartBlock/index.ts +++ b/src/helpers/processSqlChartBlock/index.ts @@ -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; }