Compare commits

...

37 commits
0.1.1 ... main

Author SHA1 Message Date
Kacper Kula
c27d97e220
Merge pull request #15 from h-sphere/changeset-release/main
[Release] Next
2025-08-14 17:35:52 +02:00
github-actions[bot]
1d12289e74 release: Release Next 2025-08-14 15:35:41 +00:00
Kacper Kula
ff048f5eba
Merge pull request #14 from h-sphere/fix/pass-columns-in-advanced-mode
fix: fixing issue with columns not being directly available in Advanced Mode
2025-08-14 17:35:13 +02:00
Kacper Kula
9775903ae1 chore: adding changeset for release 2025-08-14 16:33:11 +01:00
Kacper Kula
46ab0b037c fix: fixing issue with columns not being directly available in Advanced Mode 2025-08-14 16:30:22 +01:00
Kacper Kula
c70eeec9c5
Merge pull request #13 from h-sphere/changeset-release/main
[Release] Next
2025-08-14 17:17:42 +02:00
github-actions[bot]
68749173b0 release: Release Next 2025-08-14 15:15:15 +00:00
Kacper Kula
7c854f22b6
Merge pull request #12 from h-sphere/fix/global-config-in-advanced-mode
Fix: global config in advanced mode
2025-08-14 17:14:45 +02:00
Kacper Kula
c9237858f7 fix: added global configs to the advanced mode 2025-08-14 16:11:51 +01:00
Kacper Kula
4416495b79 chore: small ui fixes 2025-08-14 16:03:05 +01:00
Kacper Kula
ae6ef8869e
Merge pull request #11 from h-sphere/changeset-release/main
[Release] Next
2025-08-14 13:04:26 +02:00
github-actions[bot]
e8f6dd522d release: Release Next 2025-08-14 11:04:13 +00:00
Kacper Kula
7a7db26abb
Merge pull request #10 from h-sphere/feat/global-configurations
feat: adding global configurations
2025-08-14 13:03:53 +02:00
Kacper Kula
3b0da2bfcd chore: fixing styles 2025-08-14 11:59:38 +01:00
Kacper Kula
f359f0e587
Merge pull request #9 from h-sphere/feat/chart-full-screen
feat: now each chart can be opened in full screen
2025-08-14 12:45:51 +02:00
Kacper Kula
610cf9620b feat: improving chart modal, hiding button by default 2025-08-14 11:44:59 +01:00
Kacper Kula
299924673f feat: adding global configurations 2025-08-13 22:37:46 +01:00
Kacper Kula
22dbb66fcd feat: now each chart can be opened in full screen 2025-08-13 21:12:32 +01:00
Kacper Kula
e45ef49ed6 chore: fixing release script 2025-08-13 12:50:09 +01:00
Kacper Kula
1e562831da
Merge pull request #8 from h-sphere/changeset-release/main
[Release] Next
2025-08-13 13:47:58 +02:00
github-actions[bot]
39be40d358 release: Release Next 2025-08-13 11:47:02 +00:00
Kacper Kula
7de759b3d2
Merge pull request #7 from h-sphere/feat/refactor
feat: adjusted plugin after refactor
2025-08-13 13:46:41 +02:00
Kacper Kula
447548cece chore: adding changeset based publish 2025-08-13 12:45:54 +01:00
Kacper Kula
76a634cfc8 feat: added changeset 2025-08-13 12:43:55 +01:00
Kacper Kula
c3e8e6c6a9 feat: chart now reacts to resizes 2025-08-13 12:34:34 +01:00
Kacper Kula
ccf7b02733 feat: adjusted plugin after refactor 2025-07-22 23:21:54 +01:00
Kacper Kula
542ed504aa version: new version released 2025-04-09 14:50:46 +01:00
Kacper Kula
814e3e5df8
Merge pull request #6 from texastoland/bugfix/3-default-dataset
Dataset default is not being replaced by default but updated
2025-04-09 14:41:54 +01:00
Texas Toland
ef7ceed551 Provide explicit override for default dataset 2025-03-31 03:09:24 +00:00
Texas Toland
c41cbce218 Fix #3
Always prepends SQL data to dataset even if user defines additional configs.
2025-03-30 17:19:41 +00:00
Kacper Kula
c220fa20ed
Merge pull request #1 from h-sphere/feat/advanced-mode-with-js
feat: Advanced Mode that allows you to use full JavaScript syntax
2025-03-15 11:53:00 +00:00
Kacper Kula
b0bc7c05e3 docs: added examples 2025-03-15 11:51:47 +00:00
Kacper Kula
2918c79774 chore: version bump 2025-03-15 11:11:08 +00:00
Kacper Kula
0ecff2e158 docs: exposing advanced mode docs in the sidebar 2025-03-15 11:07:20 +00:00
Kacper Kula
37f746b72c feat: adding advanced mode 2025-03-15 11:06:44 +00:00
Kacper Kula
732577fae5 docs: adding few extra pages 2025-02-22 10:37:38 +01:00
Kacper Kula
39543065fa docs: initial docs version 2025-02-22 10:24:41 +01:00
46 changed files with 4511 additions and 668 deletions

8
.changeset/README.md Normal file
View file

@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

11
.changeset/config.json Normal file
View file

@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

32
.github/workflows/docs.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Deploy docs to FTP
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 21.x
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Build with VitePress
run: |
pnpm run docs:build
touch docs/.vitepress/dist/.nojekyll
- name: Deploy to FTP
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: docs/.vitepress/dist/
server-dir: ${{ secrets.FTP_SERVER_DIR }}

41
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Publish
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 18.x
cache: "pnpm"
- name: Get current version
id: get-version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
publish: pnpm run ci:publish
version: pnpm run ci:version
title: "[Release] ${{ steps.changesets.outputs.version || 'Next' }}"
commit: "release: Release ${{ steps.changesets.outputs.version || 'Next' }}"
createGithubReleases: false # this is handled inside ci:publish script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,88 +0,0 @@
name: Check Version and Release
on:
push:
branches:
- main
paths:
- 'package.json'
jobs:
check-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Get package version
id: package-version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
- name: Check if tag exists
id: check-tag
run: |
if git rev-parse "${{ steps.package-version.outputs.VERSION }}" >/dev/null 2>&1; then
echo "EXISTS=true" >> $GITHUB_OUTPUT
else
echo "EXISTS=false" >> $GITHUB_OUTPUT
fi
- name: Install packages
if: steps.check-tag.outputs.EXISTS == 'false'
run: npm install
- name: Build plugin
if: steps.check-tag.outputs.EXISTS == 'false'
run: |
npm run build
- name: Extract release notes
if: steps.check-tag.outputs.EXISTS == 'false'
id: release-notes
run: |
version="${{ steps.package-version.outputs.VERSION }}"
# Use awk to extract the section for the current version
awk -v version="$version" '
BEGIN { found=0; content="" }
/^# [0-9]+\.[0-9]+\.[0-9]+/ {
if (found) { exit }
if ($2 ~ "^"version"($| \\()") { found=1; next }
}
found { content = content $0 "\n" }
END { printf "%s", content }
' CHANGELOG.md > release_notes.txt
# Escape multiline output for GitHub Actions
echo "NOTES<<EOF" >> $GITHUB_OUTPUT
cat release_notes.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create tag and release
if: steps.check-tag.outputs.EXISTS == 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Create and push tag
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git tag -a "${{ steps.package-version.outputs.VERSION }}" -m "Release version ${{ steps.package-version.outputs.VERSION }}"
git push origin "${{ steps.package-version.outputs.VERSION }}"
# Create release
gh release create "${{ steps.package-version.outputs.VERSION }}" \
--title="${{ steps.package-version.outputs.VERSION }}" \
--notes="${{ steps.release-notes.outputs.NOTES }}" \
main.js manifest.json styles.css

View file

@ -1,5 +1,44 @@
# 0.2.1 (2025-04-09)
## 0.4.2
### Patch Changes
- 9775903: exposing columns in the advanced mode
## 0.4.1
### Patch Changes
- c923785: fixed issue with configs not being exposed in ADVANCED MODE
## 0.4.0
### Minor Changes
- 2999246: adding support for global configurations
### Patch Changes
- 22dbb66: now every chart can be opened in full screen
## 0.3.0
### Minor Changes
- 76a634c: improved resizing of the charts when container changes
- 76a634c: integrating with changes made in SQLSeal 0.38.0 to the external APIs
- fix: dataset is now not replaced but extended by default (by @texastoland)
# 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
# 0.1.0 (2025-01-31)
Initial release!
Initial release!

View file

@ -35,7 +35,7 @@ CHART {
SELECT * FROM finances
```
![Pie Chart Example](./docs/pie_chart.png)
![Pie Chart Example](./screenshots/pie_chart.png)
### Line Chart
```sqlseal
@ -56,7 +56,7 @@ GROUP BY created_date
ORDER BY created_date
```
![Line Chart Example](./docs/line_chart.png)
![Line Chart Example](./screenshots/line_chart.png)
## Syntax

View file

@ -0,0 +1,55 @@
import { defineConfig } from 'vitepress'
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "SQLSeal Charts",
description: "Charts extension for SQLSeal",
base: '/sql-seal-charts/',
head: [
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/sql-seal/favicon-32x32.png' }],
['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/sql-seal/favicon-16x16.png' }],
['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/sql-seal/apple-touch-icon.png' }],
],
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
{ text: 'Quick Start', link: '/quick-start' }
],
search: {
provider: 'local'
},
logo: '/logo.svg',
sidebar: [
{
text: 'Documentation',
items: [
{ text: 'Quick Start', link: '/quick-start' },
{ text: 'Syntax', link: '/syntax' },
{ text: 'Advanced Mode', link: '/advanced-mode' },
{ text: 'Data Analysis Features', link: '/data-analysis-features' }
]
},
{
text: 'Chart Types',
items: [
{ text: 'Line Chart', link: '/chart-type/line-chart' },
{ text: 'Bar Chart', link: '/chart-type/bar-chart' }
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/h-sphere/sql-seal' },
{ icon: 'discord', link: 'https://discord.gg/ZMRnFeAWXb' },
{ icon: 'bluesky', link: 'https://bsky.app/profile/hypersphereblog.bsky.social' }
],
footer: {
message: '',
copyright: 'By <a href="https://hypersphere.blog">hypersphere</a>.<br/>Sponsor Me: <a href="https://ko-fi.com/hypersphere">Ko-Fi</a>'
}
}
})

View file

@ -0,0 +1,108 @@
<script setup>
const stats = [
{
number: '2K+',
label: 'Downloads',
icon: '📥'
},
{
number: '39+',
label: 'Releases',
icon: '📦'
},
{
number: '51+',
label: 'GitHub Stars',
icon: '⭐'
},
{
number: '48+',
label: 'Discord Members',
icon: '🤝'
}
]
</script>
<template>
<ClientOnly>
<div class="stats">
<div class="stats-grid">
<div v-for="stat in stats" :key="stat.label" class="stat-card">
<span class="stat-icon" v-text="stat.icon" />
<span class="stat-number" v-text="stat.number" />
<span class="stat-label" v-text="stat.label" />
</div>
</div>
</div>
</ClientOnly>
</template>
<style scoped>
.stats {
margin: 3rem auto;
padding: 0 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
max-width: 1152px;
margin: 0 auto;
}
.stat-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
background-color: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-icon {
font-size: 2rem;
margin-bottom: 1rem;
}
.stat-number {
font-size: 2.25rem;
font-weight: 600;
color: var(--vp-c-brand);
line-height: 1.2;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.875rem;
color: var(--vp-c-text-2);
text-align: center;
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.stat-card {
padding: 1.5rem;
}
.stat-number {
font-size: 1.75rem;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1,18 @@
// https://vitepress.dev/guide/custom-theme
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './style.css'
import Stats from './components/Stats.vue'
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
},
enhanceApp({ app, router, siteData }) {
app.component('Stats', Stats)
}
} satisfies Theme

View file

@ -0,0 +1,135 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
*
* Each colors have exact same color scale system with 3 levels of solid
* colors with different brightness, and 1 soft color.
*
* - `XXX-1`: The most solid color used mainly for colored text. It must
* satisfy the contrast ratio against when used on top of `XXX-soft`.
*
* - `XXX-2`: The color used mainly for hover state of the button.
*
* - `XXX-3`: The color for solid background, such as bg color of the button.
* It must satisfy the contrast ratio with pure white (#ffffff) text on
* top of it.
*
* - `XXX-soft`: The color used for subtle background such as custom container
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
* on top of it.
*
* The soft color must be semi transparent alpha channel. This is crucial
* because it allows adding multiple "soft" colors on top of each other
* to create a accent, such as when having inline code block inside
* custom containers.
*
* - `default`: The color used purely for subtle indication without any
* special meanings attched to it such as bg color for menu hover state.
*
* - `brand`: Used for primary brand colors, such as link text, button with
* brand theme, etc.
*
* - `tip`: Used to indicate useful information. The default theme uses the
* brand color for this by default.
*
* - `warning`: Used to indicate warning to the users. Used in custom
* container, badges, etc.
*
* - `danger`: Used to show error, or dangerous message to the users. Used
* in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root {
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);
--vp-c-brand-1: var(--vp-c-indigo-1);
--vp-c-brand-2: var(--vp-c-indigo-2);
--vp-c-brand-3: var(--vp-c-indigo-3);
--vp-c-brand-soft: var(--vp-c-indigo-soft);
--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
--vp-c-tip-3: var(--vp-c-brand-3);
--vp-c-tip-soft: var(--vp-c-brand-soft);
--vp-c-warning-1: var(--vp-c-yellow-1);
--vp-c-warning-2: var(--vp-c-yellow-2);
--vp-c-warning-3: var(--vp-c-yellow-3);
--vp-c-warning-soft: var(--vp-c-yellow-soft);
--vp-c-danger-1: var(--vp-c-red-1);
--vp-c-danger-2: var(--vp-c-red-2);
--vp-c-danger-3: var(--vp-c-red-3);
--vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand-3);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
--vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-c-brand-1);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#8c55ff 30%,
#6979f5
);
--vp-home-hero-image-background-image: linear-gradient(-45deg, #8c55ff 50%, #6979f5 50%);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: transparent;
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}

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

1
docs/changelog.md Symbolic link
View file

@ -0,0 +1 @@
../CHANGELOG.md

View file

@ -0,0 +1,3 @@
# Bar Chart
Under construction.

View file

@ -0,0 +1,3 @@
# Line Chart
Under construction.

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!

View file

@ -0,0 +1,18 @@
# Comparison with Dataview
There are few other plugins which might sound like they do similar thing. The most popular of them, Dataview is an amazing project used by majority of Obsidian users so naturally I've got questions how does this compare to Dataview. Although I don't think this plugin is a replacement for dataview, and I personally use both of them on daily basis, here's the main comparison of the features:
| Feature | Dataview | SQLSeal |
|----------------------------------------------------|-------------------------------|--------------------------------------|
| Querying Data from your vault (files, tags, tasks) | ✅ | ✅ |
| Query language used | Dataview query language (DQL) | SQL (full compatibility with SQLite) |
| Mobile Support | ✅ | ✅ |
| Querying data files (CSV) | ❌ | ✅ |
| Editing data files (CSV) | ❌ | ✅ |
| Ability to join tables | ❌ | ✅ |
| Ability to filter data | ✅ | ✅ |
| Ability to perform basic aggregations | | |
| Support for inline queries | ✅ (using `=` prefix) | ✅ (using `S>` prefix) |
| Support for JavaScript | ✅ | ❌ (planned in the future) |
> [!NOTE]
> The table above is not-exhaustive and will be improved upon in the future.

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

27
docs/index.md Normal file
View file

@ -0,0 +1,27 @@
---
layout: home
hero:
name: "SQLSeal Charts"
# text: "Plugin enabling full SQL capabilities in Obsidian"
tagline: "Visualise your data"
image:
src: /logo.svg
alt: SQLSeal
actions:
- theme: brand
text: Get Started
link: /quick-start
# features:
# - title: Query data in your vault
# details: Use full power of SQL to select, join, filter data for your liking
# - title: Fully featured SQL Engine
# details: With SQLite under the hood, you can use all functionality of the database
# - title: Query your files and tags
# details: Use SQL to filter files in your vault
# - title: Add your own data
# details: Operate on your CSV files using SQL to process them on the go
---
CHARTS CHARTS CHARTS

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

26
docs/public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -0,0 +1,5 @@
id,name,customer,value
1,Bread,Mark,3.45
2,Milk,Mark,2.43
3,Press,Linda,4.55,
4,Cheese,Mark,3.22
1 id,name,customer,value
2 1,Bread,Mark,3.45
3 2,Milk,Mark,2.43
4 3,Press,Linda,4.55,
5 4,Cheese,Mark,3.22

69
docs/quick-start.md Normal file
View file

@ -0,0 +1,69 @@
# Quick Start
You get your first chart in less than 5 minutes! To do so, you need to install `SQLSeal` and `SQLSeal Charts`, create sample data source and write a query. This tutorial will guide you through the process step by step. By the end of it, you will get the following chart up and running:
SCREENSHOT HERE
## Installation
Before you start using SQLSeal Charts you need to install it.
> [!NOTE] SQLSeal vs SQLSeal Charts
> SQLSeal Charts **extends functionality of SQLSeal** so in order to use it, you need to install **both**.
- Go to community plugins and click Browse.
- Search for `SQLSeal`
- Install `SQLSeal` first
- Install `SQLSeal Charts` second
Now you have all plugins installed.
![Plugins](./images//community-plugins.png)
## Create dataset
To visualise data, you need the data to visualise. SQLSeal can visualise tables inside your markdown notes, datasets in common data formats (CSV, JSON) and your whole vault collection (your notes properties, etc). In this tutorial we will create a table inside your note which is the easiest way to get started. To learn more about data sources, check out SQLSeal documentation.
Create the following table inside your vault. You can copy the markdown provided below and paste it inside your Obsidian. In the end you should end up with a table.
| Category | Amount |
| ------------- | ------ |
| Grocery | 200 |
| Rent | 1500 |
| Entertainment | 130 |
| Bills | 400 |
```markdown
| Category | Amount |
| ------------- | ------ |
| Grocery | 200 |
| Rent | 1500 |
| Entertainment | 130 |
| Bills | 400 |
```
## Link the data
To visualise this data we need to create SQLSeal codeblock. In Obsidian a codeblock is denoted by three backtick symbols \`. If you are not sure how to create the codeblock, you can open your command palette in Obsidian (ctrl or cmd+P) and type `Insert code block`.
Next you need to specify the type of the block. This will tell Obsidian to render it in the Preview mode. you need to put `sqlseal` here.
Inside the code block type the following:
```sqlseal
TABLE data = table(0)
CHART
{
series: [{
type: 'pie'
}]
}
SELECT * FROM data
```
### Explenation of the code above
The code above might seem quite scary at first but I promise it all makes sense. It consists of 3 main parts:
- Table definition: here we define that we want to expose data from our markdown table as a table called `data`
- The second one is CHART view. This tells SQLSeal we want to render chart (there are other views available in the SQLSeal itself like table, list and grid). I takes configuration which tells it what type of chart we want. In our case we only want a pie chart.
- The last one is an SQL Select statement. Here you can define which data you want to use, combine data from different sources together or process the data. In our case we simply say: tale everything from our data table.
If you did everything correctly, you should see chart like below:
![Quick Start Chart](./images//quick-start-chart.png)
## Extras
The chart you've just created is interactive! You can hover on it but also you can change original data and see the data update accordingly. Try updating the data, adding or removing new rows. To speed up refresh, you can manually save the note by using CMD+S or CTRL+S.

BIN
docs/regression-example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

30
docs/syntax.md Normal file
View file

@ -0,0 +1,30 @@
# Syntax
SQLSeal Charts uses [ECharts](https://echarts.apache.org/en/index.html) under the hood. It automatically exposes data returned by your SQL query as a `data` dataset in ECharts. This means you can generate plenty of charts without worrying too much about how the data is being passed down. For more complex use-cases, you can always refer to the data by column name, it's index or even filter it down to create separate data-sets for different series.
To read more about datasets, [check out ECharts documentation](https://apache.github.io/echarts-handbook/en/concepts/dataset/).
## Variables
In addition SQLSeal exposes data returned by SQLSeal Query in the following variables:
| Variable Name | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `data` | Array of objects containing raw data |
| `columns` | array of column names |
| Object for each column | You can refer to each of the column data by their name, i.e. for `SELECT category, amount FROM data` you can use `category` and `amount` columns |
## Functions
Extra functions are exposed in SQLSeal Charts configuration object so you can use them:
| Function | Description |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `column(name: string)` | Returns array of the values for a specified column |
| `mean, max, min, uniq, uniqBy` | [Lodash](https://lodash.com/docs/4.17.15) functions to help with data processing |
| `array(...arrays)` | creates subarrays from arrays, i.e. `array([1,2], [3,4 ]) == [[1,3], [2,4]]`. Useful when grouping multiple columns together when reshaping the data |
| `assembleObjects(...definitions: { key: string, values: array }[])` | Assembles multiple arrays into array of objects using provided key values |
| `assemble(definition: Record<string, array>)` | Assembles multiple arrays into array of objects using a key=>value map |
## JavaScript functionality
Configuration has some basic JavaScript capabilities. For security reasons not all JavaScript syntax is available. This will change and more functions will be exposed.
For now the following JavaScript syntax is enabled:
- Arrow functions, i.e: `() => column('data')`
- Template literals, i.e.: \`${column('data')[0]}\`
More functionality will get exposed soon.

View file

@ -1,7 +1,7 @@
{
"id": "sqlseal-charts",
"name": "SQLSeal Charts",
"version": "0.1.1",
"version": "0.4.2",
"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.4.2",
"description": "Chart extension for SQLSeal Obsidian plugin",
"main": "main.js",
"scripts": {
@ -8,6 +8,12 @@
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"typecheck": "tsc -noEmit -skipLibCheck",
"release": "pnpm run build && changeset publish",
"ci:publish": "./scripts/tag-and-publish.sh",
"ci:version": "changeset version && pnpm run version",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"test": "jest",
"test:watch": "jest --watch"
},
@ -25,30 +31,36 @@
"author": "Kacper Kula",
"license": "MIT",
"devDependencies": {
"@changesets/cli": "^2.29.5",
"@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/json5": "^2.2.0",
"@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",
"zod": "^3.24.1"
"safe-identifier": "^0.4.2",
"vitepress": "^1.6.3",
"vue": "^3.5.13",
"zod": "^3.24.2"
}
}

File diff suppressed because it is too large Load diff

View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

72
scripts/tag-and-publish.sh Executable file
View file

@ -0,0 +1,72 @@
#!/bin/bash
# Get version - you'll need to pass this as an argument or set it another way
if [ -n "$1" ]; then
version="$1"
else
# Extract version from package.json more reliably
version=$(node -p "require('./package.json').version" 2>/dev/null || echo "")
# Fallback to grep/sed if node method fails
if [ -z "$version" ]; then
version=$(grep '"version":' package.json | head -n1 | sed -E 's/.*"version": "([^"]+)".*/\1/' | tr -d '\n\r')
fi
fi
# Clean the version string of any whitespace/newlines
version=$(echo "$version" | tr -d '\n\r' | xargs)
# Validate version format
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format '$version'. Expected format: x.y.z"
exit 1
fi
echo "Processing version: $version"
# Check if tag already exists locally
if git tag -l | grep -q "^$version$"; then
echo "Error: Git tag '$version' already exists locally"
exit 1
fi
# Check if tag exists on remote
if git ls-remote --tags origin | grep -q "refs/tags/$version$"; then
echo "Error: Git tag '$version' already exists on remote"
exit 1
fi
# Check if GitHub release already exists
if gh release view "$version" >/dev/null 2>&1; then
echo "Error: GitHub release '$version' already exists"
exit 1
fi
echo "Version checks passed - proceeding with release creation"
# Get Release Notes
awk -v version="$version" '
BEGIN { found=0; content="" }
/^# [0-9]+\.[0-9]+\.[0-9]+/ {
if (found) { exit }
if ($2 ~ "^"version"($| \\()") { found=1; next }
}
found { content = content $0 "\n" }
END { printf "%s", content }
' CHANGELOG.md > release_notes.txt
# Set NOTES variable to the content of release_notes.txt
NOTES=$(cat release_notes.txt)
# Create and push tag
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git tag -a "$version" -m "Release version $version"
git push origin "$version"
# Create release
gh release create "$version" \
--title="$version" \
--notes="$NOTES" \
main.js manifest.json styles.css

View file

@ -1,16 +1,34 @@
import { App } from "obsidian";
import { App, setIcon } 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";
import { FullScreenChartModal } from "./fullscreenModal";
import { SQLSealChartsSettings } from "./settings";
import * as JSON5 from "json5";
interface Config {
config: string
}
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) {
constructor(private readonly app: App, private readonly settings: SQLSealChartsSettings) {
}
get viewDefinition(): ViewDefinition {
return {
argument: 'javascriptTemplate',
name: 'chart',
singleLine: false
}
}
get rendererKey() {
@ -18,23 +36,78 @@ export class ChartRenderer implements RendererConfig {
}
validateConfig(config: string): Config {
return { config }
return { config: config.trim() }
}
private createFullscreenButton(container: HTMLElement, chartConfig: Record<string, any>) {
const fullscreenButton = container.createEl('button', {
cls: 'sqlseal-fullscreen-button',
attr: { 'aria-label': 'Open chart in fullscreen' }
})
setIcon(fullscreenButton, 'maximize-2')
fullscreenButton.addEventListener('click', (e) => {
e.stopPropagation();
const modal = new FullScreenChartModal(this.app, chartConfig);
modal.open();
})
}
private prepareGlobalConfigVariables(): Record<string, any> {
const globalVariables: Record<string, any> = {};
this.settings.globalConfigs.forEach(config => {
try {
const parsedConfig = JSON5.parse(config.config);
globalVariables[config.name] = parsedConfig;
} catch (e) {
console.warn(`Failed to parse global configuration '${config.name}' with JSON5:`, e);
// Still make it available as a string if JSON5 parsing fails
globalVariables[config.name] = config.config;
}
});
return globalVariables;
}
render(config: Config, el: HTMLElement) {
let isRendered: boolean = false
let chart: echarts.ECharts | null = null
let resizeObserver: ResizeObserver | 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)
// Add global configurations as variables
const globalVariables = this.prepareGlobalConfigVariables()
const allVariables = { ...variables, ...globalVariables }
let parsedConfig: Object = {}
if (isAdvancedMode) {
try {
parsedConfig = parseCodeAdvanced({ functions, variables, configs: globalVariables }, config.config)
} catch (e) {
console.error(e)
throw e
}
} else {
parsedConfig = parseCode(config.config, functions, allVariables) 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
const { dataset = [] } = configRecord
if (dataset[0]?.id !== 'data') {
configRecord.dataset = [{ id: 'data', source: data }, ...dataset]
}
if (isRendered) {
// Data update
chart?.setOption(configRecord)
@ -43,18 +116,45 @@ export class ChartRenderer implements RendererConfig {
el.empty()
const container = el.createDiv({ cls: 'sqlseal-charts-container' })
const chartDiv = container.createDiv()
const chartHeader = container.createDiv({ cls: 'sqlseal-chart-header' })
const chartDiv = container.createDiv({ cls: 'sqlseal-chart-content' })
this.createFullscreenButton(chartHeader, configRecord)
requestAnimationFrame(() => {
const box = container.getBoundingClientRect()
const width = box.width
const height = box.height
chart = echarts.init(chartDiv, null, { height: height, width: width, })
const containerBox = container.getBoundingClientRect()
const width = containerBox.width
const height = containerBox.height
chart = echarts.init(chartDiv, null, { height: height, width: width })
chart.setOption(configRecord)
isRendered = true
// Set up ResizeObserver to handle container size changes
resizeObserver = new ResizeObserver((entries) => {
if (chart && entries.length > 0) {
const entry = entries[0]
const { width: newWidth } = entry.contentRect
// Maintain 16:9 aspect ratio
const newHeight = (newWidth * 9) / 16
chart.resize({ width: newWidth, height: newHeight })
}
})
resizeObserver.observe(container)
})
},
error: (error: string) => {
return createDiv({ text: error, cls: 'sqlseal-error' })
},
destroy: () => {
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
if (chart) {
chart.dispose()
chart = null
}
}
}
}

69
src/fullscreenModal.ts Normal file
View file

@ -0,0 +1,69 @@
import { App, Modal } from "obsidian";
import * as echarts from "echarts";
export class FullScreenChartModal extends Modal {
private chart: echarts.ECharts | null = null;
private chartConfig: Record<string, any>;
private resizeHandler: (() => void) | null = null;
constructor(app: App, chartConfig: Record<string, any>) {
super(app);
this.chartConfig = chartConfig;
this.scope.register([], "Escape", () => {
this.close();
});
}
onOpen() {
const { contentEl, modalEl } = this;
contentEl.empty();
// Apply fullscreen classes
modalEl.addClass("sqlseal-fullscreen-modal");
const chartContainer = contentEl.createDiv({
cls: "sqlseal-fullscreen-chart-container",
});
// Initialize chart after modal is rendered
requestAnimationFrame(() => {
const containerRect = chartContainer.getBoundingClientRect();
const width = containerRect.width || window.innerWidth * 0.9;
const height = containerRect.height || window.innerHeight * 0.8;
this.chart = echarts.init(chartContainer, null, { width, height });
this.chart.setOption(this.chartConfig);
this.resizeHandler = () => {
if (this.chart) {
this.chart.resize();
}
};
window.addEventListener("resize", this.resizeHandler);
// Show modal after chart is loaded
modalEl.addClass("loaded");
// Ensure proper sizing after initialization
setTimeout(() => {
if (this.chart) {
this.chart.resize();
}
}, 100);
});
}
onClose() {
if (this.resizeHandler) {
window.removeEventListener("resize", this.resizeHandler);
this.resizeHandler = null;
}
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
}
}

View file

@ -1,17 +1,69 @@
import { Plugin } from 'obsidian';
import { ChartRenderer } from './chartRenderer';
import { pluginApi } from '@vanakat/plugin-api';
import type { SQLSealRegisterApi } from '@hypersphere/sqlseal'
import { Plugin } from "obsidian";
import { ChartRenderer } from "./chartRenderer";
import { pluginApi } from "@vanakat/plugin-api";
import type { SQLSealApi, SQLSealRegisterApi } from "@hypersphere/sqlseal";
import { uniqBy } from "lodash";
import { SQLSealChartsSettings, DEFAULT_SETTINGS } from "./settings";
import { SQLSealChartsSettingTab } from "./settingsTab";
const SQLSEAL_API_KEY = "___sqlSeal";
const SQLSEAL_QUEUED_PLUGINS = "___sqlSeal_queue";
const registerApi = (plugin: Plugin, fn: (api: SQLSealApi) => void) => {
if (SQLSEAL_API_KEY in window) {
// SQLSeal already registered
const api = window[SQLSEAL_API_KEY] as SQLSealApi as any;
api.registerForPluginNew({ plugin, run: fn });
return;
} else {
// SQLSeal not registered, adding it to the queue
const data = {
plugin,
run: (api: SQLSealApi) => {
fn(api);
},
};
if (SQLSEAL_QUEUED_PLUGINS in window) {
if (Array.isArray(window[SQLSEAL_QUEUED_PLUGINS])) {
window[SQLSEAL_QUEUED_PLUGINS] = uniqBy(
[...window[SQLSEAL_QUEUED_PLUGINS], data],
(i) => i.plugin
);
} else {
window[SQLSEAL_QUEUED_PLUGINS] = [data];
}
} else {
(window as any)[SQLSEAL_QUEUED_PLUGINS] = [data];
}
}
};
export default class SQLSealCharts extends Plugin {
settings: SQLSealChartsSettings;
async onload() {
this.registerWithSQLSeal();
await this.loadSettings();
// Add settings tab
this.addSettingTab(new SQLSealChartsSettingTab(this.app, this));
registerApi(this, (api) => this.sqlSealRegistered(api));
}
private registerWithSQLSeal() {
const api = pluginApi('sqlseal') as SQLSealRegisterApi
const registar = api.registerForPlugin(this)
registar.registerView('sqlseal-charts', new ChartRenderer(this.app))
sqlSealRegistered(api: SQLSealApi) {
api.registerView("sqlseal-charts", new ChartRenderer(this.app, this.settings));
if ((api as any).apiVersion >= 2) {
api.registerFlag({ key: "isAdvancedMode", name: "ADVANCED MODE" });
}
}
async loadSettings() {
const data = await this.loadData();
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
}
async saveSettings() {
await this.saveData(this.settings);
}
}

37
src/settings.ts Normal file
View file

@ -0,0 +1,37 @@
export interface GlobalChartConfig {
name: string;
config: string; // JSON string
}
export interface SQLSealChartsSettings {
globalConfigs: GlobalChartConfig[];
}
export const DEFAULT_SETTINGS: SQLSealChartsSettings = {
globalConfigs: [
{
name: "defaultTheme",
config: `{
backgroundColor: "#ffffff",
textStyle: {
color: "#333333",
fontFamily: "Arial, sans-serif"
}
}`
},
{
name: "pieChart",
config: `{
type: 'pie',
radius: '70%',
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}`
}
]
};

363
src/settingsTab.ts Normal file
View file

@ -0,0 +1,363 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import SQLSealCharts from "./main";
import { GlobalChartConfig } from "./settings";
import * as JSON5 from "json5";
export class SQLSealChartsSettingTab extends PluginSettingTab {
plugin: SQLSealCharts;
private currentView: "list" | "edit" = "list";
private editingIndex: number = -1;
constructor(app: App, plugin: SQLSealCharts) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "SQLSeal Charts Settings" });
if (this.currentView === "list") {
this.displayConfigList();
} else if (this.currentView === "edit") {
this.displayConfigEditor();
}
}
private displayConfigList(): void {
const { containerEl } = this;
containerEl.createEl("h3", { text: "Global Chart Configurations" });
const descriptionContainer = containerEl.createDiv(
"sqlseal-description-container"
);
descriptionContainer.createEl("p", {
text: "Define reusable JSON5 configurations that can be accessed as variables in your charts.",
cls: "setting-item-description",
});
const exampleContainer = descriptionContainer.createDiv(
"sqlseal-example-container"
);
exampleContainer.createEl("strong", { text: "Example Usage:" });
exampleContainer.createEl("br");
exampleContainer.createEl("code", {
text: '1. Create config named "theme" with: {backgroundColor: "#1a1a1a"}',
cls: "sqlseal-example-code",
});
exampleContainer.createEl("br");
exampleContainer.createEl("code", {
text: '2. Use in charts: {...theme, title: {text: "My Chart"}}',
cls: "sqlseal-example-code",
});
const featuresContainer = descriptionContainer.createDiv(
"sqlseal-features-container"
);
featuresContainer.createEl("br");
featuresContainer.createEl("small", {
text: "✨ Supports JSON5: unquoted keys, single quotes, trailing commas, and comments",
cls: "sqlseal-feature-text",
});
// Add new configuration button
new Setting(containerEl)
.setName("Add New Configuration")
.setDesc("Create a new global configuration")
.addButton((button) => {
button
.setButtonText("Add Configuration")
.setCta()
.onClick(() => {
this.editingIndex = -1; // -1 means new config
this.currentView = "edit";
this.display();
});
});
// Create table
const tableContainer = containerEl.createDiv(
"sqlseal-config-table-container"
);
const table = tableContainer.createEl("table", {
cls: "sqlseal-config-table",
});
// Table header
const thead = table.createEl("thead");
const headerRow = thead.createEl("tr");
headerRow.createEl("th", { text: "Name" });
headerRow.createEl("th", { text: "Preview" });
headerRow.createEl("th", { text: "" });
// Table body
const tbody = table.createEl("tbody");
if (this.plugin.settings.globalConfigs.length === 0) {
const emptyRow = tbody.createEl("tr");
const emptyCell = emptyRow.createEl("td", {
attr: { colspan: "3" },
cls: "sqlseal-empty-state",
text: 'No configurations found. Click "Add Configuration" to create one.',
});
} else {
this.plugin.settings.globalConfigs.forEach((config, index) => {
this.createConfigRow(tbody, config, index);
});
}
}
private createConfigRow(
tbody: HTMLTableSectionElement,
config: GlobalChartConfig,
index: number
): void {
const row = tbody.createEl("tr");
// Name column
const nameCell = row.createEl("td");
nameCell.createEl("span", {
text: config.name,
cls: "sqlseal-config-name",
});
// Preview column
const previewCell = row.createEl("td");
const previewText = this.getConfigPreview(config.config);
previewCell.createEl("code", {
text: previewText,
cls: "sqlseal-config-preview",
});
// Actions column
const actionsCell = row.createEl("td", { cls: "sqlseal-config-actions" });
const editButton = actionsCell.createEl("button", {
text: "Edit",
cls: "mod-cta sqlseal-action-button",
});
editButton.addEventListener("click", () => {
this.editingIndex = index;
this.currentView = "edit";
this.display();
});
const deleteButton = actionsCell.createEl("button", {
text: "Delete",
cls: "mod-warning sqlseal-action-button",
});
deleteButton.addEventListener("click", async () => {
if (
confirm(
`Are you sure you want to delete the configuration "${config.name}"?`
)
) {
this.plugin.settings.globalConfigs.splice(index, 1);
await this.plugin.saveSettings();
this.display();
}
});
}
private displayConfigEditor(): void {
const { containerEl } = this;
const isNewConfig = this.editingIndex === -1;
const originalConfig = isNewConfig
? {
name: `config${this.plugin.settings.globalConfigs.length + 1}`,
config: "{\n \n}",
}
: this.plugin.settings.globalConfigs[this.editingIndex];
// Create a working copy to avoid modifying the original until save
const config = {
name: originalConfig.name,
config: originalConfig.config,
};
// Header with back button
const headerContainer = containerEl.createDiv("sqlseal-editor-header");
const backButton = headerContainer.createEl("button", {
text: "← Back to List",
cls: "sqlseal-back-button",
});
backButton.addEventListener("click", () => {
this.currentView = "list";
this.display();
});
headerContainer.createEl("h3", {
text: isNewConfig
? "Add New Configuration"
: `Edit Configuration: ${config.name}`,
});
// Name input
new Setting(containerEl)
.setName("Configuration Name")
.setDesc("A unique name to identify this configuration")
.addText((text) => {
text.setValue(config.name);
text.onChange(async (value) => {
config.name = value;
// Don't auto-save for existing configs since we're using a working copy
});
// Save on blur
text.inputEl.addEventListener("blur", async () => {
const newValue = text.getValue();
config.name = newValue;
// Don't auto-save for existing configs since we're using a working copy
});
});
// JSON editor
const jsonContainer = containerEl.createDiv(
"sqlseal-json-editor-container"
);
const labelContainer = jsonContainer.createDiv(
"sqlseal-json-label-container"
);
labelContainer.createEl("label", {
text: "JSON5 Configuration:",
cls: "sqlseal-json-label",
});
labelContainer.createEl("small", {
text: "Supports unquoted keys, single quotes, trailing commas, and // comments",
cls: "sqlseal-json-help",
});
const jsonTextarea = jsonContainer.createEl("textarea", {
placeholder: `{
type: 'pie',
radius: '70%',
emphasis: {
itemStyle: {
shadowBlur: 10
}
}
}`,
cls: "sqlseal-json-textarea",
});
// Set the value separately to ensure it loads properly
jsonTextarea.value = config.config || "";
// Also set it after a short delay to ensure DOM is ready
requestAnimationFrame(() => {
jsonTextarea.value = config.config || "";
});
// Auto-save on blur and input with debounce
let saveTimeout: NodeJS.Timeout;
const saveConfig = async () => {
try {
// Use JSON5 for more flexible parsing
JSON5.parse(jsonTextarea.value);
jsonTextarea.classList.remove("error");
jsonTextarea.title = "";
config.config = jsonTextarea.value;
// Don't auto-save since we're using a working copy
} catch (e) {
jsonTextarea.classList.add("error");
jsonTextarea.title = `Invalid JSON5: ${(e as Error).message}`;
}
};
jsonTextarea.addEventListener("input", () => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveConfig, 500); // Debounce for 500ms
});
jsonTextarea.addEventListener("blur", saveConfig);
// Helper buttons
const helperContainer = containerEl.createDiv("sqlseal-helper-buttons");
const formatButton = helperContainer.createEl("button", {
text: "Format JSON",
cls: "mod-cta",
});
formatButton.addEventListener("click", () => {
try {
// Parse with JSON5 (allows unquoted keys, trailing commas, etc.)
const parsed = JSON5.parse(jsonTextarea.value);
// Format as standard JSON for consistency
const formatted = JSON.stringify(parsed, null, 2);
jsonTextarea.value = formatted;
config.config = formatted;
jsonTextarea.classList.remove("error");
jsonTextarea.title = "";
// Show success feedback
formatButton.textContent = "✓ Formatted";
formatButton.classList.add("sqlseal-format-success");
setTimeout(() => {
formatButton.textContent = "Format JSON";
formatButton.classList.remove("sqlseal-format-success");
}, 1500);
} catch (e) {
formatButton.textContent = "✗ Invalid JSON5";
formatButton.classList.add("sqlseal-format-error");
setTimeout(() => {
formatButton.textContent = "Format JSON";
formatButton.classList.remove("sqlseal-format-error");
}, 2000);
}
});
// Action buttons
const buttonContainer = containerEl.createDiv("sqlseal-editor-buttons");
const saveButton = buttonContainer.createEl("button", {
text: isNewConfig ? "Create Configuration" : "Save Changes",
cls: "mod-cta",
});
saveButton.addEventListener("click", async () => {
try {
JSON5.parse(config.config);
if (isNewConfig) {
this.plugin.settings.globalConfigs.push(config);
} else {
// Update the original config with our working copy
this.plugin.settings.globalConfigs[this.editingIndex].name =
config.name;
this.plugin.settings.globalConfigs[this.editingIndex].config =
config.config;
}
await this.plugin.saveSettings();
this.currentView = "list";
this.display();
} catch (e) {
alert(`Invalid JSON5: ${(e as Error).message}`);
}
});
const cancelButton = buttonContainer.createEl("button", {
text: "Cancel",
cls: "mod-warning",
});
cancelButton.addEventListener("click", () => {
this.currentView = "list";
this.display();
});
}
private getConfigPreview(configString: string): string {
try {
const parsed = JSON5.parse(configString);
const preview = JSON.stringify(parsed);
return preview.length > 50 ? preview.substring(0, 47) + "..." : preview;
} catch (e) {
return "Invalid JSON5";
}
}
}

View file

@ -0,0 +1,41 @@
import { uniqBy } from "lodash";
import { identifier } from "safe-identifier";
type Params = {
functions: Record<string, any>;
variables: Record<string, any>;
configs: Record<string, any>;
};
const sanatiseColumns = (columns: Record<string, unknown>) => {
return Object.fromEntries(
Object.entries(columns).map(([key, value]) => [identifier(key), value])
);
};
export const parseCodeAdvanced = (
{ functions, variables, configs }: Params,
code: string
) => {
const { keys, values } = uniqBy(
[
...Object.entries(functions),
...Object.entries({
...sanatiseColumns(variables.columns),
data: variables.data,
columns: variables.columns,
...configs,
}),
],
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,11 +1,302 @@
.sqlseal-charts-container {
display: grid;
justify-content: center;
display: flex;
flex-direction: column;
width: 100%;
aspect-ratio: 16 / 9;
position: relative;
}
.sqlseal-charts-container > div {
.sqlseal-charts-container:hover .sqlseal-chart-header {
opacity: 1;
}
.sqlseal-chart-header {
display: flex;
justify-content: flex-end;
position: absolute;
bottom: 8px;
right: 8px;
z-index: 10;
opacity: 0;
transition: opacity 0.2s ease;
}
.sqlseal-fullscreen-button {
background: var(--background-modifier-hover);
border: none;
border-radius: var(--radius-s);
padding: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.2s ease;
}
.sqlseal-fullscreen-button:hover {
background: var(--background-modifier-border-hover);
opacity: 1;
}
.sqlseal-chart-content {
width: 100%;
height: 100%;
flex: 1;
}
.sqlseal-fullscreen-modal {
z-index: 9999;
width: 100vw !important;
height: 100vh !important;
max-width: none !important;
max-height: none !important;
left: 0 !important;
top: 0 !important;
transform: none !important;
margin: 0 !important;
opacity: 0;
transition: opacity 0.2s ease;
}
.sqlseal-fullscreen-modal.loaded {
opacity: 1;
}
.sqlseal-fullscreen-modal .modal-content {
width: 95vw !important;
height: 90vh !important;
max-width: none !important;
max-height: none !important;
margin: 2.5vh 2.5vw !important;
position: absolute !important;
left: 2.5vw !important;
top: 5vh !important;
}
.sqlseal-fullscreen-chart-container {
width: 100%;
height: calc(90vh - 40px);
min-height: 400px;
}
/* Settings Panel Styles */
.sqlseal-config-table-container {
margin: 20px 0;
}
.sqlseal-config-table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
overflow: hidden;
}
.sqlseal-config-table th,
.sqlseal-config-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--background-modifier-border);
}
.sqlseal-config-table th {
background: var(--background-modifier-hover);
font-weight: 600;
color: var(--text-normal);
}
.sqlseal-config-table tr:last-child td {
border-bottom: none;
}
.sqlseal-config-table tr:hover {
background: var(--background-modifier-hover-alt);
}
.sqlseal-config-name {
font-weight: 500;
color: var(--text-normal);
}
.sqlseal-config-preview {
font-family: var(--font-monospace);
font-size: 12px;
background: var(--background-modifier-border);
padding: 4px 8px;
border-radius: var(--radius-s);
color: var(--text-muted);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sqlseal-config-actions {
white-space: nowrap;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.sqlseal-action-button {
padding: 4px 12px;
border: none;
border-radius: var(--radius-s);
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s ease;
}
.sqlseal-empty-state {
text-align: center;
color: var(--text-muted);
font-style: italic;
padding: 40px 20px;
}
/* Editor Styles */
.sqlseal-editor-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
border-bottom: 1px solid var(--background-modifier-border);
padding-bottom: 16px;
}
.sqlseal-back-button {
padding: 6px 12px;
background: var(--interactive-normal);
border: none;
border-radius: var(--radius-s);
cursor: pointer;
color: var(--text-normal);
font-size: 14px;
transition: background-color 0.2s ease;
}
.sqlseal-back-button:hover {
background: var(--interactive-hover);
}
.sqlseal-json-editor-container {
margin: 20px 0;
}
.sqlseal-json-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-normal);
}
.sqlseal-json-textarea {
width: 100%;
min-height: 200px;
padding: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-primary);
color: var(--text-normal);
font-family: var(--font-monospace);
font-size: 13px;
line-height: 1.4;
resize: vertical;
transition: border-color 0.2s ease;
}
.sqlseal-json-textarea:focus {
border-color: var(--interactive-accent);
outline: none;
}
.sqlseal-json-textarea.error {
border-color: var(--text-error);
}
.sqlseal-editor-buttons {
display: flex;
gap: 12px;
justify-content: flex-start;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--background-modifier-border);
}
.sqlseal-editor-buttons button {
padding: 8px 16px;
border: none;
border-radius: var(--radius-s);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
/* Documentation Styles */
.sqlseal-description-container {
margin-bottom: 20px;
padding: 16px;
background: var(--background-secondary);
border-radius: var(--radius-s);
border-left: 4px solid var(--interactive-accent);
}
.sqlseal-example-container {
margin-top: 12px;
}
.sqlseal-example-code {
font-family: var(--font-monospace);
font-size: 12px;
background: var(--background-modifier-border);
padding: 2px 6px;
border-radius: var(--radius-xs);
color: var(--text-muted);
}
.sqlseal-feature-text {
color: var(--text-accent);
font-style: italic;
}
.sqlseal-json-label-container {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 8px;
}
.sqlseal-json-help {
color: var(--text-muted);
font-size: 11px;
font-style: italic;
}
/* Helper Button Styles */
.sqlseal-helper-buttons {
display: flex;
gap: 8px;
margin: 12px 0;
justify-content: flex-start;
}
.sqlseal-helper-buttons button {
padding: 6px 12px;
border: none;
border-radius: var(--radius-s);
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
/* Success and Error States for Format Button */
.sqlseal-format-success {
background-color: #4caf50 !important;
}
.sqlseal-format-error {
background-color: #ff6b6b !important;
}

View file

@ -1,4 +1,10 @@
{
"0.1.0": "0.15.0",
"0.1.1": "0.15.0"
"0.1.1": "0.15.0",
"0.2.0": "0.15.0",
"0.2.1": "0.15.0",
"0.3.0": "0.15.0",
"0.4.0": "0.15.0",
"0.4.1": "0.15.0",
"0.4.2": "0.15.0"
}