mayacharts 0.1.0
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/LICENSE +21 -0
- package/README.md +619 -0
- package/dist/core/a11y.d.ts +10 -0
- package/dist/core/format.d.ts +2 -0
- package/dist/core/layout.d.ts +22 -0
- package/dist/core/marks/bar.d.ts +2 -0
- package/dist/core/marks/dumbbell.d.ts +3 -0
- package/dist/core/marks/heatmap.d.ts +3 -0
- package/dist/core/marks/kpi.d.ts +2 -0
- package/dist/core/marks/line.d.ts +4 -0
- package/dist/core/marks/scatter.d.ts +2 -0
- package/dist/core/registry.d.ts +13 -0
- package/dist/core/render.d.ts +8 -0
- package/dist/core/scale.d.ts +3 -0
- package/dist/core/shape.d.ts +14 -0
- package/dist/core/strings.d.ts +37 -0
- package/dist/core/svg.d.ts +13 -0
- package/dist/core/ticks.d.ts +2 -0
- package/dist/core/types.d.ts +424 -0
- package/dist/core/validate.d.ts +24 -0
- package/dist/element/animate.d.ts +13 -0
- package/dist/element/drill.d.ts +12 -0
- package/dist/element/html.d.ts +1 -0
- package/dist/element/maya-chart.d.ts +21 -0
- package/dist/element/measure.d.ts +9 -0
- package/dist/element/select.d.ts +14 -0
- package/dist/element/tooltip.d.ts +10 -0
- package/dist/element/zoom.d.ts +11 -0
- package/dist/element.d.ts +15 -0
- package/dist/element.js +4 -0
- package/dist/element.js.map +1 -0
- package/dist/flow.d.ts +3 -0
- package/dist/flow.js +3 -0
- package/dist/flow.js.map +1 -0
- package/dist/geo.d.ts +3 -0
- package/dist/geo.js +3 -0
- package/dist/geo.js.map +1 -0
- package/dist/global.d.ts +4 -0
- package/dist/hierarchy.d.ts +15 -0
- package/dist/hierarchy.js +3 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/maya.global.js +4 -0
- package/dist/maya.global.js.map +1 -0
- package/dist/styles/theme.d.ts +1 -0
- package/dist/theme.css +1 -0
- package/llms.txt +102 -0
- package/package.json +105 -0
- package/schema.json +381 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bibek Shrestha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
# mayaCharts
|
|
2
|
+
|
|
3
|
+
Beautiful, accessible charts in five lines. Zero dependencies. The browser is the chart engine.
|
|
4
|
+
|
|
5
|
+
**Status:** Milestone 2 in progress. Not yet published to npm.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Existing chart libraries were designed 2011-2016 and hand-roll animation, tooltip positioning, theming and framework wrappers. mayaCharts uses the modern platform instead: Custom Elements, Web Animations API, CSS Anchor Positioning + Popover, CSS custom properties with light-dark(), and container queries. This means it ships small, is accessible and SSR-safe by default, and stays low-maintenance. See the [full landscape research](docs/research/reports/Open%20source%20chart%20library%20landscape.md) for what exists.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
HTML:
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/mayacharts/dist/element.js"></script>
|
|
17
|
+
<maya-chart style="height: 300px">
|
|
18
|
+
<script type="application/json">
|
|
19
|
+
{
|
|
20
|
+
"type": "bar",
|
|
21
|
+
"x": "month",
|
|
22
|
+
"y": "revenue",
|
|
23
|
+
"data": [
|
|
24
|
+
{ "month": "Jan", "revenue": 12 },
|
|
25
|
+
{ "month": "Feb", "revenue": 19 }
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
</maya-chart>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
JavaScript:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import "mayacharts/element";
|
|
36
|
+
const chart = document.querySelector("maya-chart");
|
|
37
|
+
chart.spec = { type: "bar", data, x: "month", y: "revenue", series: "region", stack: true };
|
|
38
|
+
chart.data = newRows; // animates the update
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Server-side rendering:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { render, renderShell } from "mayacharts";
|
|
45
|
+
const svg = render(spec, { width: 640, height: 320 }); // bare SVG string
|
|
46
|
+
const html = renderShell(spec, { width: 640, height: 320 }); // full chart, works without JS
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## The spec
|
|
50
|
+
|
|
51
|
+
Rule: `x` is always the category, `y` is always the value, whatever the orientation. `yDomain` is always the value axis.
|
|
52
|
+
|
|
53
|
+
### Data
|
|
54
|
+
|
|
55
|
+
| Field | Type | Applies to | Default | Meaning |
|
|
56
|
+
| ----------- | ------------------------------ | --------------------- | ---------- | ------------------------------------------------------------------------------------- |
|
|
57
|
+
| `$schema` | string | all | - | Ignored; for editors and LLMs |
|
|
58
|
+
| `type` | enum | all | required | `bar line area scatter heatmap waterfall kpi dumbbell treemap sunburst sankey hexmap` |
|
|
59
|
+
| `data` | Row[] | all | required | Row objects |
|
|
60
|
+
| `aggregate` | sum / mean / count / min / max | all | sum | How rows sharing a (category, series) combine; `count` counts non-null y |
|
|
61
|
+
| `sort` | asc / desc | bar line area heatmap | data order | Categories by total across all series |
|
|
62
|
+
| `limit` | positive integer | bar line area heatmap | - | Keep top N categories; rest roll up into "Other" |
|
|
63
|
+
|
|
64
|
+
### Encoding
|
|
65
|
+
|
|
66
|
+
| Field | Type | Applies to | Meaning |
|
|
67
|
+
| ------------ | ---------------- | ------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
68
|
+
| `x` | field | all but path types | Category; scatter numeric x; hexmap state |
|
|
69
|
+
| `y` | field or field[] | all | Value; array adds measure toggle |
|
|
70
|
+
| `series` | field | bar line area scatter heatmap dumbbell | Split into series; heatmap row category; dumbbell exactly two (from, to) |
|
|
71
|
+
| `path` | field[] | treemap sunburst sankey; bar/line/area with drill | Hierarchy outer to inner; replaces `x` |
|
|
72
|
+
| `size` | field | scatter | Bubble area (sqrt scale) |
|
|
73
|
+
| `name` | field | scatter | Point identity and tooltip title |
|
|
74
|
+
| `totals` | string[] | waterfall | x values drawn as running-total bars |
|
|
75
|
+
| `stack` | boolean | bar area | Stack series instead of grouping |
|
|
76
|
+
| `horizontal` | boolean | bar dumbbell | Categories on the left axis |
|
|
77
|
+
| `y2` | field | bar | Second value field as a line on right axis (vertical bars only) |
|
|
78
|
+
|
|
79
|
+
### Formatting
|
|
80
|
+
|
|
81
|
+
| Field | Type | Default | Meaning |
|
|
82
|
+
| ------------- | ----------------- | ------------------------------- | ------------------------------------------------------- |
|
|
83
|
+
| `format` | preset or options | auto | Per field or one string for all measures; display only |
|
|
84
|
+
| `titles` | {[field]: string} | field names | Display names everywhere (axis, tooltip, legend, table) |
|
|
85
|
+
| `labels` | boolean | false (heatmap: true at ≥24 px) | Formatted value on marks |
|
|
86
|
+
| `text` | {[key]: string} | English | Localisable UI strings with {0} placeholders |
|
|
87
|
+
| `locale` | BCP 47 | en-US | Formatting locale |
|
|
88
|
+
| `currency` | ISO 4217 | USD | Currency for the currency preset |
|
|
89
|
+
| `title` | string | - | Visible heading and accessible name |
|
|
90
|
+
| `description` | string | auto | Accessible description |
|
|
91
|
+
| `yDomain` | [min, max] | - | Fixed value-axis domain |
|
|
92
|
+
| `xDomain` | [min, max] | - | Fixed x domain (scatter only) |
|
|
93
|
+
|
|
94
|
+
### Interaction
|
|
95
|
+
|
|
96
|
+
| Field | Type | Applies to | Default | Meaning |
|
|
97
|
+
| --------- | -------------- | -------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------ |
|
|
98
|
+
| `tooltip` | boolean | all | true | Hover/keyboard tooltip |
|
|
99
|
+
| `legend` | boolean | all | true when `series` is set | Legend; clicking toggles series |
|
|
100
|
+
| `drill` | boolean | treemap sunburst sankey; bar line area with `path` | false | Click/Enter zooms into a branch of path; breadcrumb, Back and Escape pop |
|
|
101
|
+
| `select` | true / "multi" | all but sankey | off | Click/Enter/legend selects marks; Escape clears. Not with drill |
|
|
102
|
+
| `zoom` | boolean | line area scatter | false | Drag to zoom; Reset, double-click, Escape restore |
|
|
103
|
+
| `animate` | boolean | all (element only) | true | Animate updates |
|
|
104
|
+
|
|
105
|
+
### Style
|
|
106
|
+
|
|
107
|
+
| Field | Type | Applies to | Default | Meaning |
|
|
108
|
+
| --------- | ----------------------------- | -------------------------- | ------------- | ------------------------------------------------------------------- |
|
|
109
|
+
| `colors` | string[] or {[series]: color} | all | theme palette | Max 8; slot assignment stable across updates |
|
|
110
|
+
| `colorBy` | "sign" / {target: n} / field | all but heatmap and sankey | - | Tone by sign or target, or ramp by numeric field. Not with `series` |
|
|
111
|
+
| `theme` | {[token]: css} | all | {} | Theme token overrides (CSS values, allowlisted) |
|
|
112
|
+
| `grid` | boolean | all | true | Grid lines perpendicular to the value axis |
|
|
113
|
+
| `xAxis` | boolean | all | true | Bottom axis |
|
|
114
|
+
| `yAxis` | boolean | all | true | Left axis |
|
|
115
|
+
| `table` | boolean | all | true | Visually hidden data table for screen readers |
|
|
116
|
+
|
|
117
|
+
## Canonical examples
|
|
118
|
+
|
|
119
|
+
**Currency bar**
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"type": "bar",
|
|
124
|
+
"x": "month",
|
|
125
|
+
"y": "revenue",
|
|
126
|
+
"format": "currency",
|
|
127
|
+
"data": [{ "month": "Jan", "revenue": 10500 }]
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Stacked bar**
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"type": "bar",
|
|
136
|
+
"x": "state",
|
|
137
|
+
"y": "units",
|
|
138
|
+
"series": "region",
|
|
139
|
+
"stack": true,
|
|
140
|
+
"data": [{ "state": "CA", "region": "West", "units": 120 }]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Horizontal sorted limited labelled bar**
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"type": "bar",
|
|
149
|
+
"horizontal": true,
|
|
150
|
+
"x": "product",
|
|
151
|
+
"y": "margin",
|
|
152
|
+
"sort": "desc",
|
|
153
|
+
"limit": 5,
|
|
154
|
+
"labels": true,
|
|
155
|
+
"data": [{ "product": "A", "margin": 22 }]
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Sign-coloured percent bar**
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"type": "bar",
|
|
164
|
+
"x": "metric",
|
|
165
|
+
"y": "variance",
|
|
166
|
+
"format": "percent",
|
|
167
|
+
"colorBy": "sign",
|
|
168
|
+
"data": [{ "metric": "revenue", "variance": 0.15 }]
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Multi-measure line with date format and zoom**
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"type": "line",
|
|
177
|
+
"x": "date",
|
|
178
|
+
"y": ["revenue", "units"],
|
|
179
|
+
"zoom": true,
|
|
180
|
+
"format": { "date": "date", "revenue": "currency" },
|
|
181
|
+
"data": [{ "date": "2024-01-01", "revenue": 10000, "units": 50 }]
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Stacked area**
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"type": "area",
|
|
190
|
+
"x": "month",
|
|
191
|
+
"y": "sales",
|
|
192
|
+
"series": "region",
|
|
193
|
+
"stack": true,
|
|
194
|
+
"data": [{ "month": "Jan", "region": "North", "sales": 1500 }]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Waterfall with totals**
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"type": "waterfall",
|
|
203
|
+
"x": "stage",
|
|
204
|
+
"y": "amount",
|
|
205
|
+
"totals": ["Q1", "FY"],
|
|
206
|
+
"data": [{ "stage": "Q1", "amount": 100 }]
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Bubble scatter with select**
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"type": "scatter",
|
|
215
|
+
"x": "population",
|
|
216
|
+
"y": "gdp",
|
|
217
|
+
"size": "area",
|
|
218
|
+
"name": "country",
|
|
219
|
+
"select": "multi",
|
|
220
|
+
"data": [{ "country": "USA", "population": 331, "gdp": 23, "area": 9.8 }]
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Heatmap count**
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"type": "heatmap",
|
|
229
|
+
"x": "hour",
|
|
230
|
+
"y": "traffic",
|
|
231
|
+
"aggregate": "count",
|
|
232
|
+
"data": [{ "hour": "09", "traffic": "high" }]
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Treemap drill**
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"type": "treemap",
|
|
241
|
+
"path": ["region", "state", "product"],
|
|
242
|
+
"y": "revenue",
|
|
243
|
+
"drill": true,
|
|
244
|
+
"data": [{ "region": "West", "state": "CA", "product": "A", "revenue": 5000 }]
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**KPI with monthly delta and sparkline**
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"type": "kpi",
|
|
253
|
+
"x": "month",
|
|
254
|
+
"y": "sales",
|
|
255
|
+
"format": "currency",
|
|
256
|
+
"data": [
|
|
257
|
+
{ "month": "Mar", "sales": 45000 },
|
|
258
|
+
{ "month": "Apr", "sales": 50700 },
|
|
259
|
+
{ "month": "May", "sales": 54500 }
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Dumbbell horizontal**
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{
|
|
268
|
+
"type": "dumbbell",
|
|
269
|
+
"x": "region",
|
|
270
|
+
"y": "revenue",
|
|
271
|
+
"series": "year",
|
|
272
|
+
"horizontal": true,
|
|
273
|
+
"format": "currency",
|
|
274
|
+
"data": [
|
|
275
|
+
{ "region": "North", "year": "2024", "revenue": 45000 },
|
|
276
|
+
{ "region": "North", "year": "2025", "revenue": 52000 }
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Bar with dual axis (line overlay)**
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"type": "bar",
|
|
286
|
+
"x": "month",
|
|
287
|
+
"y": "revenue",
|
|
288
|
+
"y2": "customers",
|
|
289
|
+
"format": { "revenue": "currency", "customers": "integer" },
|
|
290
|
+
"data": [
|
|
291
|
+
{ "month": "Jan", "revenue": 10500, "customers": 120 },
|
|
292
|
+
{ "month": "Feb", "revenue": 12000, "customers": 145 }
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Modules
|
|
298
|
+
|
|
299
|
+
Each module extends the core with chart types and shares the same spec, theme, tooltip, a11y, and animation.
|
|
300
|
+
|
|
301
|
+
| Module | Types | Size budget (gzip) |
|
|
302
|
+
| ---------------------- | ------------------------------- | ------------------ |
|
|
303
|
+
| `mayacharts/hierarchy` | treemap, sunburst | 3 KB |
|
|
304
|
+
| `mayacharts/flow` | sankey | 2.5 KB |
|
|
305
|
+
| `mayacharts/geo` | hexmap (50 US states + DC + PR) | 3.5 KB |
|
|
306
|
+
|
|
307
|
+
## Global build
|
|
308
|
+
|
|
309
|
+
Paste this anywhere:
|
|
310
|
+
|
|
311
|
+
```html
|
|
312
|
+
<script
|
|
313
|
+
src="https://cdn.jsdelivr.net/npm/mayacharts/dist/maya.global.js"
|
|
314
|
+
integrity="sha384-REPLACE_AT_RELEASE"
|
|
315
|
+
crossorigin="anonymous"
|
|
316
|
+
></script>
|
|
317
|
+
<script>
|
|
318
|
+
const chart = document.createElement("maya-chart");
|
|
319
|
+
chart.spec = { type: "bar", data, x: "month", y: "revenue" };
|
|
320
|
+
document.body.appendChild(chart);
|
|
321
|
+
</script>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The `integrity` value above is a placeholder: the real SRI hash is published with each release. Pin the version in the URL when you use it.
|
|
325
|
+
|
|
326
|
+
Or inline it in a sandbox:
|
|
327
|
+
|
|
328
|
+
```html
|
|
329
|
+
<script type="text/javascript">
|
|
330
|
+
// (paste the entire maya.global.js here)
|
|
331
|
+
</script>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The module is available as `globalThis.maya.render()`, `maya.renderShell()`, and the `<maya-chart>` element registers automatically.
|
|
335
|
+
|
|
336
|
+
## Interactions
|
|
337
|
+
|
|
338
|
+
Always on (opt out with `false`): tooltip, hover-dim, legend toggle, keyboard, crosshair on line/area.
|
|
339
|
+
|
|
340
|
+
Opt-in: `drill`, `select`, `zoom`. Two-way: `el.view`, `el.selected`.
|
|
341
|
+
|
|
342
|
+
### Keyboard
|
|
343
|
+
|
|
344
|
+
| Key | Action |
|
|
345
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
346
|
+
| Tab | Move focus between the chart, legend buttons, measure toggle, breadcrumb and Reset chip |
|
|
347
|
+
| Arrow Left/Right | Previous/next category (same series); on the measure toggle, previous/next measure |
|
|
348
|
+
| Arrow Up/Down | Previous/next series in the same category; on the measure toggle, previous/next measure |
|
|
349
|
+
| Home / End | First/last measure (measure toggle) |
|
|
350
|
+
| Enter | Drill into the focused mark, else select it |
|
|
351
|
+
| Space | Pin or unpin the tooltip on the focused mark |
|
|
352
|
+
| Escape | First of: unpin tooltip, cancel brush in progress, clear selection, reset zoom window, pop one drill level |
|
|
353
|
+
|
|
354
|
+
Touch: show tooltip on pointerup if moved < 4 px.
|
|
355
|
+
|
|
356
|
+
## Events and two-way sync
|
|
357
|
+
|
|
358
|
+
Four events, all `bubbles: true, composed: true`:
|
|
359
|
+
|
|
360
|
+
- `maya-select {selected: Sel[], target: (Sel & {value}) | null}` - mark selected
|
|
361
|
+
- `maya-view {measure, drill, window, hidden}` - measure toggled, drilled, zoomed, or a legend series hidden (user actions only; `window` is the zoom slice)
|
|
362
|
+
- `maya-error {code, path, message}` - spec error (cancelable; preventDefault() hides error box)
|
|
363
|
+
- `maya-render {}` - render complete (ThoughtSpot: call `viz.events.emitRenderCompletedEvent()`)
|
|
364
|
+
|
|
365
|
+
Properties: `el.view` and `el.selected` (getters and setters; no events on set).
|
|
366
|
+
|
|
367
|
+
Example: selecting in one chart drives another:
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
chart1.addEventListener("maya-select", (e) => {
|
|
371
|
+
if (e.detail.target) {
|
|
372
|
+
chart2.selected = [e.detail.target];
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Export
|
|
378
|
+
|
|
379
|
+
```js
|
|
380
|
+
const svg = el.toSVG(); // standalone SVG with CSS custom properties resolved
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
PNG via canvas:
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
const svg = el.toSVG();
|
|
387
|
+
const img = new Image();
|
|
388
|
+
img.onload = () => {
|
|
389
|
+
const canvas = document.createElement("canvas");
|
|
390
|
+
canvas.width = 640;
|
|
391
|
+
canvas.height = 320;
|
|
392
|
+
canvas.getContext("2d").drawImage(img, 0, 0);
|
|
393
|
+
canvas.toBlob((blob) => saveAs(blob, "chart.png"));
|
|
394
|
+
};
|
|
395
|
+
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Frameworks
|
|
399
|
+
|
|
400
|
+
**React 19**
|
|
401
|
+
|
|
402
|
+
```jsx
|
|
403
|
+
import "mayacharts/element";
|
|
404
|
+
export const Chart = (props) => (
|
|
405
|
+
<maya-chart onmaya-select={(e) => console.log(e.detail)} {...props} />
|
|
406
|
+
);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**React 18**
|
|
410
|
+
|
|
411
|
+
```jsx
|
|
412
|
+
import { useRef } from "react";
|
|
413
|
+
import "mayacharts/element";
|
|
414
|
+
export const Chart = ({ spec }) => {
|
|
415
|
+
const ref = useRef(null);
|
|
416
|
+
return <maya-chart ref={ref} spec={JSON.stringify(spec)} />;
|
|
417
|
+
};
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Vue**
|
|
421
|
+
|
|
422
|
+
```vue
|
|
423
|
+
<script setup>
|
|
424
|
+
// vite.config: vue({ template: { compilerOptions: { isCustomElement: (t) => t === "maya-chart" } } })
|
|
425
|
+
import { ref } from "vue";
|
|
426
|
+
import "mayacharts/element";
|
|
427
|
+
const spec = ref({ type: "bar", ... });
|
|
428
|
+
</script>
|
|
429
|
+
<template>
|
|
430
|
+
<maya-chart :spec.prop="spec" @maya-select="handle" />
|
|
431
|
+
</template>
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Svelte 5**
|
|
435
|
+
|
|
436
|
+
```svelte
|
|
437
|
+
<script>
|
|
438
|
+
import "mayacharts/element";
|
|
439
|
+
let spec = { type: "bar", ... };
|
|
440
|
+
</script>
|
|
441
|
+
<maya-chart {spec} onmaya-select={(e) => console.log(e.detail)} />
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Angular**
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
|
|
448
|
+
@Component({
|
|
449
|
+
selector: "app-chart",
|
|
450
|
+
template: `<maya-chart [spec]="spec" (maya-select)="handle($event)"></maya-chart>`,
|
|
451
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
452
|
+
})
|
|
453
|
+
export class ChartComponent { ... }
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Plain HTML**
|
|
457
|
+
|
|
458
|
+
```html
|
|
459
|
+
<maya-chart id="chart" style="height: 300px"></maya-chart>
|
|
460
|
+
<script type="module">
|
|
461
|
+
const chart = document.getElementById("chart");
|
|
462
|
+
chart.spec = { type: "bar", ... };
|
|
463
|
+
</script>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Node SSR**
|
|
467
|
+
|
|
468
|
+
```js
|
|
469
|
+
import { renderShell } from "mayacharts";
|
|
470
|
+
const html = renderShell(spec, { width: 640, height: 320 });
|
|
471
|
+
res.send(html);
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Embedding in hosts
|
|
475
|
+
|
|
476
|
+
Map columnar data `{schema, data}` to row objects, then call `render()` or set `el.spec`. Listen for `maya-render` to signal the host.
|
|
477
|
+
|
|
478
|
+
Generic recipe:
|
|
479
|
+
|
|
480
|
+
```js
|
|
481
|
+
const columns = { month: ["Jan", "Feb"], revenue: [12, 19] };
|
|
482
|
+
const data = Object.entries(columns).map(([field, values]) =>
|
|
483
|
+
Object.fromEntries(values.map((v, i) => [field, v])),
|
|
484
|
+
);
|
|
485
|
+
const spec = { type: "bar", x: "month", y: "revenue", data };
|
|
486
|
+
chart.spec = spec;
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**ThoughtSpot**: Call `viz.getDataFromSearchQuery().getData()` to get the columnar table, then `viz.events.emitRenderCompletedEvent()` on `maya-render`:
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
const table = viz.getDataFromSearchQuery().getData();
|
|
493
|
+
const rows = table.columns.map((col) =>
|
|
494
|
+
Object.fromEntries(col.values.map((v, i) => [col.name, v])),
|
|
495
|
+
);
|
|
496
|
+
chart.data = rows;
|
|
497
|
+
chart.addEventListener("maya-render", () => {
|
|
498
|
+
viz.events.emitRenderCompletedEvent();
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Theming
|
|
503
|
+
|
|
504
|
+
Every colour and font is a CSS custom property. Set them on the element or host:
|
|
505
|
+
|
|
506
|
+
```css
|
|
507
|
+
maya-chart {
|
|
508
|
+
--maya-accent: oklch(0.65 0.2 145);
|
|
509
|
+
--maya-font: "Inter", sans-serif;
|
|
510
|
+
--maya-series-1: #2563eb;
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Theme tokens
|
|
515
|
+
|
|
516
|
+
| Token | CSS variable | Light default | Dark default | Colours |
|
|
517
|
+
| --------- | ---------------------------------- | ------------------------------------ | --------------------- | ------------------------------- |
|
|
518
|
+
| font | --maya-font | system-ui, sans-serif | system-ui, sans-serif | Axis labels, tooltip text |
|
|
519
|
+
| fontSize | --maya-font-size | 12px | 12px | All text |
|
|
520
|
+
| fg | --maya-fg | #1f2328 | #e6edf3 | Title, value labels |
|
|
521
|
+
| fgMuted | --maya-fg-muted | #656d76 | #9198a1 | Axis text, legend, breadcrumb |
|
|
522
|
+
| grid | --maya-grid | fg at 12% (color-mix) | fg at 12% (color-mix) | Grid lines |
|
|
523
|
+
| bg | --maya-bg | #fff | #0d1117 | Background, tooltip text |
|
|
524
|
+
| accent | --maya-accent | oklch(.6 .17 255) | oklch(.6 .17 255) | Focus outline, series 1 |
|
|
525
|
+
| radius | --maya-radius | 2px | 2px | Mark border radius |
|
|
526
|
+
| tooltipBg | --maya-tooltip-bg | --maya-fg | --maya-fg | Tooltip background |
|
|
527
|
+
| tooltipFg | --maya-tooltip-fg | --maya-bg | --maya-bg | Tooltip text |
|
|
528
|
+
| focus | --maya-focus | --maya-accent | --maya-accent | Keyboard focus ring |
|
|
529
|
+
| good | --maya-good | #1a7f37 | #3fb950 | colorBy positive / above target |
|
|
530
|
+
| bad | --maya-bad | #cf222e | #f85149 | colorBy negative / below target |
|
|
531
|
+
| series1-8 | --maya-series-1 to --maya-series-8 | 1: --maya-accent; 2-8: oklch presets | same as light | Series colours (max 8) |
|
|
532
|
+
|
|
533
|
+
Brand palette example:
|
|
534
|
+
|
|
535
|
+
```css
|
|
536
|
+
maya-chart {
|
|
537
|
+
--maya-accent: #7c3aed;
|
|
538
|
+
--maya-series-1: #7c3aed;
|
|
539
|
+
--maya-series-2: #ec4899;
|
|
540
|
+
--maya-series-3: #f59e0b;
|
|
541
|
+
--maya-good: #10b981;
|
|
542
|
+
--maya-bad: #ef4444;
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Dark mode is automatic via `color-scheme: light dark`. Override with `prefers-color-scheme`:
|
|
547
|
+
|
|
548
|
+
```css
|
|
549
|
+
@media (prefers-color-scheme: dark) {
|
|
550
|
+
maya-chart {
|
|
551
|
+
--maya-accent: #60a5fa;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
High contrast systems (forced-colors: active) are supported; outline tones instead of colour.
|
|
557
|
+
|
|
558
|
+
## Security
|
|
559
|
+
|
|
560
|
+
The chart spec is plain JSON generated by end users or LLMs, not trusted code. mayaCharts escapes all text, allowlists CSS values, and works inside Trusted Types policies.
|
|
561
|
+
|
|
562
|
+
### Implementation
|
|
563
|
+
|
|
564
|
+
- All text is HTML-escaped before rendering
|
|
565
|
+
- CSS values for colors and theme are allowlisted (blocks url(), expression() and injection syntax)
|
|
566
|
+
- `renderShell()` embeds only the spec fields actively used (data minimisation)
|
|
567
|
+
- A11y table shows encoded fields only, capped at 1000 rows
|
|
568
|
+
- No inline styles: Dynamic styles use CSSOM `setProperty()` or CSS custom properties
|
|
569
|
+
- No network requests or telemetry
|
|
570
|
+
- Compatible with Trusted Types policy `"mayacharts"` (call `trustedTypes.createPolicy("mayacharts", {createHTML: s => s})`)
|
|
571
|
+
- Supports CSP `trusted-types mayacharts` and `nonce` on the shell style tag
|
|
572
|
+
|
|
573
|
+
See [SECURITY.md](SECURITY.md) for the full security model.
|
|
574
|
+
|
|
575
|
+
## Accessibility
|
|
576
|
+
|
|
577
|
+
Fully conformant with WCAG 2.2 AA, verified by axe-core on every gallery tile in both light and dark modes.
|
|
578
|
+
|
|
579
|
+
Implemented: semantic role and title, accessible description, data table for screen readers, keyboard navigation (Tab, arrows, Enter, Space, Escape), live region updates, focus management, forced-colors support, reduced motion support, ≥3:1 contrast in both light and dark modes, non-colour cues (text tone), 12 px touch target enlargement.
|
|
580
|
+
|
|
581
|
+
Known ceilings: label truncation at 40% width; UTC dates unless Intl options say otherwise; scatter keyboard order follows draw order (not spatial); stacked bar labels show segments, not totals.
|
|
582
|
+
|
|
583
|
+
See [STABILITY.md](STABILITY.md) for the full accessibility and performance envelope.
|
|
584
|
+
|
|
585
|
+
## Performance
|
|
586
|
+
|
|
587
|
+
Upper bounds asserted by `test/perf.test.ts` for `render()` in Node (test thresholds, not measured timings; output is uncompressed SVG string length):
|
|
588
|
+
|
|
589
|
+
| Scenario | Marks | Render time threshold | Output threshold |
|
|
590
|
+
| ------------------------ | ----- | --------------------- | ---------------- |
|
|
591
|
+
| 5k-category bar | 5000 | < 400 ms | < 2.2 MB |
|
|
592
|
+
| 5k-point scatter | 5000 | < 200 ms | - |
|
|
593
|
+
| Heatmap 50x52 | 2600 | < 100 ms | - |
|
|
594
|
+
| 20k-row bar, `limit: 20` | 21 | < 400 ms | < 200 KB |
|
|
595
|
+
|
|
596
|
+
Animation skips above 1500 marks. 5000-mark hard cap suggests `limit` or `aggregate`. Table capped at 1000 rows.
|
|
597
|
+
|
|
598
|
+
## Errors
|
|
599
|
+
|
|
600
|
+
Every error links to [errors.html](site/errors.html#<code>) with a one-paragraph cause and a fixed example. Error shape: `{code, path, message}`.
|
|
601
|
+
|
|
602
|
+
## Development
|
|
603
|
+
|
|
604
|
+
```bash
|
|
605
|
+
npm install
|
|
606
|
+
npm test # unit, property, fuzz, hostile-string, perf, leak tests
|
|
607
|
+
npm run build # dist/
|
|
608
|
+
npm run size # check budgets
|
|
609
|
+
npm run e2e # Playwright
|
|
610
|
+
npm run dev # demo site at localhost:5173
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
## Versioning
|
|
614
|
+
|
|
615
|
+
mayaCharts follows semantic versioning. Public API: spec keys and semantics, schema.json, error code/path, element properties/methods, event detail shapes, CSS custom property tokens, data-* attribute roles and values, .maya-* class names. See [STABILITY.md](STABILITY.md) for the full stability policy and pre-1.0 deprecation path via HINTS.
|
|
616
|
+
|
|
617
|
+
## License
|
|
618
|
+
|
|
619
|
+
MIT, and the core library will stay MIT. Any paid offerings will be things outside the core, such as support contracts, hosted services and separate add-ons. Nothing in the library will move behind a paywall. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResolvedSpec, Shaped } from "./types.ts";
|
|
2
|
+
export declare const titleText: (spec: ResolvedSpec) => string;
|
|
3
|
+
export type Fmt = (field: string, v: unknown, step?: number) => string;
|
|
4
|
+
export declare function describe(spec: ResolvedSpec, shaped: Shaped, fmt: Fmt, noun: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Visually hidden table. Cartesian/heatmap: category rows x series columns. Scatter and path
|
|
7
|
+
* types: the raw rows, only the encoded fields. Capped at 1000 rows. `tone` returns the tone
|
|
8
|
+
* word for a value (never colour alone) or null.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dataTable(spec: ResolvedSpec, shaped: Shaped, fmt: Fmt, tone: (v: number) => string | null): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Axis, Box, LinearScale, ResolvedSpec, Scale } from "./types.ts";
|
|
2
|
+
export interface Frame {
|
|
3
|
+
plot: Box;
|
|
4
|
+
/** Bottom and left scales; null when the mark has no such axis. */
|
|
5
|
+
x: Scale | null;
|
|
6
|
+
y: Scale | null;
|
|
7
|
+
/** Right linear scale (third axis); null when the mark asks for none. */
|
|
8
|
+
y2: LinearScale | null;
|
|
9
|
+
grid: string;
|
|
10
|
+
/** Inner markup of the axis-y and axis-x groups. */
|
|
11
|
+
ay: string;
|
|
12
|
+
ax: string;
|
|
13
|
+
/** svg attributes: data-plot, data-n | data-xd + data-yd. */
|
|
14
|
+
attrs: Record<string, string | number | null>;
|
|
15
|
+
}
|
|
16
|
+
type Fmt = (field: string, v: unknown, step?: number) => string;
|
|
17
|
+
/** Axes, grid, plot box and scales for the two axes a mark asks for. */
|
|
18
|
+
export declare function frame(spec: ResolvedSpec, [bx, ly, ry]: [Axis, Axis, Axis?], size: {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}, fmt: Fmt): Frame;
|
|
22
|
+
export {};
|