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:
Kacper Kula 2025-03-15 11:53:00 +00:00 committed by GitHub
commit c220fa20ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1161 additions and 783 deletions

View file

@ -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

View file

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View 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).
![Clustering Example](./clustering-example.png)
```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:
![Regression Example](./regression-example.png)
```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

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

View file

@ -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!",

View file

@ -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"
}
}

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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' })
}
}
}

View 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)
}

View file

@ -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"
}