feat: initial version

This commit is contained in:
Kacper Kula 2025-01-31 13:44:21 +00:00
commit 4f65a78103
24 changed files with 4865 additions and 0 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
ko_fi: hypersphere

88
.github/workflows/tag-and-publish.yml vendored Normal file
View file

@ -0,0 +1,88 @@
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

49
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: Run Tests on Pull Request
on:
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm run test
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Run typecheck
run: pnpm run typecheck

26
.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
docs/.vitepress/dist
docs/.vitepress/cache
coverage/
dist/

2
CHANGELOG.md Normal file
View file

@ -0,0 +1,2 @@
# 0.1.0 (2025-01-31)
Initial release!

85
README.md Normal file
View file

@ -0,0 +1,85 @@
# SQLSeal Charts
SQLSeal Charts is an extension for SQLSeal Obsidian Plugin allowing to visualise your data using charts!
Powered by [ECharts](https://echarts.apache.org/en/index.html). Check their documentation for more examples of usage.
## Features
- Support for basic charts: line, bar, pie charts
- Support for compex charts like scatterplot, heatmap, etc.
## Getting started
Install the plugin by downloading latest release and unpacking it in `.obsidian/plugins/sqlseal-charts`. Make sure you have SQLSeal plugin already installed.
## Example query
### Pie Chart
Query below takes the first table in your current markdown tile and displays it as a piechart.
| Category | Amount |
| ------------- | ------ |
| Rent | 1500 |
| Groceries | 200 |
| Entertainment | 150 |
| Repairs | 200 |
```sqlseal
TABLE finances = table(0)
CHART {
series: [{
type: 'pie'
}]
}
SELECT * FROM finances
```
![Pie Chart Example](./docs/pie_chart.png)
### Line Chart
```sqlseal
CHART {
xAxis: {
type: 'category'
},
yAxis: {},
series: [{
type: 'bar',
}]
}
SELECT
strftime("%Y-%m-%d", created_at) as created_date,
COUNT(*) as count
FROM files
GROUP BY created_date
ORDER BY created_date
```
![Line Chart Example](./docs/line_chart.png)
## 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/).
For more advanced transformations, SQLSeal Charts exposes the following data and functions to be used to transform data further.
### Data
| 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
The following functions are exposed in your query to allow you to further process data.
| 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 |
| Repairs | 200 |
| `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 |

BIN
docs/line_chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/pie_chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

37
esbuild.config.mjs Normal file
View file

@ -0,0 +1,37 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
...builtins,
],
format: "cjs",
target: "es2016",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

9
jest.config.js Normal file
View file

@ -0,0 +1,9 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "sqlseal-charts",
"name": "SQLSeal Charts",
"version": "0.1.0",
"minAppVersion": "0.15.0",
"description": "Charting extension for SQLSeal plugin. Generate pie charts, bar charts, line charts and more using data stored in your vault!",
"author": "hypersphere",
"authorUrl": "https://hypersphere.blog/",
"fundingUrl": "ko-fi.com/hypersphere"
}

53
package.json Normal file
View file

@ -0,0 +1,53 @@
{
"name": "sqlseal-charts",
"version": "0.1.0",
"description": "Chart extension for SQLSeal Obsidian plugin",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"typecheck": "tsc -noEmit -skipLibCheck",
"test": "jest",
"test:watch": "jest --watch"
},
"keywords": [
"obsidian",
"plugin",
"sqlseal",
"charts",
"visualisation"
],
"repository": {
"type": "github",
"url": "https://github.com/h-sphere/sql-seal-charts"
},
"author": "Kacper Kula",
"license": "MIT",
"devDependencies": {
"@types/acorn": "^6.0.4",
"@types/estree": "^1.0.6",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.10",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.24.2",
"jest": "^29.7.0",
"obsidian": "^1.7.2",
"ts-jest": "^29.2.5",
"tslib": "^2.8.1",
"typescript": "^5.7.3"
},
"dependencies": {
"@types/leaflet": "^1.9.16",
"@types/lodash": "^4.17.14",
"@vanakat/plugin-api": "^0.2.1",
"acorn": "^8.14.0",
"echarts": "^5.6.0",
"echarts-gl": "^2.0.9",
"json5": "^2.2.3",
"lodash": "^4.17.21",
"zod": "^3.24.1"
}
}

3938
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

52
scripts/data_generator.js Normal file
View file

@ -0,0 +1,52 @@
const fs = require('fs');
const path = require('path');
const OUTPUT_FILE = path.join(__dirname, '../../../../obsidianPlayground/My\ Vault/test_data.csv');
const TOTAL_RECORDS = 50;
const UPDATE_INTERVAL = 1 * 1000; // 10 seconds
// Generate dates starting from today
function generateDates() {
const dates = [];
const today = new Date();
for(let i = 0; i < TOTAL_RECORDS; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
dates.push(date.toISOString().split('T')[0]); // YYYY-MM-DD format
}
return dates;
}
// Generate random values between 1 and 100
function generateValues() {
return Array(TOTAL_RECORDS).fill(0)
.map(() => Math.floor(Math.random() * 100) + 1);
}
// Create CSV content
function createCSV(dates, values, values2) {
const headers = ['date', 'value1', 'value2'];
const rows = dates.map((date, index) => `${date},${values[index]},${values2[index]}`);
return [headers.join(','), ...rows].join('\n');
}
// Main function to update data
function updateData() {
const dates = generateDates();
const values = generateValues();
const values2 = generateValues()
const csvContent = createCSV(dates, values, values2);
fs.writeFileSync(OUTPUT_FILE, csvContent, 'utf-8');
console.log(`Data updated at ${new Date().toLocaleTimeString()}`);
}
// Initial update
updateData();
// Schedule updates
setInterval(updateData, UPDATE_INTERVAL);
console.log(`Data generator started. Writing to ${OUTPUT_FILE}`);

57
src/chartRenderer.ts Normal file
View file

@ -0,0 +1,57 @@
import { App } from "obsidian";
import { parseCode } from "./utils/configParser";
import { prepareDataVariables } from "./utils/prepareDataVariables";
import * as echarts from 'echarts';
import type { RendererConfig } from "@hypersphere/sqlseal";
interface Config {
config: string
}
export class ChartRenderer implements RendererConfig {
constructor(private readonly app: App) {
}
get rendererKey() {
return 'chart'
}
validateConfig(config: string): Config {
return { config }
}
render(config: Config, el: HTMLElement) {
let isRendered: boolean = false
let chart: echarts.ECharts | null = null
return {
render: ({ columns, data }: { columns: string[], data: Record<string, unknown>[]}) => {
const { functions, variables } = prepareDataVariables({ columns, data })
const parsedConfig = parseCode(config.config, functions, variables)
const dataset = [{ id: 'data', source: data }, ...(parsedConfig.dataset ?? [])]
parsedConfig.dataset = dataset
if (isRendered) {
// Data update
chart?.setOption(parsedConfig)
return
}
el.empty()
const container = el.createDiv({ cls: 'sqlseal-charts-container' })
const chartDiv = container.createDiv()
requestAnimationFrame(() => {
const box = container.getBoundingClientRect()
const width = box.width
const height = box.height
chart = echarts.init(chartDiv, null, { height: height, width: width, })
chart.setOption(parsedConfig)
isRendered = true
})
},
error: (error: string) => {
return createDiv({ text: error, cls: 'sqlseal-error' })
}
}
}
}

17
src/main.ts Normal file
View file

@ -0,0 +1,17 @@
import { Plugin } from 'obsidian';
import { ChartRenderer } from './chartRenderer';
import { pluginApi } from '@vanakat/plugin-api';
import type { SQLSealRegisterApi } from '@hypersphere/sqlseal'
export default class SQLSealCharts extends Plugin {
async onload() {
this.registerWithSQLSeal();
}
private registerWithSQLSeal() {
const api = pluginApi('sqlseal') as SQLSealRegisterApi
const registar = api.registerForPlugin(this)
registar.registerView('sqlseal-charts', new ChartRenderer(this.app))
}
}

View file

@ -0,0 +1,94 @@
import { parseCode } from "./configParser";
describe('config parser', () => {
it('should properly parse basic data', () => {
// Example usage:
const dot = (config: any) => ({ type: 'dot', ...config });
// Test the parser
const testCode = `{
a: 'test',
marks: [
dot({ x: 'x', y: 'something' })
]
}`;
const res = parseCode(testCode, { dot })
expect(res).toEqual({
a: 'test',
marks: [{
type: 'dot',
x: 'x',
y: 'something'
}]
})
})
it('should properly parse config with data provided', () => {
const variables = {
data: [1, 2, 3, 4],
config: { value: 42 }
};
const functions = {
dot: (config: any) => ({ type: 'dot', ...config })
};
// This will work:
const code1 = `{
a: data[0],
b: data[1],
marks: [
dot({ x: data[2], y: data[3] })
]
}`;
const res = parseCode(code1, functions, variables)
expect(res).toEqual({
a: 1,
b: 2,
marks: [{
type: 'dot',
x: 3,
y: 4
}]
})
})
it('should properly parse config with arrow functions and template literals', () => {
const variables = {
data: [1, 2, 3, 4],
config: { value: 42 }
};
const functions = {
dot: (config: any) => ({ type: 'dot', ...config }),
column: (name: string) => ([1,2,3])
};
// This will work:
const code1 = `{
a: data[0],
b: data[1],
fn: (x) => \`\${x.a.b[0]} DATA\`,
data: column('data'),
marks: [
dot({ x: data[2], y: data[3] })
]
}`;
const res = parseCode(code1, functions, variables)
expect(res).toEqual({
a: 1,
b: 2,
fn: expect.any(Function),
data: [1,2,3],
marks: [{
type: 'dot',
x: 3,
y: 4
}]
})
})
})

205
src/utils/configParser.ts Normal file
View file

@ -0,0 +1,205 @@
import * as acorn from 'acorn';
import { Node, ObjectExpression, Property, CallExpression, Identifier, Literal,
ArrayExpression, MemberExpression, ArrowFunctionExpression, BlockStatement,
TemplateLiteral } from 'estree';
type AllowedFunctions = Record<string, (...args: any[]) => any>;
type AllowedVariables = Record<string, any>;
// Extended to include function scope
interface EvaluationContext {
allowedFunctions: AllowedFunctions;
allowedVariables: AllowedVariables;
scope: Record<string, any>;
}
export function parseCode(
code: string,
allowedFunctions: AllowedFunctions = {},
allowedVariables: AllowedVariables = {}
) {
const wrappedCode = `(${code})`;
const ast = acorn.parse(wrappedCode, {
ecmaVersion: 2020,
sourceType: 'module'
}) as any;
const context: EvaluationContext = {
allowedFunctions,
allowedVariables,
scope: {}
};
function evaluateNode(node: Node, context: EvaluationContext): any {
if (!node) {
throw new Error('Null node encountered');
}
switch (node.type) {
case 'ObjectExpression':
return evaluateObject(node as ObjectExpression, context);
case 'ArrayExpression':
return evaluateArray(node as ArrayExpression, context);
case 'CallExpression':
return evaluateCall(node as CallExpression, context);
case 'Identifier':
return evaluateIdentifier(node as Identifier, context);
case 'Literal':
return (node as Literal).value;
case 'MemberExpression':
return evaluateMemberExpression(node as MemberExpression, context);
case 'ArrowFunctionExpression':
return evaluateArrowFunction(node as ArrowFunctionExpression, context);
case 'BlockStatement':
return evaluateBlockStatement(node as BlockStatement, context);
case 'TemplateLiteral':
return evaluateTemplateLiteral(node as any, context);
case 'TemplateElement':
return (node as any).value.cooked;
default:
throw new Error(`Unsupported node type: ${node.type}`);
}
}
function evaluateArrowFunction(node: ArrowFunctionExpression, context: EvaluationContext): Function {
return (...args: any[]) => {
// Create a new scope for the function execution
const functionContext: EvaluationContext = {
...context,
scope: { ...context.scope }
};
// Bind parameters to arguments
node.params.forEach((param, index) => {
if (param.type === 'Identifier') {
functionContext.scope[param.name] = args[index];
} else {
throw new Error('Only simple parameter names are supported');
}
});
// Handle both expression and block body
if (node.body.type === 'BlockStatement') {
return evaluateBlockStatement(node.body, functionContext);
} else {
return evaluateNode(node.body, functionContext);
}
};
}
function evaluateBlockStatement(node: BlockStatement, context: EvaluationContext): any {
let result: any;
for (const statement of node.body) {
result = evaluateNode(statement, context);
}
return result; // Returns the value of the last statement
}
function evaluateTemplateLiteral(node: TemplateLiteral, context: EvaluationContext): string {
let result = '';
const expressions = node.expressions;
const quasis = node.quasis;
for (let i = 0; i < quasis.length; i++) {
// Add the template string part
result += evaluateNode(quasis[i], context);
// Add the expression value if there is one
if (i < expressions.length) {
const value = evaluateNode(expressions[i], context);
result += value != null ? String(value) : '';
}
}
return result;
}
function evaluateIdentifier(node: Identifier, context: EvaluationContext): any {
const varName = node.name;
// Check scope first, then allowed variables
if (context.scope.hasOwnProperty(varName)) {
return context.scope[varName];
}
if (context.allowedVariables.hasOwnProperty(varName)) {
return context.allowedVariables[varName];
}
throw new Error(`Variable "${varName}" is not allowed`);
}
function evaluateMemberExpression(node: MemberExpression, context: EvaluationContext): any {
// Evaluate the object part of the member expression
const object = node.object.type === 'MemberExpression'
? evaluateMemberExpression(node.object, context)
: node.object.type === 'Identifier'
? evaluateIdentifier(node.object, context)
: evaluateNode(node.object, context);
if (object == null) {
throw new Error('Cannot access properties of null or undefined');
}
// Handle computed (bracket notation) access
if (node.computed) {
const propertyValue = evaluateNode(node.property, context);
if (typeof propertyValue !== 'number' && typeof propertyValue !== 'string') {
throw new Error('Property access must use number or string');
}
return object[propertyValue];
}
// Handle dot notation access
if (node.property.type === 'Identifier') {
return object[node.property.name];
}
throw new Error('Invalid property access');
}
function evaluateObject(node: ObjectExpression, context: EvaluationContext): Record<string, any> {
const result: Record<string, any> = {};
for (const prop of node.properties) {
const property = prop as Property;
const key = property.key.type === 'Identifier' ?
property.key.name :
(property.key as Literal).value;
result[key as string] = evaluateNode(property.value, context);
}
return result;
}
function evaluateArray(node: ArrayExpression, context: EvaluationContext): any[] {
return node.elements.map(element =>
element ? evaluateNode(element, context) : null
);
}
function evaluateCall(node: CallExpression, context: EvaluationContext): any {
let func: Function;
if (node.callee.type === 'Identifier') {
const callee = node.callee.name;
if (!context.allowedFunctions[callee]) {
throw new Error(`Function "${callee}" is not allowed`);
}
func = context.allowedFunctions[callee];
} else if (node.callee.type === 'ArrowFunctionExpression') {
func = evaluateArrowFunction(node.callee as ArrowFunctionExpression, context);
} else {
throw new Error('Unsupported function call type');
}
const args = node.arguments.map(arg => evaluateNode(arg, context));
return func(...args);
}
const expressionStatement = ast.body[0];
if (!expressionStatement || expressionStatement.type !== 'ExpressionStatement') {
throw new Error('Invalid input structure');
}
return evaluateNode(expressionStatement.expression, context);
}

View file

@ -0,0 +1,32 @@
import { prepareDataVariables } from "./prepareDataVariables"
describe('prepare data variables', () => {
it('should properly prepare variables for the dataset', () => {
const { functions, variables } = prepareDataVariables({
data: [{a: 5, b: 9}, { a: 6, b: 10}],
columns: ['a', 'b']
})
expect(variables.data).toHaveLength(2)
expect(variables.columns).toHaveLength(2)
expect(variables['a']).toEqual([5, 6])
expect(variables['b']).toEqual([9, 10])
expect(functions.max([1,2,3])).toEqual(3)
expect(functions.min([1,2,3])).toEqual(1)
expect(functions.mean([1, 2, 3])).toEqual(2)
expect(functions.column('a')).toEqual([5,6])
expect(functions.array(functions.column('a'), functions.column('b'))).toEqual([[5, 9], [6, 10]])
// Assemble and Assemble Objects
const { assemble, assembleObjects } = functions
const result = [
{ id: 1, value: 50 },
{ id: 2, value: 51 },
{ id: 3, value: 52 }
]
expect(assembleObjects({ key: 'id', values: [1,2,3]}, { key: 'value', values: [50, 51, 52 ]})).toEqual(result)
expect(assemble({ id: [1,2,3], value: [50, 51, 52]})).toEqual(result)
})
})

View file

@ -0,0 +1,58 @@
import { min, max, uniq, uniqBy, mean } from 'lodash'
interface Config {
data: Record<string, unknown>[];
columns: string[];
}
export function prepareDataVariables({ data, columns }: Config) {
const column = (f: string) => {
return data.map((d: any) => d[f])
}
function array<T>(...arrays: T[][]): T[][] {
const minLength = Math.min(...arrays.map(arr => arr.length));
return Array.from({ length: minLength }, (_, i) =>
arrays.map(arr => arr[i])
);
}
interface ValueDefinition {
key: string;
values: any[];
}
function assembleObjects(...definitions: ValueDefinition[]): Record<string, any>[] {
const minLength = Math.min(...definitions.map(def => def.values.length));
return Array.from({ length: minLength }, (_, index) => {
return definitions.reduce((obj, def) => {
obj[def.key] = def.values[index];
return obj;
}, {} as Record<string, any>);
});
}
function assemble(def: Record<string, any[]>): Record<string, any>[] {
const definitions = Object.values(def)
const minLength = Math.min(...definitions.map(def => def.length));
return Array.from({ length: minLength }, (_, index) => {
return Object.entries(def).reduce((obj, [key, value]) => {
obj[key] = value[index]
return obj;
}, {} as Record<string, any>);
});
}
const columnsData: Record<string, unknown> = columns.reduce((acc, col) => ({
...acc,
[col]: column(col)
}), {})
return {
functions: { column, array, assembleObjects, min, max, uniq, uniqBy, assemble, mean },
variables: { ...columnsData, data, columns } as Record<string, unknown[]>
}
}

11
styles.css Normal file
View file

@ -0,0 +1,11 @@
.sqlseal-charts-container {
display: grid;
justify-content: center;
width: 100%;
aspect-ratio: 16 / 9;
}
.sqlseal-charts-container > div {
width: 100%;
height: 100%;
}

24
tsconfig.json Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.1.0": "0.15.0"
}