plotleaf 0.2.0 → 0.3.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/README.md CHANGED
@@ -12,9 +12,9 @@ A report is one Markdown file, optionally accompanied by local JSON datasets. No
12
12
 
13
13
  The initial MVP includes validation, live preview/reload, self-contained HTML export and a generated component specification. All bundled examples use fictional data.
14
14
 
15
- Version **0.2.0** adds donut charts and prepared funnel/progress views to the Markdown report language.
15
+ **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
16
 
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.
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, maps, subtotals and reusable compositions remain planned increments.
18
18
 
19
19
  ## Quick start
20
20
 
@@ -79,7 +79,7 @@ Attributes are literal values, not JavaScript. Keep leaf directives on one line
79
79
 
80
80
  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
81
 
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.
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/`, `examples/onboarding/` and `examples/catalog/` contain only fictional data. Catalogue illustrations are original, purpose-built raster assets.
83
83
 
84
84
  ## Donut and funnel
85
85
 
@@ -94,9 +94,26 @@ Funnel data supplies each stage explicitly, for example `{"stage":"Activated","t
94
94
 
95
95
  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
96
 
97
+ ## Image and sparkline columns
98
+
99
+ ```markdown
100
+ :::table{data="./products.json" title="Product performance"}
101
+ ::column{field="image" label="Preview" kind="image" alt="description"}
102
+ ::column{field="product" label="Product"}
103
+ ::column{field="weekly_orders" label="Weekly orders" kind="sparkline" sortBy="last_week"}
104
+ ::column{field="revenue" label="Revenue" format="currency:USD"}
105
+ :::
106
+ ```
107
+
108
+ 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.
109
+
110
+ 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.
111
+
112
+ 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.
113
+
97
114
  ## Data and security boundaries
98
115
 
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.
116
+ 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
117
 
101
118
  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
119
 
@@ -110,14 +127,14 @@ Use Bun 1.4.4 for development and Node 22+ for the published CLI/runtime checks:
110
127
  bun install --frozen-lockfile
111
128
  bun run build
112
129
 
113
- node dist/cli.js examples/storefront/report.md
130
+ node dist/cli.js examples/report.md
114
131
  node dist/cli.js validate examples/storefront/report.md --json
115
132
  node dist/cli.js build examples/storefront/report.md
116
133
  ```
117
134
 
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.
135
+ 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
136
 
120
- Open `examples/storefront/report.html` after export, or try the narrative example in `examples/adoption/report.md`.
137
+ 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
138
 
122
139
  ## Development and verification
123
140
 
@@ -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;