yamchart 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ZLFZUYVM.js → chunk-F6QAWPYU.js} +13 -2
- package/dist/chunk-F6QAWPYU.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/templates/default/CLAUDE.md +24 -2
- package/dist/templates/default/docs/yamchart-reference.md +313 -37
- package/dist/{update-ATYDSPT4.js → update-HCR6MYJX.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-ZLFZUYVM.js.map +0 -1
- /package/dist/{update-ATYDSPT4.js.map → update-HCR6MYJX.js.map} +0 -0
|
@@ -33,7 +33,7 @@ async function checkForUpdate(currentVersion, { skipCache = false } = {}) {
|
|
|
33
33
|
if (!cached) {
|
|
34
34
|
writeCache(latest);
|
|
35
35
|
}
|
|
36
|
-
if (latest !== currentVersion && latest
|
|
36
|
+
if (latest !== currentVersion && isNewerVersion(latest, currentVersion)) {
|
|
37
37
|
return { current: currentVersion, latest };
|
|
38
38
|
}
|
|
39
39
|
return null;
|
|
@@ -64,6 +64,17 @@ async function fetchReleaseNotes(version) {
|
|
|
64
64
|
}
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
|
+
function isNewerVersion(a, b) {
|
|
68
|
+
const pa = a.split(".").map(Number);
|
|
69
|
+
const pb = b.split(".").map(Number);
|
|
70
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
71
|
+
const na = pa[i] ?? 0;
|
|
72
|
+
const nb = pb[i] ?? 0;
|
|
73
|
+
if (na > nb) return true;
|
|
74
|
+
if (na < nb) return false;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
67
78
|
async function fetchLatestVersion() {
|
|
68
79
|
const controller = new AbortController();
|
|
69
80
|
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
@@ -86,4 +97,4 @@ export {
|
|
|
86
97
|
checkForUpdate,
|
|
87
98
|
fetchReleaseNotes
|
|
88
99
|
};
|
|
89
|
-
//# sourceMappingURL=chunk-
|
|
100
|
+
//# sourceMappingURL=chunk-F6QAWPYU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/update-check.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nconst REGISTRY_URL = 'https://registry.npmjs.org/yamchart';\nconst GITHUB_RELEASES_URL = 'https://api.github.com/repos/simon-spenc/yamchart/releases/tags';\nconst CACHE_DIR = join(homedir(), '.yamchart');\nconst CACHE_FILE = join(CACHE_DIR, 'update-check.json');\nconst CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface UpdateCheckResult {\n current: string;\n latest: string;\n}\n\ninterface CachedCheck {\n latest: string;\n checkedAt: number;\n}\n\nfunction readCache(): CachedCheck | null {\n try {\n const raw = readFileSync(CACHE_FILE, 'utf-8');\n const data = JSON.parse(raw) as CachedCheck;\n if (Date.now() - data.checkedAt < CACHE_TTL_MS) {\n return data;\n }\n } catch {\n // Cache miss or corrupt — ignore\n }\n return null;\n}\n\nfunction writeCache(latest: string): void {\n try {\n mkdirSync(CACHE_DIR, { recursive: true });\n writeFileSync(CACHE_FILE, JSON.stringify({ latest, checkedAt: Date.now() }));\n } catch {\n // Non-critical — ignore write errors\n }\n}\n\nexport async function checkForUpdate(currentVersion: string, { skipCache = false } = {}): Promise<UpdateCheckResult | null> {\n try {\n // Check cache first (unless explicitly bypassed)\n const cached = skipCache ? null : readCache();\n const latest = cached?.latest ?? await fetchLatestVersion();\n if (!latest) return null;\n\n if (!cached) {\n writeCache(latest);\n }\n\n if (latest !== currentVersion && isNewerVersion(latest, currentVersion)) {\n return { current: currentVersion, latest };\n }\n\n return null;\n } catch {\n return null; // Silent fail — never block CLI on update check\n }\n}\n\nexport async function fetchReleaseNotes(version: string): Promise<string | null> {\n // Try with v prefix first (v0.4.0), then without (0.4.0)\n for (const tag of [`v${version}`, version]) {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n\n const response = await fetch(`${GITHUB_RELEASES_URL}/${tag}`, {\n signal: controller.signal,\n headers: {\n Accept: 'application/vnd.github.v3+json',\n 'User-Agent': 'yamchart-cli',\n },\n });\n clearTimeout(timeout);\n\n if (!response.ok) continue;\n\n const data = await response.json() as { body?: string | null };\n const body = data.body?.trim();\n return body || null;\n } catch {\n continue;\n }\n }\n return null;\n}\n\n/** Compare semver strings numerically (a > b). */\nfunction isNewerVersion(a: string, b: string): boolean {\n const pa = a.split('.').map(Number);\n const pb = b.split('.').map(Number);\n for (let i = 0; i < Math.max(pa.length, pb.length); i++) {\n const na = pa[i] ?? 0;\n const nb = pb[i] ?? 0;\n if (na > nb) return true;\n if (na < nb) return false;\n }\n return false;\n}\n\nasync function fetchLatestVersion(): Promise<string | null> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000); // 3s timeout\n\n try {\n const response = await fetch(REGISTRY_URL, {\n signal: controller.signal,\n headers: { Accept: 'application/vnd.npm.install-v1+json' },\n });\n clearTimeout(timeout);\n\n if (!response.ok) return null;\n\n const data = await response.json() as { 'dist-tags'?: { latest?: string } };\n return data['dist-tags']?.latest ?? null;\n } catch {\n clearTimeout(timeout);\n return null;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
checkForUpdate
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-F6QAWPYU.js";
|
|
5
5
|
import {
|
|
6
6
|
findProjectRoot,
|
|
7
7
|
loadEnvFile,
|
|
@@ -218,7 +218,7 @@ program.command("test").description("Run model tests (@returns schema checks and
|
|
|
218
218
|
}
|
|
219
219
|
});
|
|
220
220
|
program.command("update").description("Check for yamchart updates").action(async () => {
|
|
221
|
-
const { runUpdate } = await import("./update-
|
|
221
|
+
const { runUpdate } = await import("./update-HCR6MYJX.js");
|
|
222
222
|
await runUpdate(pkg.version);
|
|
223
223
|
});
|
|
224
224
|
program.command("reset-password").description("Reset a user password (requires auth to be enabled)").requiredOption("-e, --email <email>", "Email address of the user").action(async (options) => {
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
# {{name}}
|
|
2
2
|
|
|
3
|
+
This is a [Yamchart](https://github.com/simon-spenc/yamchart) project — Git-native BI dashboards defined as YAML configs and SQL models.
|
|
4
|
+
|
|
3
5
|
## Project Notes
|
|
4
6
|
|
|
5
|
-
Add your project-specific notes, conventions, and context here.
|
|
7
|
+
Add your project-specific notes, conventions, and context here (e.g. database schema, naming conventions, team preferences).
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yamchart dev # Start dev server with hot reload
|
|
13
|
+
yamchart validate # Validate all config files
|
|
14
|
+
yamchart test # Run model assertions
|
|
15
|
+
yamchart tables # List database tables
|
|
16
|
+
yamchart describe <table> # Show table columns
|
|
17
|
+
yamchart search <keyword> # Find tables/columns
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## File Structure
|
|
21
|
+
|
|
22
|
+
- `yamchart.yaml` — project config, default connection, theme, auth
|
|
23
|
+
- `connections/*.yaml` — database connections (DuckDB, Postgres, MySQL, SQLite, Snowflake)
|
|
24
|
+
- `models/*.sql` — SQL with Jinja templating (`{{ param }}`, `{% if %}`, `{{ ref('table') }}`)
|
|
25
|
+
- `charts/*.yaml` — chart definitions (line, bar, area, pie, donut, scatter, kpi, combo, heatmap, funnel, waterfall, gauge, table)
|
|
26
|
+
- `dashboards/*.yaml` — 12-column grid layouts with chart/text widgets, optional tabs
|
|
27
|
+
- `schedules/*.yaml` — cron-based Slack reports and threshold alerts
|
|
6
28
|
|
|
7
29
|
## Yamchart Reference
|
|
8
30
|
|
|
9
|
-
See [docs/yamchart-reference.md](docs/yamchart-reference.md) for the full
|
|
31
|
+
See [docs/yamchart-reference.md](docs/yamchart-reference.md) for the full configuration reference including all chart types, parameters, dashboard tabs, filters, connections, schedules, authentication, and formatting options.
|
|
@@ -6,11 +6,12 @@ Yamchart is a Git-native BI framework. Dashboards are defined as YAML configs an
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
{{name}}/
|
|
9
|
-
├── yamchart.yaml # Project config
|
|
9
|
+
├── yamchart.yaml # Project config, default connection, theme, auth
|
|
10
10
|
├── connections/ # Database connection configs (.yaml)
|
|
11
11
|
├── models/ # SQL models with Jinja templating (.sql)
|
|
12
12
|
├── charts/ # Chart definitions (.yaml)
|
|
13
|
-
|
|
13
|
+
├── dashboards/ # Dashboard layouts (.yaml)
|
|
14
|
+
└── schedules/ # Scheduled reports and alerts (.yaml)
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
## Commands
|
|
@@ -21,11 +22,16 @@ yamchart validate # Validate all config files
|
|
|
21
22
|
yamchart validate --dry-run # Also test queries with EXPLAIN
|
|
22
23
|
yamchart test # Run model @returns and @tests assertions
|
|
23
24
|
yamchart test <model> # Test specific model
|
|
25
|
+
yamchart update # Check for and install updates
|
|
24
26
|
yamchart tables # List tables in connected database
|
|
27
|
+
yamchart tables --schema public # Filter to a specific schema
|
|
25
28
|
yamchart describe <table> # Show columns and types (accepts dbt model names)
|
|
26
29
|
yamchart sample <table> # Show sample rows (default: 5, accepts dbt model names)
|
|
30
|
+
yamchart sample <table> -l 10 # Custom row limit
|
|
27
31
|
yamchart search <keyword> # Find tables and columns matching keyword
|
|
28
|
-
yamchart query "SELECT ..." # Execute ad-hoc SQL
|
|
32
|
+
yamchart query "SELECT ..." # Execute ad-hoc SQL (default limit: 100)
|
|
33
|
+
yamchart query "SELECT ..." --limit 500 # Custom row limit
|
|
34
|
+
yamchart reset-password --email <e> # Reset a user's password (requires auth enabled)
|
|
29
35
|
```
|
|
30
36
|
|
|
31
37
|
## SQL Models (`models/*.sql`)
|
|
@@ -35,9 +41,13 @@ Models are SQL files with metadata comments and Jinja templating.
|
|
|
35
41
|
```sql
|
|
36
42
|
-- @name: model_name
|
|
37
43
|
-- @description: What this model computes
|
|
44
|
+
-- @param start_date: date = 2025-01-01
|
|
45
|
+
-- @param category: string = All
|
|
38
46
|
-- @returns:
|
|
39
|
-
-- -
|
|
40
|
-
-- -
|
|
47
|
+
-- - period: date
|
|
48
|
+
-- - revenue: number
|
|
49
|
+
-- @tests:
|
|
50
|
+
-- - SELECT * FROM ({{this}}) t WHERE revenue < 0
|
|
41
51
|
|
|
42
52
|
SELECT
|
|
43
53
|
date_trunc('{{ granularity }}', order_date) AS period,
|
|
@@ -47,16 +57,21 @@ WHERE order_date BETWEEN '{{ start_date }}' AND '{{ end_date }}'
|
|
|
47
57
|
{% if category and category != 'All' %}
|
|
48
58
|
AND category = '{{ category }}'
|
|
49
59
|
{% endif %}
|
|
60
|
+
{% if user.department %}
|
|
61
|
+
AND department = '{{ user.department }}'
|
|
62
|
+
{% endif %}
|
|
50
63
|
GROUP BY 1
|
|
51
64
|
ORDER BY 1
|
|
52
65
|
```
|
|
53
66
|
|
|
54
67
|
**Metadata tags:** `@name` (required), `@description`, `@param name: type = default`, `@returns` (column: type list), `@tests` (SQL assertions where zero rows = pass)
|
|
55
68
|
|
|
56
|
-
**Templating:** `{{ variable }}` for substitution, `{% if %}` / `{% endif %}` for conditionals, `{{ ref('table') }}` for table references, `{{ user.attribute }}` for row-level security.
|
|
69
|
+
**Templating:** `{{ variable }}` for substitution, `{% if %}` / `{% endif %}` for conditionals, `{{ ref('table') }}` for table references, `{{ user.email|role|<attribute> }}` for row-level security.
|
|
57
70
|
|
|
58
71
|
**Date parameters:** When a chart has `type: date_range`, the model receives `start_date` and `end_date` automatically.
|
|
59
72
|
|
|
73
|
+
**Tests:** `{{this}}` in `@tests` expands to the compiled SQL. Zero rows returned = pass; any rows = failure.
|
|
74
|
+
|
|
60
75
|
## Charts (`charts/*.yaml`)
|
|
61
76
|
|
|
62
77
|
### Common Structure
|
|
@@ -67,10 +82,16 @@ title: My Chart # Display title (required)
|
|
|
67
82
|
description: Description # Optional
|
|
68
83
|
source:
|
|
69
84
|
model: model_name # SQL model to use
|
|
85
|
+
connection: prod # Optional — override default connection
|
|
70
86
|
parameters: [] # Interactive filters (optional)
|
|
71
87
|
chart:
|
|
72
88
|
type: line # Chart type
|
|
73
89
|
# ... type-specific config
|
|
90
|
+
drillDown: # Optional — right-click to navigate
|
|
91
|
+
chart: detail-chart
|
|
92
|
+
field: category
|
|
93
|
+
refresh: # Optional — caching/scheduling
|
|
94
|
+
cache_ttl: "30m"
|
|
74
95
|
```
|
|
75
96
|
|
|
76
97
|
### Chart Types
|
|
@@ -83,6 +104,8 @@ chart:
|
|
|
83
104
|
y: { field: revenue, format: "$,.0f" }
|
|
84
105
|
series:
|
|
85
106
|
field: category # Multi-series by grouping field (optional)
|
|
107
|
+
stacking: stacked # Optional: stacked or percent (bar/area only)
|
|
108
|
+
gradient: true # Optional: auto gradient on fill
|
|
86
109
|
```
|
|
87
110
|
|
|
88
111
|
**Pie / Donut** — part-to-whole:
|
|
@@ -91,6 +114,10 @@ chart:
|
|
|
91
114
|
type: pie # or donut
|
|
92
115
|
x: { field: segment, type: nominal }
|
|
93
116
|
y: { field: amount, format: "$,.0f" }
|
|
117
|
+
centerValue: # Donut only — center display
|
|
118
|
+
field: total # 'total' sums all values, or use a field name
|
|
119
|
+
label: "Total"
|
|
120
|
+
format: "$,.0f"
|
|
94
121
|
```
|
|
95
122
|
|
|
96
123
|
**Scatter** — correlations with optional size/group/regression:
|
|
@@ -99,28 +126,47 @@ chart:
|
|
|
99
126
|
type: scatter
|
|
100
127
|
x: { field: revenue, type: quantitative }
|
|
101
128
|
y: { field: orders, type: quantitative }
|
|
102
|
-
size: { field: avg_value, min: 8, max: 40 }
|
|
103
|
-
group: { field: region }
|
|
104
|
-
regression:
|
|
129
|
+
size: { field: avg_value, min: 8, max: 40, label: "Avg Value" } # Bubble encoding
|
|
130
|
+
group: { field: region } # Color by category
|
|
131
|
+
regression: # Trend line
|
|
132
|
+
type: linear
|
|
133
|
+
show_equation: true
|
|
134
|
+
show_r_squared: true
|
|
105
135
|
```
|
|
106
136
|
|
|
107
|
-
**KPI** — single metric with comparison:
|
|
137
|
+
**KPI** — single metric with optional comparison:
|
|
108
138
|
```yaml
|
|
109
139
|
chart:
|
|
110
140
|
type: kpi
|
|
111
|
-
value: { field:
|
|
141
|
+
value: { field: revenue }
|
|
112
142
|
format: { type: currency, currency: USD, decimals: 0 }
|
|
113
|
-
|
|
143
|
+
unit: "sessions" # Optional suffix label
|
|
144
|
+
|
|
145
|
+
# Option A: Auto comparison (server computes previous period)
|
|
146
|
+
comparison:
|
|
147
|
+
enabled: true
|
|
148
|
+
period: previous # Server shifts date range back automatically
|
|
149
|
+
type: percent_change # or absolute
|
|
150
|
+
|
|
151
|
+
# Option B: Manual comparison (use a column from query results)
|
|
152
|
+
comparison:
|
|
153
|
+
enabled: true
|
|
154
|
+
field: previous_value # Column name with the comparison value
|
|
155
|
+
label: "vs last month"
|
|
156
|
+
type: percent_change
|
|
114
157
|
```
|
|
115
158
|
|
|
159
|
+
> **Auto comparison:** `period: previous` runs the model a second time with the shifted date range. It infers the shift from the active date preset (e.g. `last_30_days` → previous 30 days). Displays period labels, change badge, and "vs" previous value automatically.
|
|
160
|
+
|
|
116
161
|
**Heatmap** — two-dimensional intensity:
|
|
117
162
|
```yaml
|
|
118
163
|
chart:
|
|
119
164
|
type: heatmap
|
|
120
165
|
x: { field: hour, type: ordinal }
|
|
121
166
|
y: { field: day, type: ordinal }
|
|
122
|
-
value: { field: count }
|
|
167
|
+
value: { field: count, label: "Count" }
|
|
123
168
|
color_range: { min: '#EBF5FB', max: '#1A5276' }
|
|
169
|
+
show_values: true
|
|
124
170
|
```
|
|
125
171
|
|
|
126
172
|
**Funnel** — conversion stages:
|
|
@@ -129,6 +175,7 @@ chart:
|
|
|
129
175
|
type: funnel
|
|
130
176
|
stage: { field: stage_name }
|
|
131
177
|
value: { field: user_count }
|
|
178
|
+
show_conversion: true
|
|
132
179
|
```
|
|
133
180
|
|
|
134
181
|
**Waterfall** — incremental deltas:
|
|
@@ -137,7 +184,11 @@ chart:
|
|
|
137
184
|
type: waterfall
|
|
138
185
|
category: { field: label }
|
|
139
186
|
value: { field: amount }
|
|
140
|
-
total_field: is_total
|
|
187
|
+
total_field: is_total # Column name marking total rows
|
|
188
|
+
colors:
|
|
189
|
+
increase: "#10B981"
|
|
190
|
+
decrease: "#EF4444"
|
|
191
|
+
total: "#3B82F6"
|
|
141
192
|
```
|
|
142
193
|
|
|
143
194
|
**Gauge** — single metric dial:
|
|
@@ -151,6 +202,7 @@ chart:
|
|
|
151
202
|
- { value: 60, color: '#EF4444' }
|
|
152
203
|
- { value: 85, color: '#F59E0B' }
|
|
153
204
|
- { value: 100, color: '#22C55E' }
|
|
205
|
+
format: ".1f"
|
|
154
206
|
```
|
|
155
207
|
|
|
156
208
|
**Table** — sortable data table (renders all query columns by default):
|
|
@@ -178,7 +230,7 @@ chart:
|
|
|
178
230
|
|
|
179
231
|
### Multi-Series
|
|
180
232
|
|
|
181
|
-
**Long format** — group by a field:
|
|
233
|
+
**Long format** — group by a field in the data:
|
|
182
234
|
```yaml
|
|
183
235
|
series:
|
|
184
236
|
field: region
|
|
@@ -191,19 +243,27 @@ series:
|
|
|
191
243
|
```yaml
|
|
192
244
|
series:
|
|
193
245
|
columns:
|
|
194
|
-
-
|
|
195
|
-
|
|
246
|
+
- field: revenue
|
|
247
|
+
name: Revenue
|
|
248
|
+
color: "#3B82F6"
|
|
249
|
+
- field: cost
|
|
250
|
+
name: Cost
|
|
251
|
+
color: "#EF4444"
|
|
252
|
+
style: dashed # solid, dashed, or dotted
|
|
253
|
+
opacity: 0.8 # 0–1
|
|
254
|
+
gradient: true # or { from: "#hex", to: "#hex" }
|
|
196
255
|
```
|
|
197
256
|
|
|
198
257
|
### Stacking
|
|
199
258
|
|
|
200
259
|
```yaml
|
|
201
260
|
chart:
|
|
202
|
-
type: bar
|
|
203
|
-
stacking: stacked #
|
|
261
|
+
type: bar # or area
|
|
262
|
+
stacking: stacked # normal stacking
|
|
263
|
+
stacking: percent # normalizes to 100%, caps y-axis, shows % labels
|
|
204
264
|
```
|
|
205
265
|
|
|
206
|
-
### Parameters
|
|
266
|
+
### Parameters (Interactive Filters)
|
|
207
267
|
|
|
208
268
|
```yaml
|
|
209
269
|
parameters:
|
|
@@ -213,23 +273,47 @@ parameters:
|
|
|
213
273
|
|
|
214
274
|
- name: region
|
|
215
275
|
type: select
|
|
276
|
+
label: Region
|
|
216
277
|
options: [North, South, East, West]
|
|
217
278
|
default: North
|
|
218
279
|
|
|
280
|
+
- name: tags
|
|
281
|
+
type: multi_select
|
|
282
|
+
options:
|
|
283
|
+
- value: a
|
|
284
|
+
label: Category A
|
|
285
|
+
- value: b
|
|
286
|
+
label: Category B
|
|
287
|
+
|
|
219
288
|
- name: category
|
|
220
289
|
type: dynamic_select
|
|
221
290
|
source: { model: category_options, value_field: id, label_field: name }
|
|
291
|
+
|
|
292
|
+
- name: search_term
|
|
293
|
+
type: text
|
|
294
|
+
|
|
295
|
+
- name: min_revenue
|
|
296
|
+
type: number
|
|
297
|
+
default: 0
|
|
298
|
+
|
|
299
|
+
- name: is_active
|
|
300
|
+
type: boolean
|
|
301
|
+
default: true
|
|
222
302
|
```
|
|
223
303
|
|
|
224
|
-
**
|
|
304
|
+
**Parameter types:** `date_range`, `select`, `multi_select`, `dynamic_select`, `text`, `number`, `boolean`
|
|
305
|
+
|
|
306
|
+
**Date presets:** `last_7_days`, `last_30_days`, `last_90_days`, `last_12_months`, `year_to_date`, `month_to_date`, `quarter_to_date`, `previous_month`, `previous_quarter`, `previous_year`
|
|
225
307
|
|
|
226
308
|
### Drill-Down
|
|
227
309
|
|
|
310
|
+
Right-click any chart element to navigate to a related detail chart with filtered data.
|
|
311
|
+
|
|
228
312
|
```yaml
|
|
229
313
|
drillDown:
|
|
230
314
|
chart: detail-chart # Target chart name
|
|
231
|
-
field: category # Field to pass as filter
|
|
232
|
-
columns: # Optional table columns on landing
|
|
315
|
+
field: category # Field to pass as filter (defaults to x-axis field)
|
|
316
|
+
columns: # Optional table columns on landing page
|
|
233
317
|
- { field: name, label: Name }
|
|
234
318
|
- { field: revenue, label: Revenue, format: "$,.0f" }
|
|
235
319
|
```
|
|
@@ -239,27 +323,53 @@ drillDown:
|
|
|
239
323
|
```yaml
|
|
240
324
|
chart:
|
|
241
325
|
gradient: true # Auto gradient
|
|
326
|
+
gradient: { from: "#3B82F6", to: "#1E40AF" } # Custom gradient
|
|
327
|
+
|
|
242
328
|
color: "#3B82F6" # Single color override
|
|
243
|
-
|
|
329
|
+
|
|
330
|
+
color: # Conditional coloring (bar charts)
|
|
244
331
|
conditions:
|
|
245
332
|
- { when: "> 0", color: "#10B981" }
|
|
246
333
|
- { when: "< 0", color: "#EF4444" }
|
|
247
334
|
default: "#6B7280"
|
|
248
335
|
```
|
|
249
336
|
|
|
337
|
+
### Axis Schema
|
|
338
|
+
|
|
339
|
+
```yaml
|
|
340
|
+
x:
|
|
341
|
+
field: date
|
|
342
|
+
type: temporal # temporal, quantitative, ordinal, or nominal
|
|
343
|
+
format: "$,.0f" # Optional d3-format / strftime string
|
|
344
|
+
label: "Date" # Optional axis label
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Caching
|
|
348
|
+
|
|
349
|
+
```yaml
|
|
350
|
+
refresh:
|
|
351
|
+
cache_ttl: "30m" # Cache duration: 30s, 5m, 1h, 1d
|
|
352
|
+
schedule: "0 * * * *" # Optional cron for pre-warming
|
|
353
|
+
timezone: America/New_York # Optional
|
|
354
|
+
```
|
|
355
|
+
|
|
250
356
|
## Dashboards (`dashboards/*.yaml`)
|
|
251
357
|
|
|
252
358
|
12-column grid layout with chart and text widgets.
|
|
253
359
|
|
|
360
|
+
### Basic Layout
|
|
361
|
+
|
|
254
362
|
```yaml
|
|
255
363
|
name: overview
|
|
256
364
|
title: Executive Overview
|
|
365
|
+
description: High-level business metrics
|
|
257
366
|
|
|
258
|
-
filters:
|
|
367
|
+
filters: # Optional — dashboard-level filters (shared across all tabs)
|
|
259
368
|
- { name: date_range, type: date_range, default: last_30_days }
|
|
369
|
+
- { name: region, type: select, options: [All, North, South], default: All }
|
|
260
370
|
|
|
261
371
|
layout:
|
|
262
|
-
gap: 16
|
|
372
|
+
gap: 16 # Pixels between widgets (default: 16)
|
|
263
373
|
rows:
|
|
264
374
|
- height: 180
|
|
265
375
|
widgets:
|
|
@@ -277,7 +387,62 @@ layout:
|
|
|
277
387
|
vs last month: {{revenue-kpi@previous_month}}
|
|
278
388
|
```
|
|
279
389
|
|
|
280
|
-
|
|
390
|
+
### Dashboard Tabs
|
|
391
|
+
|
|
392
|
+
Split a dashboard into tabbed sub-pages. Use `tabs` instead of `layout` (mutually exclusive).
|
|
393
|
+
|
|
394
|
+
```yaml
|
|
395
|
+
name: analytics
|
|
396
|
+
title: Analytics Dashboard
|
|
397
|
+
|
|
398
|
+
filters: # Dashboard-level — shown on all tabs
|
|
399
|
+
- { name: date_range, type: date_range, default: last_30_days }
|
|
400
|
+
|
|
401
|
+
tabs:
|
|
402
|
+
- name: overview
|
|
403
|
+
label: Overview
|
|
404
|
+
layout:
|
|
405
|
+
rows:
|
|
406
|
+
- height: 400
|
|
407
|
+
widgets:
|
|
408
|
+
- { type: chart, ref: revenue-trend, cols: 12 }
|
|
409
|
+
|
|
410
|
+
- name: details
|
|
411
|
+
label: Details
|
|
412
|
+
filters: # Tab-level — only shown on this tab
|
|
413
|
+
- { name: category, type: select, options: [All, A, B, C] }
|
|
414
|
+
layout:
|
|
415
|
+
rows:
|
|
416
|
+
- height: 400
|
|
417
|
+
widgets:
|
|
418
|
+
- { type: chart, ref: detail-table, cols: 12 }
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Tab navigation is reflected in the URL as `?tab=details`. Dashboard-level filters persist across tabs; tab-level filters are scoped to their tab.
|
|
422
|
+
|
|
423
|
+
### KPI References in Text Widgets
|
|
424
|
+
|
|
425
|
+
Embed live KPI values in markdown text widgets:
|
|
426
|
+
|
|
427
|
+
| Syntax | Description |
|
|
428
|
+
|--------|-------------|
|
|
429
|
+
| `{{chartName}}` | Primary value from a KPI chart |
|
|
430
|
+
| `{{chart.field}}` | Specific field from chart results |
|
|
431
|
+
| `{{chart@preset}}` | Value with a date preset applied |
|
|
432
|
+
| `{{chart@2025-01-01..2025-12-31}}` | Value with a fixed date range |
|
|
433
|
+
|
|
434
|
+
### Cross-Filtering
|
|
435
|
+
|
|
436
|
+
Click any chart element on a dashboard to filter all other charts by the clicked dimension. Cross-filters are dashboard-scoped and auto-clear on navigation.
|
|
437
|
+
|
|
438
|
+
### Edit Mode
|
|
439
|
+
|
|
440
|
+
The dashboard UI includes an edit mode for drag-and-drop layout editing:
|
|
441
|
+
- Resize and reposition widgets on the grid
|
|
442
|
+
- Add new chart or text widgets via the toolbar
|
|
443
|
+
- Double-click text widgets to edit markdown inline (KPI autocomplete via `{`)
|
|
444
|
+
- Undo/redo support
|
|
445
|
+
- Changes are saved back to the YAML file
|
|
281
446
|
|
|
282
447
|
## Connections (`connections/*.yaml`)
|
|
283
448
|
|
|
@@ -296,18 +461,137 @@ database: analytics
|
|
|
296
461
|
user: ${DB_USER}
|
|
297
462
|
password: ${DB_PASSWORD}
|
|
298
463
|
ssl: true
|
|
464
|
+
schema: public
|
|
465
|
+
|
|
466
|
+
# MySQL
|
|
467
|
+
name: mysql-db
|
|
468
|
+
type: mysql
|
|
469
|
+
config: { host: ${DB_HOST}, port: 3306, database: mydb }
|
|
470
|
+
auth: { type: env, user_var: MYSQL_USER, password_var: MYSQL_PASSWORD }
|
|
471
|
+
|
|
472
|
+
# Snowflake
|
|
473
|
+
name: snow
|
|
474
|
+
type: snowflake
|
|
475
|
+
config: { account: xy12345, warehouse: COMPUTE_WH, database: ANALYTICS, schema: PUBLIC }
|
|
476
|
+
auth: { type: env, user_var: SF_USER, password_var: SF_PASSWORD }
|
|
477
|
+
# SSO: auth: { type: externalbrowser, user_var: SF_USER }
|
|
478
|
+
|
|
479
|
+
# SQLite
|
|
480
|
+
name: local-sqlite
|
|
481
|
+
type: sqlite
|
|
482
|
+
path: ./data/app.db
|
|
299
483
|
```
|
|
300
484
|
|
|
301
485
|
**Supported:** DuckDB, PostgreSQL, MySQL, SQLite, Snowflake
|
|
302
486
|
|
|
303
487
|
Use `${ENV_VAR}` for credentials. Never commit secrets.
|
|
304
488
|
|
|
489
|
+
## Schedules (`schedules/*.yaml`)
|
|
490
|
+
|
|
491
|
+
### Scheduled Report
|
|
492
|
+
|
|
493
|
+
Deliver chart data summaries to Slack on a cron schedule.
|
|
494
|
+
|
|
495
|
+
```yaml
|
|
496
|
+
name: weekly_sales_report
|
|
497
|
+
type: report
|
|
498
|
+
schedule: "0 9 * * MON" # Cron (5-6 fields)
|
|
499
|
+
timezone: America/New_York # Optional
|
|
500
|
+
channel:
|
|
501
|
+
slack:
|
|
502
|
+
webhook_url: ${SLACK_WEBHOOK_URL}
|
|
503
|
+
channel: "#sales" # Optional display override
|
|
504
|
+
charts:
|
|
505
|
+
- revenue_by_region
|
|
506
|
+
- top_products
|
|
507
|
+
params:
|
|
508
|
+
date_range: last_7_days
|
|
509
|
+
message: "Weekly sales report" # Optional header text
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Threshold Alert
|
|
513
|
+
|
|
514
|
+
Monitor a chart value against a threshold with cooldown.
|
|
515
|
+
|
|
516
|
+
```yaml
|
|
517
|
+
name: revenue_drop_alert
|
|
518
|
+
type: alert
|
|
519
|
+
schedule: "*/15 * * * *" # Check every 15 minutes
|
|
520
|
+
channel:
|
|
521
|
+
slack:
|
|
522
|
+
webhook_url: ${SLACK_WEBHOOK_URL}
|
|
523
|
+
chart: daily_revenue # Single chart
|
|
524
|
+
params:
|
|
525
|
+
date_range: last_7_days
|
|
526
|
+
condition:
|
|
527
|
+
field: revenue
|
|
528
|
+
operator: lt # lt, gt, lte, gte, eq
|
|
529
|
+
value: 1000
|
|
530
|
+
cooldown: "1h" # Min time between alerts (30s, 5m, 1h, 1d)
|
|
531
|
+
message: "Revenue dropped below $1,000! Current: ${{value}}"
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Alert behavior:** Evaluates the first row of query results. Empty results skip evaluation. `{{value}}` in the message is replaced with the actual value. Cooldown is in-memory and resets on server restart.
|
|
535
|
+
|
|
536
|
+
## Authentication (`yamchart.yaml`)
|
|
537
|
+
|
|
538
|
+
Optional built-in authentication for self-hosted deployments.
|
|
539
|
+
|
|
540
|
+
```yaml
|
|
541
|
+
auth:
|
|
542
|
+
enabled: true
|
|
543
|
+
db_path: ~/.yamchart/auth.db # Optional (default shown)
|
|
544
|
+
session_ttl: "30d" # Optional (default: 30 days)
|
|
545
|
+
providers: # Optional SSO
|
|
546
|
+
google:
|
|
547
|
+
client_id: ${GOOGLE_CLIENT_ID}
|
|
548
|
+
client_secret: ${GOOGLE_CLIENT_SECRET}
|
|
549
|
+
microsoft:
|
|
550
|
+
client_id: ${MS_CLIENT_ID}
|
|
551
|
+
client_secret: ${MS_CLIENT_SECRET}
|
|
552
|
+
tenant: ${MS_TENANT_ID}
|
|
553
|
+
oidc:
|
|
554
|
+
client_id: ${OIDC_CLIENT_ID}
|
|
555
|
+
client_secret: ${OIDC_CLIENT_SECRET}
|
|
556
|
+
issuer: https://auth.example.com
|
|
557
|
+
display_name: "Company SSO"
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- First visit prompts a setup wizard to create an admin account
|
|
561
|
+
- **Roles:** admin (manage users + content), editor (content only), viewer (read-only)
|
|
562
|
+
- **User attributes:** Admins can set key-value pairs per user (e.g. `department: Sales`) for row-level security
|
|
563
|
+
- **RLS in SQL:** `{{ user.email }}`, `{{ user.role }}`, `{{ user.department }}` etc.
|
|
564
|
+
- **CLI recovery:** `yamchart reset-password --email user@example.com`
|
|
565
|
+
|
|
566
|
+
## Project Config (`yamchart.yaml`)
|
|
567
|
+
|
|
568
|
+
```yaml
|
|
569
|
+
version: "1"
|
|
570
|
+
name: my-project
|
|
571
|
+
description: Analytics dashboards
|
|
572
|
+
|
|
573
|
+
defaults:
|
|
574
|
+
connection: local # Default connection name
|
|
575
|
+
cache_ttl: "30m" # Default cache TTL
|
|
576
|
+
base_url: https://bi.example.com # For SSO callbacks and alert links
|
|
577
|
+
week_start: monday # sunday–saturday
|
|
578
|
+
|
|
579
|
+
theme:
|
|
580
|
+
palette: ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"]
|
|
581
|
+
gradient: false
|
|
582
|
+
opacity: 1.0
|
|
583
|
+
|
|
584
|
+
auth:
|
|
585
|
+
enabled: false # Enable built-in authentication
|
|
586
|
+
```
|
|
587
|
+
|
|
305
588
|
## Axis Types
|
|
306
589
|
|
|
307
590
|
| Type | Use | Example |
|
|
308
591
|
|------|-----|---------|
|
|
309
592
|
| `temporal` | Dates/times | `2024-01-15` |
|
|
310
|
-
| `ordinal` |
|
|
593
|
+
| `ordinal` | Ordered categories | `Mon, Tue, Wed` |
|
|
594
|
+
| `nominal` | Unordered categories | `Electronics` |
|
|
311
595
|
| `quantitative` | Numbers | `250.5` |
|
|
312
596
|
|
|
313
597
|
## Number Formats (d3-format)
|
|
@@ -318,12 +602,4 @@ Use `${ENV_VAR}` for credentials. Never commit secrets.
|
|
|
318
602
|
| `,.0f` | 1,234 | Numbers |
|
|
319
603
|
| `.2%` | 12.34% | Percentages |
|
|
320
604
|
| `.2s` | 1.2M | SI prefix |
|
|
321
|
-
|
|
322
|
-
## Theme (`yamchart.yaml`)
|
|
323
|
-
|
|
324
|
-
```yaml
|
|
325
|
-
theme:
|
|
326
|
-
palette: ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"]
|
|
327
|
-
gradient: false
|
|
328
|
-
opacity: 1.0
|
|
329
|
-
```
|
|
605
|
+
| `.2f` | 1.23 | Fixed decimals |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
checkForUpdate,
|
|
3
3
|
fetchReleaseNotes
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-F6QAWPYU.js";
|
|
5
5
|
import {
|
|
6
6
|
detail,
|
|
7
7
|
error,
|
|
@@ -85,4 +85,4 @@ export {
|
|
|
85
85
|
refreshDocs,
|
|
86
86
|
runUpdate
|
|
87
87
|
};
|
|
88
|
-
//# sourceMappingURL=update-
|
|
88
|
+
//# sourceMappingURL=update-HCR6MYJX.js.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/update-check.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nconst REGISTRY_URL = 'https://registry.npmjs.org/yamchart';\nconst GITHUB_RELEASES_URL = 'https://api.github.com/repos/simon-spenc/yamchart/releases/tags';\nconst CACHE_DIR = join(homedir(), '.yamchart');\nconst CACHE_FILE = join(CACHE_DIR, 'update-check.json');\nconst CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface UpdateCheckResult {\n current: string;\n latest: string;\n}\n\ninterface CachedCheck {\n latest: string;\n checkedAt: number;\n}\n\nfunction readCache(): CachedCheck | null {\n try {\n const raw = readFileSync(CACHE_FILE, 'utf-8');\n const data = JSON.parse(raw) as CachedCheck;\n if (Date.now() - data.checkedAt < CACHE_TTL_MS) {\n return data;\n }\n } catch {\n // Cache miss or corrupt — ignore\n }\n return null;\n}\n\nfunction writeCache(latest: string): void {\n try {\n mkdirSync(CACHE_DIR, { recursive: true });\n writeFileSync(CACHE_FILE, JSON.stringify({ latest, checkedAt: Date.now() }));\n } catch {\n // Non-critical — ignore write errors\n }\n}\n\nexport async function checkForUpdate(currentVersion: string, { skipCache = false } = {}): Promise<UpdateCheckResult | null> {\n try {\n // Check cache first (unless explicitly bypassed)\n const cached = skipCache ? null : readCache();\n const latest = cached?.latest ?? await fetchLatestVersion();\n if (!latest) return null;\n\n if (!cached) {\n writeCache(latest);\n }\n\n // Compare versions (simple string comparison works for semver)\n if (latest !== currentVersion && latest > currentVersion) {\n return { current: currentVersion, latest };\n }\n\n return null;\n } catch {\n return null; // Silent fail — never block CLI on update check\n }\n}\n\nexport async function fetchReleaseNotes(version: string): Promise<string | null> {\n // Try with v prefix first (v0.4.0), then without (0.4.0)\n for (const tag of [`v${version}`, version]) {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n\n const response = await fetch(`${GITHUB_RELEASES_URL}/${tag}`, {\n signal: controller.signal,\n headers: {\n Accept: 'application/vnd.github.v3+json',\n 'User-Agent': 'yamchart-cli',\n },\n });\n clearTimeout(timeout);\n\n if (!response.ok) continue;\n\n const data = await response.json() as { body?: string | null };\n const body = data.body?.trim();\n return body || null;\n } catch {\n continue;\n }\n }\n return null;\n}\n\nasync function fetchLatestVersion(): Promise<string | null> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000); // 3s timeout\n\n try {\n const response = await fetch(REGISTRY_URL, {\n signal: controller.signal,\n headers: { Accept: 'application/vnd.npm.install-v1+json' },\n });\n clearTimeout(timeout);\n\n if (!response.ok) return null;\n\n const data = await response.json() as { 'dist-tags'?: { latest?: string } };\n return data['dist-tags']?.latest ?? null;\n } catch {\n clearTimeout(timeout);\n return null;\n }\n}\n"],"mappings":";AAAA,SAAS,cAAc,eAAe,iBAAiB;AACvD,SAAS,YAAY;AACrB,SAAS,eAAe;AAExB,IAAM,eAAe;AACrB,IAAM,sBAAsB;AAC5B,IAAM,YAAY,KAAK,QAAQ,GAAG,WAAW;AAC7C,IAAM,aAAa,KAAK,WAAW,mBAAmB;AACtD,IAAM,eAAe,KAAK,KAAK,KAAK;AAYpC,SAAS,YAAgC;AACvC,MAAI;AACF,UAAM,MAAM,aAAa,YAAY,OAAO;AAC5C,UAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,QAAI,KAAK,IAAI,IAAI,KAAK,YAAY,cAAc;AAC9C,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,SAAS,WAAW,QAAsB;AACxC,MAAI;AACF,cAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AACxC,kBAAc,YAAY,KAAK,UAAU,EAAE,QAAQ,WAAW,KAAK,IAAI,EAAE,CAAC,CAAC;AAAA,EAC7E,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,eAAe,gBAAwB,EAAE,YAAY,MAAM,IAAI,CAAC,GAAsC;AAC1H,MAAI;AAEF,UAAM,SAAS,YAAY,OAAO,UAAU;AAC5C,UAAM,SAAS,QAAQ,UAAU,MAAM,mBAAmB;AAC1D,QAAI,CAAC,OAAQ,QAAO;AAEpB,QAAI,CAAC,QAAQ;AACX,iBAAW,MAAM;AAAA,IACnB;AAGA,QAAI,WAAW,kBAAkB,SAAS,gBAAgB;AACxD,aAAO,EAAE,SAAS,gBAAgB,OAAO;AAAA,IAC3C;AAEA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,kBAAkB,SAAyC;AAE/E,aAAW,OAAO,CAAC,IAAI,OAAO,IAAI,OAAO,GAAG;AAC1C,QAAI;AACF,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AAEzD,YAAM,WAAW,MAAM,MAAM,GAAG,mBAAmB,IAAI,GAAG,IAAI;AAAA,QAC5D,QAAQ,WAAW;AAAA,QACnB,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,QAChB;AAAA,MACF,CAAC;AACD,mBAAa,OAAO;AAEpB,UAAI,CAAC,SAAS,GAAI;AAElB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,aAAO,QAAQ;AAAA,IACjB,QAAQ;AACN;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,qBAA6C;AAC1D,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AAEzD,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,cAAc;AAAA,MACzC,QAAQ,WAAW;AAAA,MACnB,SAAS,EAAE,QAAQ,sCAAsC;AAAA,IAC3D,CAAC;AACD,iBAAa,OAAO;AAEpB,QAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,WAAO,KAAK,WAAW,GAAG,UAAU;AAAA,EACtC,QAAQ;AACN,iBAAa,OAAO;AACpB,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
File without changes
|