mirror of
https://github.com/h-sphere/sql-seal-charts.git
synced 2026-07-22 05:37:34 +00:00
Merge pull request #1 from h-sphere/feat/advanced-mode-with-js
feat: Advanced Mode that allows you to use full JavaScript syntax
This commit is contained in:
commit
c220fa20ed
13 changed files with 1161 additions and 783 deletions
|
|
@ -1,3 +1,7 @@
|
|||
# 0.2.0 (2025-03-15)
|
||||
feat: You can now use "ADVANCED MODE" to use full JavaScript to customise your charts
|
||||
feat: Enabling EChart-Stat module. You can now perform regression, clustering and histogram operations directly in Obsidian!
|
||||
|
||||
# 0.1.1 (2025-02-01)
|
||||
chore: Fixed typing in configParser
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ export default defineConfig({
|
|||
text: 'Documentation',
|
||||
items: [
|
||||
{ text: 'Quick Start', link: '/quick-start' },
|
||||
{ text: 'Syntax', link: '/syntax' }
|
||||
{ text: 'Syntax', link: '/syntax' },
|
||||
{ text: 'Advanced Mode', link: '/advanced-mode' },
|
||||
{ text: 'Data Analysis Features', link: '/data-analysis-features' }
|
||||
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
33
docs/advanced-mode.md
Normal file
33
docs/advanced-mode.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Advanced Mode
|
||||
You can turn on Advanced mode which will enable you to write pure JavaScript. You have access to your query result and helper functions inside it so you can use it to transform your data, perform additional logic, etc.
|
||||
In advanced mode you need to return manually the object that will be used as a chart configuration in the end.
|
||||
|
||||
## Example
|
||||
|
||||
More advanced examples to be added later.
|
||||
|
||||
```sqlseal
|
||||
ADVANCED MODE
|
||||
-- now you can use all JavaScript syntax to your heart content
|
||||
CHART
|
||||
const values = (new Array(5)).map((_, i) => i)
|
||||
return {
|
||||
tooltip: {},
|
||||
legend: {
|
||||
data: ['sales']
|
||||
},
|
||||
xAxis: {
|
||||
data: ['Shirts', 'Cardigans', 'Chiffons', 'Pants', 'Heels', 'Socks']
|
||||
},
|
||||
yAxis: {},
|
||||
series: [
|
||||
{
|
||||
name: 'sales',
|
||||
type: 'bar',
|
||||
data: [5, 20, 36, 10, 10, 20]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
SELECT * FROM files
|
||||
```
|
||||
BIN
docs/clustering-example.png
Normal file
BIN
docs/clustering-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
258
docs/data-analysis-features.md
Normal file
258
docs/data-analysis-features.md
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# Data Analysis Features
|
||||
SQLSeal Chart comes integrated with EChart-Stat module that allows you to perform:
|
||||
- clustering (i.e. KMeans)
|
||||
- regression
|
||||
- histogram generation
|
||||
|
||||
## Enabling the feature
|
||||
The feature is enabled by default, no extra work is needed
|
||||
|
||||
## Examples
|
||||
|
||||
### Clustering
|
||||
You can generate your data cluster. This demo is based on the official demo from ECharts [that can be found here](https://echarts.apache.org/examples/en/editor.html?c=scatter-clustering).
|
||||
|
||||

|
||||
|
||||
```sqlseal
|
||||
TABLE clustering = file(./Clustering Data.csv)
|
||||
ADVANCED MODE
|
||||
CHART
|
||||
|
||||
const datasetArray = data.map(d => ([d.x, d.y]))
|
||||
|
||||
var CLUSTER_COUNT = 6;
|
||||
var DIENSIION_CLUSTER_INDEX = 2;
|
||||
var COLOR_ALL = [
|
||||
'#37A2DA',
|
||||
'#e06343',
|
||||
'#37a354',
|
||||
'#b55dba',
|
||||
'#b5bd48',
|
||||
'#8378EA',
|
||||
'#96BFFF'
|
||||
];
|
||||
var pieces = [];
|
||||
for (var i = 0; i < CLUSTER_COUNT; i++) {
|
||||
pieces.push({
|
||||
value: i,
|
||||
label: 'cluster ' + i,
|
||||
color: COLOR_ALL[i]
|
||||
});
|
||||
}
|
||||
return {
|
||||
dataset: [
|
||||
{
|
||||
source: datasetArray
|
||||
},
|
||||
{
|
||||
transform: {
|
||||
type: 'ecStat:clustering',
|
||||
print: true,
|
||||
config: {
|
||||
clusterCount: CLUSTER_COUNT,
|
||||
outputType: 'single',
|
||||
outputClusterIndexDimension: DIENSIION_CLUSTER_INDEX
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
position: 'top'
|
||||
},
|
||||
visualMap: {
|
||||
type: 'piecewise',
|
||||
top: 'middle',
|
||||
min: 0,
|
||||
max: CLUSTER_COUNT,
|
||||
left: 10,
|
||||
splitNumber: CLUSTER_COUNT,
|
||||
dimension: DIENSIION_CLUSTER_INDEX,
|
||||
pieces: pieces
|
||||
},
|
||||
grid: {
|
||||
left: 120
|
||||
},
|
||||
xAxis: {},
|
||||
yAxis: {},
|
||||
series: {
|
||||
type: 'scatter',
|
||||
encode: { tooltip: [0, 1] },
|
||||
symbolSize: 15,
|
||||
itemStyle: {
|
||||
borderColor: '#555'
|
||||
},
|
||||
datasetIndex: 1
|
||||
}
|
||||
};
|
||||
|
||||
SELECT * FROM clustering
|
||||
```
|
||||
|
||||
|
||||
Sample data:
|
||||
| x | y |
|
||||
| --------- | ---------- |
|
||||
| 3.275154 | 2.957587 |
|
||||
| -3.344465 | 2.603513 |
|
||||
| 0.355083 | -3.376585 |
|
||||
| 1.852435 | 3.547351 |
|
||||
| -2.078973 | 2.552013 |
|
||||
| -0.993756 | -0.884433 |
|
||||
| 2.682252 | 4.007573 |
|
||||
| -3.087776 | 2.878713 |
|
||||
| -1.565978 | -1.256985 |
|
||||
| 2.441611 | 0.444826 |
|
||||
| -0.659487 | 3.111284 |
|
||||
| -0.459601 | -2.618005 |
|
||||
| 2.17768 | 2.387793 |
|
||||
| -2.920969 | 2.917485 |
|
||||
| -0.028814 | -4.168078 |
|
||||
| 3.625746 | 2.119041 |
|
||||
| -3.912363 | 1.325108 |
|
||||
| -0.551694 | -2.814223 |
|
||||
| 2.855808 | 3.483301 |
|
||||
| -3.594448 | 2.856651 |
|
||||
| 0.421993 | -2.372646 |
|
||||
| 1.650821 | 3.407572 |
|
||||
| -2.082902 | 3.384412 |
|
||||
| -0.718809 | -2.492514 |
|
||||
| 4.513623 | 3.841029 |
|
||||
| -4.822011 | 4.607049 |
|
||||
| -0.656297 | -1.449872 |
|
||||
| 1.919901 | 4.439368 |
|
||||
| -3.287749 | 3.918836 |
|
||||
| -1.576936 | -2.977622 |
|
||||
| 3.598143 | 1.97597 |
|
||||
| -3.977329 | 4.900932 |
|
||||
| -1.79108 | -2.184517 |
|
||||
| 3.914654 | 3.559303 |
|
||||
| -1.910108 | 4.166946 |
|
||||
| -1.226597 | -3.317889 |
|
||||
| 1.148946 | 3.345138 |
|
||||
| -2.113864 | 3.548172 |
|
||||
| 0.845762 | -3.589788 |
|
||||
| 2.629062 | 3.535831 |
|
||||
| -1.640717 | 2.990517 |
|
||||
| -1.881012 | -2.485405 |
|
||||
| 4.606999 | 3.510312 |
|
||||
| -4.366462 | 4.023316 |
|
||||
| 0.765015 | -3.00127 |
|
||||
| 3.121904 | 2.173988 |
|
||||
| -4.025139 | 4.65231 |
|
||||
| -0.559558 | -3.840539 |
|
||||
| 4.376754 | 4.863579 |
|
||||
| -1.874308 | 4.032237 |
|
||||
| -0.089337 | -3.026809 |
|
||||
| 3.997787 | 2.518662 |
|
||||
| -3.082978 | 2.884822 |
|
||||
| 0.845235 | -3.454465 |
|
||||
| 1.327224 | 3.358778 |
|
||||
| -2.889949 | 3.596178 |
|
||||
| -0.966018 | -2.839827 |
|
||||
| 2.960769 | 3.079555 |
|
||||
| -3.275518 | 1.577068 |
|
||||
| 0.639276 | -3.41284 |
|
||||
|
||||
|
||||
### Regression
|
||||
You can use regression to match a function against your data. Following is the example from ECharts adapted for use in Obsidian:
|
||||
|
||||

|
||||
|
||||
```sqlseal
|
||||
TABLE regression = file(./Regression Data.csv)
|
||||
ADVANCED MODE
|
||||
CHART
|
||||
const dataArray = data.map(d => ([d.x, d.y]))
|
||||
|
||||
return {
|
||||
dataset: [
|
||||
{
|
||||
source: dataArray
|
||||
},
|
||||
{
|
||||
transform: {
|
||||
type: 'ecStat:regression',
|
||||
config: {
|
||||
method: 'exponential'
|
||||
// 'end' by default
|
||||
// formulaOn: 'start'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
title: {
|
||||
text: '1981 - 1998 gross domestic product GDP (trillion yuan)',
|
||||
subtext: 'By ecStat.regression',
|
||||
sublink: 'https://github.com/ecomfe/echarts-stat',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'scatter',
|
||||
type: 'scatter',
|
||||
datasetIndex: 0
|
||||
},
|
||||
{
|
||||
name: 'line',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
datasetIndex: 1,
|
||||
symbolSize: 0.1,
|
||||
symbol: 'circle',
|
||||
label: { show: true, fontSize: 16 },
|
||||
labelLayout: { dx: -20 },
|
||||
encode: { label: 2, tooltip: 1 }
|
||||
}
|
||||
]
|
||||
}
|
||||
SELECT * FROM regression
|
||||
```
|
||||
|
||||
Sample data:
|
||||
|
||||
| x | y |
|
||||
| --- | ------- |
|
||||
| 1 | 4862.4 |
|
||||
| 2 | 5294.7 |
|
||||
| 3 | 5934.5 |
|
||||
| 4 | 7171 |
|
||||
| 5 | 8964.4 |
|
||||
| 6 | 10202.2 |
|
||||
| 7 | 11962.5 |
|
||||
| 8 | 14928.3 |
|
||||
| 9 | 16909.2 |
|
||||
| 10 | 18547.9 |
|
||||
| 11 | 21617.8 |
|
||||
| 12 | 26638.1 |
|
||||
| 13 | 34634.4 |
|
||||
| 14 | 46759.4 |
|
||||
| 15 | 58478.1 |
|
||||
| 16 | 67884.6 |
|
||||
| 17 | 74462.6 |
|
||||
| 18 | 79395.7 |
|
||||
|
||||
|
||||
### Histogram
|
||||
Example to be implemented. Check back soon!
|
||||
BIN
docs/regression-example.png
Normal file
BIN
docs/regression-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "sqlseal-charts",
|
||||
"name": "SQLSeal Charts",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"isDesktopOnly": false,
|
||||
"description": "Charts extension for SQLSeal plugin. Generate pie charts, bar charts, line charts and more using data stored in your vault!",
|
||||
|
|
|
|||
27
package.json
27
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sqlseal-charts",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Chart extension for SQLSeal Obsidian plugin",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -31,29 +31,30 @@
|
|||
"@types/acorn": "^6.0.4",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.12.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"@types/node": "^22.13.10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"esbuild": "^0.25.1",
|
||||
"jest": "^29.7.0",
|
||||
"obsidian": "^1.7.2",
|
||||
"ts-jest": "^29.2.5",
|
||||
"obsidian": "^1.8.7",
|
||||
"ts-jest": "^29.2.6",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hypersphere/sqlseal": "^0.21.0",
|
||||
"@hypersphere/sqlseal": "^0.28.0",
|
||||
"@types/leaflet": "^1.9.16",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@vanakat/plugin-api": "^0.2.1",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn": "^8.14.1",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"echarts-stat": "^1.2.0",
|
||||
"json5": "^2.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"vitepress": "^1.6.3",
|
||||
"vue": "^3.5.13",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
1539
pnpm-lock.yaml
1539
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -2,38 +2,74 @@ import { App } from "obsidian";
|
|||
import { parseCode } from "./utils/configParser";
|
||||
import { prepareDataVariables } from "./utils/prepareDataVariables";
|
||||
import * as echarts from 'echarts';
|
||||
import * as ecStat from 'echarts-stat';
|
||||
import type { RendererConfig } from "@hypersphere/sqlseal";
|
||||
import { ViewDefinition } from "@hypersphere/sqlseal/dist/src/grammar/parser";
|
||||
import { parseCodeAdvanced } from "./utils/advancedParser";
|
||||
|
||||
interface Config {
|
||||
config: string
|
||||
}
|
||||
|
||||
console.log((ecStat as any).transform)
|
||||
|
||||
echarts.registerTransform((ecStat as any).transform.clustering);
|
||||
echarts.registerTransform((ecStat as any).transform.regression);
|
||||
echarts.registerTransform((ecStat as any).transform.histogram);
|
||||
|
||||
export class ChartRenderer implements RendererConfig {
|
||||
|
||||
constructor(private readonly app: App) {
|
||||
}
|
||||
|
||||
get viewDefinition(): ViewDefinition {
|
||||
return {
|
||||
argument: 'javascriptTemplate',
|
||||
name: 'chart',
|
||||
singleLine: false
|
||||
}
|
||||
}
|
||||
|
||||
get rendererKey() {
|
||||
return 'chart'
|
||||
}
|
||||
|
||||
validateConfig(config: string): Config {
|
||||
return { config }
|
||||
return { config: config.trim() }
|
||||
}
|
||||
|
||||
render(config: Config, el: HTMLElement) {
|
||||
let isRendered: boolean = false
|
||||
let chart: echarts.ECharts | null = null
|
||||
return {
|
||||
render: ({ columns, data }: { columns: string[], data: Record<string, unknown>[]}) => {
|
||||
render: ({ columns, data, flags }: { columns: string[], data: Record<string, unknown>[], flags: Record<string, boolean> }) => {
|
||||
|
||||
const isAdvancedMode = !!(flags?.isAdvancedMode)
|
||||
|
||||
if (config.config[0] !== '{' && !isAdvancedMode) {
|
||||
throw new Error('To process JavaScript, set ADVANCED MODE flag')
|
||||
}
|
||||
const { functions, variables } = prepareDataVariables({ columns, data })
|
||||
const parsedConfig = parseCode(config.config, functions, variables)
|
||||
|
||||
let parsedConfig: Object = {}
|
||||
if (isAdvancedMode) {
|
||||
try {
|
||||
parsedConfig = parseCodeAdvanced({ functions, variables }, config.config)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
parsedConfig = parseCode(config.config, functions, variables) as Object
|
||||
}
|
||||
if (!parsedConfig || typeof parsedConfig !== 'object') {
|
||||
throw new Error('Issue with parsing config')
|
||||
}
|
||||
const configRecord = parsedConfig as Record<string, any>
|
||||
const dataset = [{ id: 'data', source: data }, ...(configRecord.dataset ?? [])]
|
||||
configRecord.dataset = dataset
|
||||
if (!configRecord.dataset) {
|
||||
const dataset = [{ id: 'data', source: data }, ...(configRecord.dataset ?? [])]
|
||||
configRecord.dataset = dataset
|
||||
}
|
||||
|
||||
if (isRendered) {
|
||||
// Data update
|
||||
|
|
|
|||
|
|
@ -13,5 +13,9 @@ export default class SQLSealCharts extends Plugin {
|
|||
const api = pluginApi('sqlseal') as SQLSealRegisterApi
|
||||
const registar = api.registerForPlugin(this)
|
||||
registar.registerView('sqlseal-charts', new ChartRenderer(this.app))
|
||||
|
||||
if (api.apiVersion >= 2) {
|
||||
registar.registerFlag({ key: 'isAdvancedMode', name: 'ADVANCED MODE' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
src/utils/advancedParser.ts
Normal file
22
src/utils/advancedParser.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { uniqBy } from "lodash"
|
||||
|
||||
type Params = {
|
||||
functions: Record<string, any>,
|
||||
variables: Record<string, any>
|
||||
}
|
||||
|
||||
export const parseCodeAdvanced = ({ functions, variables }: Params, code: string) => {
|
||||
const { keys, values } = uniqBy([
|
||||
...Object.entries(functions),
|
||||
...Object.entries({
|
||||
data: variables.data,
|
||||
columns: variables.columns
|
||||
})
|
||||
], 0).reduce(({ keys, values}, [key, value]) => ({
|
||||
keys: [...keys, key],
|
||||
values: [...values, value]
|
||||
}), { keys: [], values: []})
|
||||
const fn = new Function(...keys, code)
|
||||
|
||||
return fn(...values)
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"0.1.0": "0.15.0",
|
||||
"0.1.1": "0.15.0"
|
||||
"0.1.1": "0.15.0",
|
||||
"0.2.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue