mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
created piechart
This commit is contained in:
parent
479f1ad8a5
commit
7d255212cd
7 changed files with 261 additions and 59 deletions
97
src/helpers/processSqlChartBlock/charts/PieChart.ts
Normal file
97
src/helpers/processSqlChartBlock/charts/PieChart.ts
Normal 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}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue