Compare commits
37 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c27d97e220 | ||
|
|
1d12289e74 | ||
|
|
ff048f5eba | ||
|
|
9775903ae1 | ||
|
|
46ab0b037c | ||
|
|
c70eeec9c5 | ||
|
|
68749173b0 | ||
|
|
7c854f22b6 | ||
|
|
c9237858f7 | ||
|
|
4416495b79 | ||
|
|
ae6ef8869e | ||
|
|
e8f6dd522d | ||
|
|
7a7db26abb | ||
|
|
3b0da2bfcd | ||
|
|
f359f0e587 | ||
|
|
610cf9620b | ||
|
|
299924673f | ||
|
|
22dbb66fcd | ||
|
|
e45ef49ed6 | ||
|
|
1e562831da | ||
|
|
39be40d358 | ||
|
|
7de759b3d2 | ||
|
|
447548cece | ||
|
|
76a634cfc8 | ||
|
|
c3e8e6c6a9 | ||
|
|
ccf7b02733 | ||
|
|
542ed504aa | ||
|
|
814e3e5df8 | ||
|
|
ef7ceed551 | ||
|
|
c41cbce218 | ||
|
|
c220fa20ed | ||
|
|
b0bc7c05e3 | ||
|
|
2918c79774 | ||
|
|
0ecff2e158 | ||
|
|
37f746b72c | ||
|
|
732577fae5 | ||
|
|
39543065fa |
8
.changeset/README.md
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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 }}
|
||||
88
.github/workflows/tag-and-publish.yml
vendored
|
|
@ -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
|
||||
41
CHANGELOG.md
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ CHART {
|
|||
SELECT * FROM finances
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
### Line Chart
|
||||
```sqlseal
|
||||
|
|
@ -56,7 +56,7 @@ GROUP BY created_date
|
|||
ORDER BY created_date
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Syntax
|
||||
|
|
|
|||
55
docs/.vitepress/config.mts
Normal 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>'
|
||||
}
|
||||
}
|
||||
})
|
||||
108
docs/.vitepress/theme/components/Stats.vue
Normal 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>
|
||||
18
docs/.vitepress/theme/index.ts
Normal 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
|
||||
135
docs/.vitepress/theme/style.css
Normal 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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
../CHANGELOG.md
|
||||
3
docs/chart-type/bar-chart.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Bar Chart
|
||||
|
||||
Under construction.
|
||||
3
docs/chart-type/line-chart.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Line Chart
|
||||
|
||||
Under construction.
|
||||
BIN
docs/clustering-example.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
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!
|
||||
18
docs/faq/comparison-with-dataview.md
Normal 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.
|
||||
BIN
docs/images/community-plugins.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
docs/images/quick-start-chart.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
27
docs/index.md
Normal 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
|
||||
BIN
docs/langing-page/charts.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/langing-page/discord_logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/langing-page/example.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
docs/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
docs/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
26
docs/public/logo.svg
Normal file
|
After Width: | Height: | Size: 43 KiB |
5
docs/public/transactions.csv
Normal 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
|
||||
|
69
docs/quick-start.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||

|
||||
|
||||
|
||||
## 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
|
After Width: | Height: | Size: 198 KiB |
30
docs/syntax.md
Normal 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.
|
||||
|
|
@ -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!",
|
||||
|
|
|
|||
38
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
3042
pnpm-lock.yaml
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
72
scripts/tag-and-publish.sh
Executable 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
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/main.ts
|
|
@ -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
|
|
@ -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
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/utils/advancedParser.ts
Normal 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);
|
||||
};
|
||||
297
styles.css
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||