plotleaf 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -8,13 +8,40 @@ Beautiful, interactive reports from Markdown and prepared JSON. Built for coding
8
8
 
9
9
  A report is one Markdown file, optionally accompanied by local JSON datasets. No application, configuration file, account, database or hosted service. Export one HTML file that keeps working offline.
10
10
 
11
+ ## Start with your coding agent
12
+
13
+ Copy this into your agent and replace the report brief:
14
+
15
+ ```text
16
+ Create a polished, interactive report with plotleaf.
17
+
18
+ Report brief: [Describe the report and point to the data you want to use.]
19
+
20
+ Use Node 22+ and npx; no app scaffolding, Bun or account is needed.
21
+ First run `npx --yes plotleaf --help`. If it lists `guide`, read
22
+ `npx --yes plotleaf guide`. Otherwise read the authoring guide at:
23
+ https://raw.githubusercontent.com/LoueeD/plotleaf/v0.3.0/skills/plotleaf/SKILL.md
24
+ Then run `npx --yes plotleaf schema` for the installed component API.
25
+
26
+ Use the data I provide; don't invent values or findings. If the brief or
27
+ data is missing, ask for what's needed. Prepare one report.md and any local
28
+ JSON/assets it needs, following the guide and installed schema.
29
+
30
+ Run `npx --yes plotleaf validate report.md --json` and fix all errors.
31
+ Start `npx --yes plotleaf dev report.md --no-open` and give me the preview URL.
32
+ Build `npx --yes plotleaf build report.md --out report.html` and give me
33
+ the source and portable HTML paths, with any data limitations.
34
+ ```
35
+
36
+ The `guide` command is available in **0.3.1 or newer**. The prompt also works with 0.3.0 through the linked authoring guide.
37
+
11
38
  ## Status
12
39
 
13
40
  The initial MVP includes validation, live preview/reload, self-contained HTML export and a generated component specification. All bundled examples use fictional data.
14
41
 
15
- Version **0.2.0** adds donut charts and prepared funnel/progress views to the Markdown report language.
42
+ **0.3.0** adds local PNG/JPEG image cells, static sparklines and a cumulative example report. Donut charts and prepared funnel/progress views are available since 0.2.0. Check your installed CLI version or schema before using newer components.
16
43
 
17
- Implemented components: responsive grids, metrics/comparisons, line/area/bar/donut charts, prepared funnel/progress stages, literal chart targets, sortable/formatted tables and callouts. Reports support light/dark system preference, native chart-data disclosures and responsive layouts. Filters, rich table cells, maps and reusable compositions remain planned increments.
44
+ Implemented components: responsive grids, metrics/comparisons, line/area/bar/donut charts, prepared funnel/progress stages, literal chart targets, sortable/formatted tables and callouts. Reports support light/dark system preference, native chart-data disclosures and responsive layouts. Filters, maps, subtotals and reusable compositions remain planned increments.
18
45
 
19
46
  ## Quick start
20
47
 
@@ -30,6 +57,8 @@ npx plotleaf schema
30
57
 
31
58
  `plotleaf report.md` is an alias for `dev`. `build` defaults to `report.html` beside the source. Relative dataset paths resolve from the Markdown file's directory; an explicit `--out` resolves from the caller's working directory. Exports contain no preview connection and need no source files at viewing time.
32
59
 
60
+ For agent discovery, `npx plotleaf guide` (0.3.1+) prints the bundled report-authoring guide as Markdown, and `npx plotleaf schema` prints the component specification as JSON. The guide is the same `skills/plotleaf/SKILL.md` shipped with the package. These commands print documentation without creating files, changing project configuration or starting a server; once installed, they read local package content.
61
+
33
62
  ## A report
34
63
 
35
64
  ```markdown
@@ -79,7 +108,7 @@ Attributes are literal values, not JavaScript. Keep leaf directives on one line
79
108
 
80
109
  Numbers, comparisons, trends and findings are prepared by the author. `percent` uses ratios (`0.12` → 12%); `percentage-points` uses point values (`0.8` → 0.8 pt); `currency:GBP` uses major units. Whole currency amounts omit `.00`; nonzero cents retain two decimal places. Formatting is `en-GB`; date display is UTC. An increase does not imply positive business sentiment.
81
110
 
82
- Use `schema` for current property schemas, child rules and input limits. The installed package includes `schema/components.json`, generated from the validation registry, and `skills/plotleaf/SKILL.md`, a reusable report-authoring guide. Public examples under `examples/storefront/`, `examples/adoption/` and `examples/onboarding/` contain only fictional data.
111
+ Use `schema` for current property schemas, child rules and input limits. The installed package includes `schema/components.json`, generated from the validation registry, and `skills/plotleaf/SKILL.md`, a reusable report-authoring guide. Public examples under `examples/storefront/`, `examples/adoption/`, `examples/onboarding/` and `examples/catalog/` contain only fictional data. Catalogue illustrations are original, purpose-built raster assets.
83
112
 
84
113
  ## Donut and funnel
85
114
 
@@ -94,9 +123,26 @@ Funnel data supplies each stage explicitly, for example `{"stage":"Activated","t
94
123
 
95
124
  Try the fictional `examples/onboarding/report.md` from a source checkout. See [M3 contracts and tradeoffs](https://github.com/LoueeD/plotleaf/blob/main/docs/m3-visualizations.md).
96
125
 
126
+ ## Image and sparkline columns
127
+
128
+ ```markdown
129
+ :::table{data="./products.json" title="Product performance"}
130
+ ::column{field="image" label="Preview" kind="image" alt="description"}
131
+ ::column{field="product" label="Product"}
132
+ ::column{field="weekly_orders" label="Weekly orders" kind="sparkline" sortBy="last_week"}
133
+ ::column{field="revenue" label="Revenue" format="currency:USD"}
134
+ :::
135
+ ```
136
+
137
+ An image value is a relative PNG/JPEG path or null; `alt` names its row's text-alternative field. The fixed thumbnail box is 64 × 48. Images are bounded, validated and embedded once per unique content hash, with no source paths or runtime requests. Remote/data URLs, active SVG and animated images are rejected.
138
+
139
+ Only explicitly declared sparkline fields may contain arrays: at most 64 finite numbers/nulls in sample order. Rows share the observed y scale for that column. Nulls create gaps; a last-sample label and native value disclosure remain readable without JavaScript. Charts and scalar/inferred tables keep the flat scalar data contract.
140
+
141
+ Rich headers are plain text unless `sortBy` names a scalar field; scalar columns retain raw-value sorting. Alternatives, explicit sort keys and embedded image metadata are readable in the export. Try the fictional `examples/catalog/report.md` from a source build. See [M4 contracts](https://github.com/LoueeD/plotleaf/blob/main/docs/m4-rich-tables.md) for limits and validation details.
142
+
97
143
  ## Data and security boundaries
98
144
 
99
- Prepare JSON arrays of flat scalar records. Charts preserve input order and do not calculate, join or aggregate data. Date/number x values must be unique and increasing; line/area nulls are gaps and bar nulls are errors. Tables sort raw typed values. Explicit table columns also select the fields exported; an inferred table includes every source field.
145
+ Prepare JSON arrays of flat scalar records; only explicitly declared sparkline fields permit bounded numeric/null sequences. Charts preserve input order and do not calculate, join or aggregate data. Date/number x values must be unique and increasing; line/area nulls are gaps and bar nulls are errors. Tables sort raw typed values. Explicit table columns also select the fields exported; an inferred table includes every source field.
100
146
 
101
147
  Local JSON must stay within the report directory, including after resolving symlinks. Raw HTML, remote assets/data, executable components and report-authored code are rejected. Script/data escaping, sanitization, size limits and safe output writes are part of compilation. Invalid reports do not replace an existing export.
102
148
 
@@ -110,14 +156,14 @@ Use Bun 1.4.4 for development and Node 22+ for the published CLI/runtime checks:
110
156
  bun install --frozen-lockfile
111
157
  bun run build
112
158
 
113
- node dist/cli.js examples/storefront/report.md
159
+ node dist/cli.js examples/report.md
114
160
  node dist/cli.js validate examples/storefront/report.md --json
115
161
  node dist/cli.js build examples/storefront/report.md
116
162
  ```
117
163
 
118
- The first CLI command opens a browser and watches the Markdown and referenced JSON. Add `--no-open` to print the URL without launching a browser. Ctrl-C stops the server. A failed edit shows diagnostics and recovers after you fix it.
164
+ The first CLI command opens a browser and watches the Markdown, referenced JSON and local table images. Add `--no-open` to print the URL without launching a browser. Ctrl-C stops the server. A failed edit shows diagnostics and recovers after you fix it.
119
165
 
120
- Open `examples/storefront/report.html` after export, or try the narrative example in `examples/adoption/report.md`.
166
+ Use `bun run dev` for the cumulative review report in `examples/report.md`. It keeps metrics/comparisons, area/line/bar/donut charts, targets, funnel stages, scalar tables, image cells, sparklines, prose and callouts together as milestones are added. It reuses the focused examples' datasets and image assets; their separate periods and populations remain explicit. Focused examples, including the narrative report in `examples/adoption/report.md`, remain available.
121
167
 
122
168
  ## Development and verification
123
169
 
@@ -1 +1 @@
1
- (()=>{var d=new Intl.Collator("en-GB",{numeric:!0,sensitivity:"base"});for(let a of document.querySelectorAll("table[data-plotleaf-table]")){let r=a.tBodies[0];if(!r)continue;let i=[...r.rows],s=-1,n=!1;for(let o of a.querySelectorAll("thead button[data-column]"))o.disabled=!1,o.addEventListener("click",()=>{let u=Number(o.dataset.column);n=s===u?!n:!1,s=u;let c=i.map((e,t)=>({row:e,index:t,value:JSON.parse(e.cells[u]?.dataset.raw??"null")}));c.sort((e,t)=>{if(e.value===null||t.value===null)return e.value===t.value?e.index-t.index:e.value===null?1:-1;let l=typeof e.value==="number"&&typeof t.value==="number"?e.value-t.value:d.compare(String(e.value),String(t.value));return(n?-l:l)||e.index-t.index}),r.replaceChildren(...c.map((e)=>e.row)),a.querySelectorAll("thead th").forEach((e)=>{let t=e.contains(o);e.setAttribute("aria-sort",t?n?"descending":"ascending":"none");let l=e.querySelector(".sort-indicator");if(l)l.textContent=t?n?"↓":"↑":"↕"})})}})();
1
+ (()=>{var d=new Intl.Collator("en-GB",{numeric:!0,sensitivity:"base"});for(let a of document.querySelectorAll("table[data-plotleaf-table]")){let r=a.tBodies[0];if(!r)continue;let i=[...r.rows],u=-1,n=!1;for(let o of a.querySelectorAll("thead button[data-column]"))o.disabled=!1,o.addEventListener("click",()=>{let s=Number(o.dataset.column);n=u===s?!n:!1,u=s;let c=i.map((e,t)=>({row:e,index:t,value:JSON.parse(e.cells[s]?.dataset.raw??"null")}));c.sort((e,t)=>{if(e.value===null||t.value===null)return e.value===t.value?e.index-t.index:e.value===null?1:-1;let l=typeof e.value==="number"&&typeof t.value==="number"?e.value-t.value:d.compare(String(e.value),String(t.value));return(n?-l:l)||e.index-t.index}),r.replaceChildren(...c.map((e)=>e.row)),a.querySelectorAll("thead th[aria-sort]").forEach((e)=>{let t=e.contains(o);e.setAttribute("aria-sort",t?n?"descending":"ascending":"none");let l=e.querySelector(".sort-indicator");if(l)l.textContent=t?n?"↓":"↑":"↕"})})}})();
@@ -1,4 +1,4 @@
1
- (()=>{var eD=Object.defineProperty;var tD=(t)=>t;function rD(t,e){this[t]=tD.bind(null,e)}var iD=(t,e)=>{for(var r in e)eD(t,r,{get:e[r],enumerable:!0,configurable:!0,set:rD.bind(e,r)})};var ft={};iD(ft,{Arc:()=>Ao,BezierCurve:()=>Co,BoundingRect:()=>ae,Circle:()=>To,CompoundPath:()=>el,Ellipse:()=>Tf,Group:()=>we,HOVER_LAYER_FOR_INCREMENTAL:()=>ca,HOVER_LAYER_FROM_THRESHOLD:()=>nd,HOVER_LAYER_NO:()=>id,Image:()=>_t,IncrementalDisplayable:()=>$v,Line:()=>Ht,LinearGradient:()=>Eo,OrientedBoundingRect:()=>Pf,Path:()=>ie,Point:()=>fe,Polygon:()=>Ef,Polyline:()=>fn,RadialGradient:()=>jv,Rect:()=>De,Ring:()=>Cf,Sector:()=>dr,Text:()=>Qe,WH:()=>pn,XY:()=>ai,applyTransform:()=>Rf,calcZ2Range:()=>BL,clipPointsByRect:()=>aS,clipRectByRect:()=>oS,createIcon:()=>kf,decomposeTransform:()=>UL,ensureCopyRect:()=>Lo,ensureCopyTransform:()=>Uf,expandOrShrinkRect:()=>Oo,extendPath:()=>eS,extendShape:()=>Jx,getCurrentCanvasPainter:()=>FL,getShapeClass:()=>tS,getTransform:()=>nS,groupTransition:()=>od,initProps:()=>$e,isBoundingRectAxisAligned:()=>zf,isElementRemoved:()=>fa,lineLineIntersect:()=>sS,linePolygonIntersect:()=>RL,makeImage:()=>Nf,makePath:()=>rl,mergePath:()=>iS,payloadDisableAnimation:()=>zL,registerShape:()=>mr,removeElement:()=>Do,removeElementWithFadeOut:()=>cn,resizePath:()=>ad,retrieveZInfo:()=>il,setTooltipConfig:()=>Bf,subPixelOptimize:()=>PL,subPixelOptimizeLine:()=>pa,subPixelOptimizeRect:()=>LL,transformDirection:()=>IL,traverseElements:()=>Ci,traverseUpdateZ:()=>nl,updateProps:()=>Ve});var nD=new Intl.Collator("en-GB",{numeric:!0,sensitivity:"base"});for(let t of document.querySelectorAll("table[data-plotleaf-table]")){let e=t.tBodies[0];if(!e)continue;let r=[...e.rows],i=-1,n=!1;for(let a of t.querySelectorAll("thead button[data-column]"))a.disabled=!1,a.addEventListener("click",()=>{let o=Number(a.dataset.column);n=i===o?!n:!1,i=o;let s=r.map((l,u)=>({row:l,index:u,value:JSON.parse(l.cells[o]?.dataset.raw??"null")}));s.sort((l,u)=>{if(l.value===null||u.value===null)return l.value===u.value?l.index-u.index:l.value===null?1:-1;let f=typeof l.value==="number"&&typeof u.value==="number"?l.value-u.value:nD.compare(String(l.value),String(u.value));return(n?-f:f)||l.index-u.index}),e.replaceChildren(...s.map((l)=>l.row)),t.querySelectorAll("thead th").forEach((l)=>{let u=l.contains(a);l.setAttribute("aria-sort",u?n?"descending":"ascending":"none");let f=l.querySelector(".sort-indicator");if(f)f.textContent=u?n?"↓":"↑":"↕"})})}/*! *****************************************************************************
1
+ (()=>{var eD=Object.defineProperty;var tD=(t)=>t;function rD(t,e){this[t]=tD.bind(null,e)}var iD=(t,e)=>{for(var r in e)eD(t,r,{get:e[r],enumerable:!0,configurable:!0,set:rD.bind(e,r)})};var ft={};iD(ft,{Arc:()=>Ao,BezierCurve:()=>Co,BoundingRect:()=>ae,Circle:()=>To,CompoundPath:()=>el,Ellipse:()=>Tf,Group:()=>we,HOVER_LAYER_FOR_INCREMENTAL:()=>ca,HOVER_LAYER_FROM_THRESHOLD:()=>nd,HOVER_LAYER_NO:()=>id,Image:()=>_t,IncrementalDisplayable:()=>$v,Line:()=>Ht,LinearGradient:()=>Eo,OrientedBoundingRect:()=>Pf,Path:()=>ie,Point:()=>fe,Polygon:()=>Ef,Polyline:()=>fn,RadialGradient:()=>jv,Rect:()=>De,Ring:()=>Cf,Sector:()=>dr,Text:()=>Qe,WH:()=>pn,XY:()=>ai,applyTransform:()=>Rf,calcZ2Range:()=>BL,clipPointsByRect:()=>aS,clipRectByRect:()=>oS,createIcon:()=>kf,decomposeTransform:()=>UL,ensureCopyRect:()=>Lo,ensureCopyTransform:()=>Uf,expandOrShrinkRect:()=>Oo,extendPath:()=>eS,extendShape:()=>Jx,getCurrentCanvasPainter:()=>FL,getShapeClass:()=>tS,getTransform:()=>nS,groupTransition:()=>od,initProps:()=>$e,isBoundingRectAxisAligned:()=>zf,isElementRemoved:()=>fa,lineLineIntersect:()=>sS,linePolygonIntersect:()=>RL,makeImage:()=>Nf,makePath:()=>rl,mergePath:()=>iS,payloadDisableAnimation:()=>zL,registerShape:()=>mr,removeElement:()=>Do,removeElementWithFadeOut:()=>cn,resizePath:()=>ad,retrieveZInfo:()=>il,setTooltipConfig:()=>Bf,subPixelOptimize:()=>PL,subPixelOptimizeLine:()=>pa,subPixelOptimizeRect:()=>LL,transformDirection:()=>IL,traverseElements:()=>Ci,traverseUpdateZ:()=>nl,updateProps:()=>Ve});var nD=new Intl.Collator("en-GB",{numeric:!0,sensitivity:"base"});for(let t of document.querySelectorAll("table[data-plotleaf-table]")){let e=t.tBodies[0];if(!e)continue;let r=[...e.rows],i=-1,n=!1;for(let a of t.querySelectorAll("thead button[data-column]"))a.disabled=!1,a.addEventListener("click",()=>{let o=Number(a.dataset.column);n=i===o?!n:!1,i=o;let s=r.map((l,u)=>({row:l,index:u,value:JSON.parse(l.cells[o]?.dataset.raw??"null")}));s.sort((l,u)=>{if(l.value===null||u.value===null)return l.value===u.value?l.index-u.index:l.value===null?1:-1;let f=typeof l.value==="number"&&typeof u.value==="number"?l.value-u.value:nD.compare(String(l.value),String(u.value));return(n?-f:f)||l.index-u.index}),e.replaceChildren(...s.map((l)=>l.row)),t.querySelectorAll("thead th[aria-sort]").forEach((l)=>{let u=l.contains(a);l.setAttribute("aria-sort",u?n?"descending":"ascending":"none");let f=l.querySelector(".sort-indicator");if(f)f.textContent=u?n?"↓":"↑":"↕"})})}/*! *****************************************************************************
2
2
  Copyright (c) Microsoft Corporation.
3
3
 
4
4
  Permission to use, copy, modify, and/or distribute this software for any
@@ -470,6 +470,77 @@ tbody tr:last-child td {
470
470
  .numeric {
471
471
  text-align: end;
472
472
  }
473
+ .image-definitions {
474
+ position: absolute;
475
+ width: 0;
476
+ height: 0;
477
+ overflow: hidden;
478
+ }
479
+ .table-image {
480
+ display: block;
481
+ width: 64px;
482
+ height: 48px;
483
+ border-radius: 4px;
484
+ outline: 1px solid var(--rule);
485
+ outline-offset: -1px;
486
+ background: var(--page);
487
+ }
488
+ .sparkline-cell {
489
+ display: inline-grid;
490
+ grid-template-columns: 112px minmax(3ch, auto);
491
+ align-items: center;
492
+ gap: 2px 12px;
493
+ font-variant-numeric: tabular-nums;
494
+ }
495
+ .sparkline-chart {
496
+ display: block;
497
+ width: 112px;
498
+ height: 32px;
499
+ color: var(--series);
500
+ }
501
+ .sparkline-chart path {
502
+ stroke: currentColor;
503
+ stroke-width: 1.8px;
504
+ stroke-linecap: round;
505
+ stroke-linejoin: round;
506
+ fill: none;
507
+ }
508
+ .sparkline-chart circle {
509
+ fill: currentColor;
510
+ }
511
+ .sparkline-last {
512
+ color: var(--ink);
513
+ }
514
+ .sparkline-empty {
515
+ font-size: 12px;
516
+ color: var(--muted);
517
+ white-space: normal;
518
+ }
519
+ .sparkline-data {
520
+ grid-column: 1 / -1;
521
+ text-align: start;
522
+ font-size: 12px;
523
+ line-height: 1.5;
524
+ white-space: normal;
525
+ max-width: 190px;
526
+ color: var(--muted);
527
+ }
528
+ .sparkline-data summary {
529
+ cursor: pointer;
530
+ width: fit-content;
531
+ padding-block: 3px;
532
+ min-height: 24px;
533
+ }
534
+ .sparkline-values {
535
+ margin: 8px 0;
536
+ padding-inline-start: 26px;
537
+ max-height: 180px;
538
+ overflow: auto;
539
+ overflow-wrap: anywhere;
540
+ }
541
+ .sparkline-values li {
542
+ margin-block: 0;
543
+ }
473
544
  th button {
474
545
  border: 0;
475
546
  background: transparent;