mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
created chart block
This commit is contained in:
parent
9e425ec19a
commit
af908987ff
12 changed files with 446 additions and 2 deletions
9
main.ts
9
main.ts
|
|
@ -13,7 +13,7 @@ import {
|
|||
|
||||
import { DBService } from "./src/dbService";
|
||||
import { inspectTableStructure, convertEntriesInNotes } from "./src/commands";
|
||||
import { pickTableName, processSqlBlock } from "./src/helpers";
|
||||
import { pickTableName, processSqlBlock, processSqlChartBlock } from "./src/helpers";
|
||||
import { SqliteDBSettings, DEFAULT_SETTINGS } from "./src/types";
|
||||
|
||||
export default class SqliteDBPlugin extends Plugin {
|
||||
|
|
@ -62,6 +62,13 @@ export default class SqliteDBPlugin extends Plugin {
|
|||
}
|
||||
);
|
||||
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"sql-chart",
|
||||
async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
|
||||
await processSqlChartBlock(this.dbService, source, el);
|
||||
}
|
||||
);
|
||||
|
||||
this.addSettingTab(new SqliteDBSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { pickTableName } from "./pickTableName";
|
||||
import { processSqlBlock } from "./processSqlBlock";
|
||||
import { processSqlChartBlock } from "./processSqlChartBlock/index";
|
||||
|
||||
export { pickTableName, processSqlBlock };
|
||||
export { pickTableName, processSqlBlock, processSqlChartBlock };
|
||||
|
|
|
|||
29
src/helpers/processSqlChartBlock/charts/BarChart.ts
Normal file
29
src/helpers/processSqlChartBlock/charts/BarChart.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { getColorForIndex } from "../helpers/getColorForIndex";
|
||||
|
||||
interface BarChartOptions {
|
||||
stacked?: boolean;
|
||||
}
|
||||
|
||||
export function createBarChart(labels: string[], datasets: any[], options?: BarChartOptions) {
|
||||
return {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: datasets.map((dataset, index) => ({
|
||||
...dataset,
|
||||
backgroundColor: getColorForIndex(index, 0.6),
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
stacked: options?.stacked
|
||||
},
|
||||
x: {
|
||||
stacked: options?.stacked
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
124
src/helpers/processSqlChartBlock/charts/LineChart.ts
Normal file
124
src/helpers/processSqlChartBlock/charts/LineChart.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
interface LineChartOptions {
|
||||
fill?: boolean;
|
||||
tension?: number;
|
||||
pointRadius?: number;
|
||||
pointHoverRadius?: number;
|
||||
showLegend?: boolean;
|
||||
yAxisMin?: number;
|
||||
yAxisMax?: number;
|
||||
tooltips?: boolean;
|
||||
animations?: boolean;
|
||||
}
|
||||
|
||||
export function createLineChart(labels: string[], datasets: any[], options: LineChartOptions = {}) {
|
||||
const defaultOptions: LineChartOptions = {
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5,
|
||||
showLegend: true,
|
||||
tooltips: true,
|
||||
animations: true
|
||||
};
|
||||
|
||||
const chartOptions = { ...defaultOptions, ...options };
|
||||
|
||||
return {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: datasets.map((dataset, index) => ({
|
||||
...dataset,
|
||||
fill: chartOptions.fill,
|
||||
tension: chartOptions.tension,
|
||||
pointRadius: chartOptions.pointRadius,
|
||||
pointHoverRadius: chartOptions.pointHoverRadius,
|
||||
borderWidth: 2,
|
||||
// Point styling
|
||||
pointBackgroundColor: dataset.borderColor,
|
||||
pointBorderColor: dataset.borderColor,
|
||||
pointBorderWidth: 1,
|
||||
// Hover effects
|
||||
hoverBorderWidth: 2,
|
||||
hoverBorderColor: dataset.borderColor,
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
// maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
animations: chartOptions.animations ? {
|
||||
tension: {
|
||||
duration: 1000,
|
||||
easing: 'linear',
|
||||
from: 0.4,
|
||||
to: 0.3,
|
||||
}
|
||||
} : false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: chartOptions.showLegend,
|
||||
position: 'top' as const,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: chartOptions.tooltips,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleFont: {
|
||||
size: 13
|
||||
},
|
||||
bodyFont: {
|
||||
size: 12
|
||||
},
|
||||
padding: 10,
|
||||
cornerRadius: 4,
|
||||
displayColors: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: true,
|
||||
drawOnChartArea: true,
|
||||
drawTicks: true,
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 11
|
||||
},
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
min: chartOptions.yAxisMin,
|
||||
max: chartOptions.yAxisMax,
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: true,
|
||||
drawOnChartArea: true,
|
||||
drawTicks: true,
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
16
src/helpers/processSqlChartBlock/charts/index.ts
Normal file
16
src/helpers/processSqlChartBlock/charts/index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { createLineChart } from './LineChart';
|
||||
import { createBarChart } from './BarChart';
|
||||
|
||||
export type ChartType = 'line' | 'bar' | 'scatter' | 'pie';
|
||||
|
||||
export function createChart(type: ChartType, labels: string[], datasets: any[], options?: any) {
|
||||
switch (type) {
|
||||
case 'line':
|
||||
return createLineChart(labels, datasets, options);
|
||||
case 'bar':
|
||||
return createBarChart(labels, datasets, options);
|
||||
//todo add pie and others
|
||||
default:
|
||||
return createLineChart(labels, datasets); // fallback to line chart
|
||||
}
|
||||
}
|
||||
34
src/helpers/processSqlChartBlock/helpers/buildSqlQuery.ts
Normal file
34
src/helpers/processSqlChartBlock/helpers/buildSqlQuery.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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}"
|
||||
`;
|
||||
}
|
||||
|
||||
if (config.startDate && config.endDate) {
|
||||
query += ` WHERE ${config.xColumn} BETWEEN ? AND ?`;
|
||||
params.push(config.startDate, config.endDate);
|
||||
}
|
||||
|
||||
query += ` ORDER BY ${config.xColumn}`;
|
||||
if (config.groupBy) {
|
||||
query += `, ${config.groupBy}`;
|
||||
}
|
||||
|
||||
return { query, params };
|
||||
}
|
||||
11
src/helpers/processSqlChartBlock/helpers/getColorForIndex.ts
Normal file
11
src/helpers/processSqlChartBlock/helpers/getColorForIndex.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export function getColorForIndex(index: number, alpha: number = 1): string {
|
||||
const colors = [
|
||||
`rgba(255, 99, 132, ${alpha})`, // red
|
||||
`rgba(54, 162, 235, ${alpha})`, // blue
|
||||
`rgba(75, 192, 192, ${alpha})`, // green
|
||||
`rgba(255, 206, 86, ${alpha})`, // yellow
|
||||
`rgba(153, 102, 255, ${alpha})`, // purple
|
||||
`rgba(255, 159, 64, ${alpha})`, // orange
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
6
src/helpers/processSqlChartBlock/helpers/index.ts
Normal file
6
src/helpers/processSqlChartBlock/helpers/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { parseChartParams } from "./parseChartParams";
|
||||
import { validateColumns } from "./validateColumns";
|
||||
import { buildSqlQuery } from "./buildSqlQuery";
|
||||
import { processChartData } from "./processChartData";
|
||||
|
||||
export { parseChartParams, validateColumns, buildSqlQuery, processChartData };
|
||||
82
src/helpers/processSqlChartBlock/helpers/parseChartParams.ts
Normal file
82
src/helpers/processSqlChartBlock/helpers/parseChartParams.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
export interface ChartOptions {
|
||||
fill?: boolean;
|
||||
tension?: number;
|
||||
pointRadius?: number;
|
||||
pointHoverRadius?: number;
|
||||
showLegend?: boolean;
|
||||
yAxisMin?: number;
|
||||
yAxisMax?: number;
|
||||
tooltips?: boolean;
|
||||
animations?: boolean;
|
||||
}
|
||||
|
||||
export interface ChartConfig {
|
||||
table: string;
|
||||
xColumn: string;
|
||||
yColumns: string[];
|
||||
groupBy?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
chartType: string;
|
||||
chartOptions?: ChartOptions;
|
||||
}
|
||||
|
||||
export function parseChartParams(source: string): ChartConfig | null {
|
||||
const lines = source.split("\n");
|
||||
const params: any = {};
|
||||
let chartOptions: any = {};
|
||||
let inChartOptions = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
// Handle chartOptions block
|
||||
if (trimmed === 'chartOptions: {') {
|
||||
inChartOptions = true;
|
||||
continue;
|
||||
}
|
||||
if (trimmed === '}') {
|
||||
inChartOptions = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
else if (!isNaN(Number(value))) value = Number(value);
|
||||
chartOptions[key] = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle regular parameters
|
||||
const parts = trimmed.split(":").map((p) => p.trim());
|
||||
if (parts.length >= 2) {
|
||||
const key = parts[0];
|
||||
const val = parts.slice(1).join(":").trim();
|
||||
params[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.table || !params.xColumn || !params.yColumns) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
table: params.table,
|
||||
xColumn: params.xColumn,
|
||||
yColumns: params.yColumns.split(',').map((c: string) => c.trim()),
|
||||
groupBy: params.groupBy?.trim(),
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate,
|
||||
chartType: params.chartType || 'line',
|
||||
chartOptions: Object.keys(chartOptions).length > 0 ? chartOptions : undefined
|
||||
};
|
||||
}
|
||||
58
src/helpers/processSqlChartBlock/helpers/processChartData.ts
Normal file
58
src/helpers/processSqlChartBlock/helpers/processChartData.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { getColorForIndex } from './getColorForIndex';
|
||||
import { ChartConfig } from './parseChartParams';
|
||||
|
||||
export function processChartData(rowObj: any, config: ChartConfig) {
|
||||
let labels: any[];
|
||||
let datasets: any[];
|
||||
|
||||
if (config.groupBy) {
|
||||
const { labels: groupLabels, datasets: groupDatasets } = processGroupedData(rowObj, config);
|
||||
labels = groupLabels;
|
||||
datasets = groupDatasets;
|
||||
} else {
|
||||
labels = rowObj.values.map((row: any[]) => row[0]);
|
||||
datasets = processUngroupedData(rowObj, config);
|
||||
}
|
||||
|
||||
return { labels, datasets };
|
||||
}
|
||||
|
||||
function processGroupedData(rowObj: any, config: ChartConfig) {
|
||||
const groupedData = new Map();
|
||||
|
||||
rowObj.values.forEach((row: any[]) => {
|
||||
const x = row[0];
|
||||
const group = String(row[1]);
|
||||
const y = row[2];
|
||||
|
||||
if (!groupedData.has(group)) {
|
||||
groupedData.set(group, { x: [], y: [] });
|
||||
}
|
||||
|
||||
groupedData.get(group).x.push(x);
|
||||
groupedData.get(group).y.push(y);
|
||||
});
|
||||
|
||||
const labels = Array.from(groupedData.values())[0].x;
|
||||
const datasets = Array.from(groupedData.entries()).map(([group, data], index) => ({
|
||||
label: group,
|
||||
data: data.y,
|
||||
borderColor: getColorForIndex(index),
|
||||
backgroundColor: getColorForIndex(index, 0.2),
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
}));
|
||||
|
||||
return { labels, datasets };
|
||||
}
|
||||
|
||||
function processUngroupedData(rowObj: any, config: ChartConfig) {
|
||||
return config.yColumns.map((col, index) => ({
|
||||
label: col,
|
||||
data: rowObj.values.map((row: any[]) => row[index + 1]),
|
||||
borderColor: getColorForIndex(index),
|
||||
backgroundColor: getColorForIndex(index, 0.2),
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
}));
|
||||
}
|
||||
21
src/helpers/processSqlChartBlock/helpers/validateColumns.ts
Normal file
21
src/helpers/processSqlChartBlock/helpers/validateColumns.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { ChartConfig } from "./parseChartParams";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const invalidColumns = requestedColumns.filter(col => !availableColumns.includes(col));
|
||||
if (invalidColumns.length > 0) {
|
||||
return {
|
||||
message: `The following columns do not exist: ${invalidColumns.join(", ")}`,
|
||||
availableColumns
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
55
src/helpers/processSqlChartBlock/index.ts
Normal file
55
src/helpers/processSqlChartBlock/index.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Notice } from "obsidian";
|
||||
import { DBService } from "../../dbService";
|
||||
import { parseChartParams, validateColumns, buildSqlQuery, processChartData } from "./helpers";
|
||||
import { ChartType, createChart } from "./charts";
|
||||
|
||||
export async function processSqlChartBlock(dbService: DBService, source: string, el: HTMLElement) {
|
||||
const db = dbService.getDB();
|
||||
if (!db) {
|
||||
new Notice("No DB loaded. Please open the DB first.");
|
||||
el.createEl("p", { text: "Database not loaded." });
|
||||
return;
|
||||
}
|
||||
|
||||
const config = parseChartParams(source);
|
||||
if (!config) {
|
||||
el.createEl("p", { text: "Required parameters are: table, xColumn, yColumns" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const validationError = await validateColumns(db, config);
|
||||
if (validationError) {
|
||||
el.createEl("p", { text: validationError.message });
|
||||
if (validationError.availableColumns) {
|
||||
el.createEl("p", { text: `Available columns are: ${validationError.availableColumns.join(", ")}` });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { query, params } = buildSqlQuery(config);
|
||||
const result = db.exec(query, params);
|
||||
|
||||
if (!result || result.length === 0 || result[0].values.length === 0) {
|
||||
el.createEl("p", { text: "No data found for the specified parameters." });
|
||||
return;
|
||||
}
|
||||
|
||||
const { labels, datasets } = processChartData(result[0], config);
|
||||
|
||||
const chartData = createChart(
|
||||
config.chartType as ChartType,
|
||||
labels,
|
||||
datasets,
|
||||
config.chartOptions
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
window.renderChart(chartData, el);
|
||||
|
||||
} catch (error) {
|
||||
el.createEl("p", { text: "An error occurred while processing the chart." });
|
||||
el.createEl("p", { text: String(error) });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue