plotleaf 0.1.0 → 0.2.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.
@@ -15,6 +15,14 @@
15
15
  --series-fill-end: rgba(79, 178, 239, 0);
16
16
  --tooltip: #ffffff;
17
17
  --target: #969ba2;
18
+ --slice-1: #429ee4;
19
+ --slice-2: #64afa1;
20
+ --slice-3: #8696b8;
21
+ --slice-4: #d4a05b;
22
+ --slice-5: #7984be;
23
+ --slice-6: #90c7df;
24
+ --slice-7: #b39581;
25
+ --slice-8: #a4acb9;
18
26
  font-family:
19
27
  ui-sans-serif,
20
28
  -apple-system,
@@ -42,6 +50,14 @@
42
50
  --series-fill-end: rgba(72, 158, 224, 0);
43
51
  --tooltip: #25272c;
44
52
  --target: #878d97;
53
+ --slice-1: #65b5ed;
54
+ --slice-2: #80c5b6;
55
+ --slice-3: #a2b2d0;
56
+ --slice-4: #e1b575;
57
+ --slice-5: #a1a9de;
58
+ --slice-6: #a1d8eb;
59
+ --slice-7: #c5aa99;
60
+ --slice-8: #b7bfcd;
45
61
  }
46
62
  }
47
63
  * {
@@ -251,6 +267,163 @@ code {
251
267
  .chart-data caption {
252
268
  font-size: 15px;
253
269
  }
270
+ .funnel-stages {
271
+ list-style: none;
272
+ padding: 0;
273
+ margin: 0;
274
+ max-width: none;
275
+ display: grid;
276
+ gap: 16px;
277
+ }
278
+ .funnel-stage {
279
+ --stage-fill: var(--series);
280
+ margin: 0;
281
+ min-width: 0;
282
+ font-size: 16px;
283
+ line-height: 1.5;
284
+ font-variant-numeric: tabular-nums;
285
+ }
286
+ .funnel-stage[data-status="on-target"] {
287
+ --stage-fill: var(--positive);
288
+ }
289
+ .funnel-stage[data-status="below-target"] {
290
+ --stage-fill: var(--warning);
291
+ }
292
+ .funnel-stage[data-status="at-risk"] {
293
+ --stage-fill: var(--negative);
294
+ }
295
+ .funnel-heading,
296
+ .funnel-values {
297
+ display: flex;
298
+ flex-wrap: wrap;
299
+ gap: 2px 12px;
300
+ }
301
+ .funnel-heading {
302
+ justify-content: space-between;
303
+ gap: 4px 16px;
304
+ overflow-wrap: anywhere;
305
+ }
306
+ .funnel-values {
307
+ color: var(--muted);
308
+ }
309
+ .funnel-values strong {
310
+ font-weight: 550;
311
+ }
312
+ .funnel-stage meter {
313
+ display: block;
314
+ appearance: none;
315
+ width: 100%;
316
+ height: 10px;
317
+ margin-block-start: 8px;
318
+ border: 0;
319
+ border-radius: 999px;
320
+ background: var(--soft);
321
+ }
322
+ .funnel-stage meter::-webkit-meter-bar {
323
+ height: 10px;
324
+ border: 0;
325
+ border-radius: 999px;
326
+ background: var(--soft);
327
+ box-shadow: none;
328
+ }
329
+ .funnel-stage meter::-webkit-meter-optimum-value,
330
+ .funnel-stage meter::-webkit-meter-suboptimum-value,
331
+ .funnel-stage meter::-webkit-meter-even-less-good-value {
332
+ border-radius: 999px;
333
+ background: var(--stage-fill);
334
+ }
335
+ .funnel-stage meter::-moz-meter-bar {
336
+ border-radius: 999px;
337
+ background: var(--stage-fill);
338
+ }
339
+ .funnel-status {
340
+ display: inline-flex;
341
+ align-items: center;
342
+ gap: 6px;
343
+ margin-block-start: 5px;
344
+ color: var(--muted);
345
+ font-size: 12px;
346
+ }
347
+ .funnel-status::before {
348
+ content: "";
349
+ width: 6px;
350
+ height: 6px;
351
+ border-radius: 50%;
352
+ background: var(--stage-fill);
353
+ }
354
+ .donut-layout {
355
+ display: flex;
356
+ flex-wrap: wrap;
357
+ align-items: center;
358
+ justify-content: center;
359
+ gap: 12px 24px;
360
+ }
361
+ .donut-plot {
362
+ flex: 1 1 180px;
363
+ min-width: 0;
364
+ max-width: 300px;
365
+ height: 260px;
366
+ }
367
+ .donut-legend {
368
+ flex: 1 1 210px;
369
+ min-width: 0;
370
+ max-width: 500px;
371
+ list-style: none;
372
+ padding: 0;
373
+ margin: 0;
374
+ font-size: 14px;
375
+ line-height: 1.5;
376
+ font-variant-numeric: tabular-nums;
377
+ }
378
+ .donut-legend li {
379
+ display: grid;
380
+ grid-template-columns: minmax(0, 1fr) auto auto;
381
+ align-items: baseline;
382
+ gap: 8px 14px;
383
+ margin: 0;
384
+ padding-block: 10px;
385
+ }
386
+ .donut-key {
387
+ display: flex;
388
+ align-items: baseline;
389
+ gap: 9px;
390
+ overflow-wrap: anywhere;
391
+ }
392
+ .donut-swatch {
393
+ flex: 0 0 8px;
394
+ height: 8px;
395
+ border-radius: 50%;
396
+ }
397
+ .donut-swatch[data-slice="1"] {
398
+ background: var(--slice-1);
399
+ }
400
+ .donut-swatch[data-slice="2"] {
401
+ background: var(--slice-2);
402
+ }
403
+ .donut-swatch[data-slice="3"] {
404
+ background: var(--slice-3);
405
+ }
406
+ .donut-swatch[data-slice="4"] {
407
+ background: var(--slice-4);
408
+ }
409
+ .donut-swatch[data-slice="5"] {
410
+ background: var(--slice-5);
411
+ }
412
+ .donut-swatch[data-slice="6"] {
413
+ background: var(--slice-6);
414
+ }
415
+ .donut-swatch[data-slice="7"] {
416
+ background: var(--slice-7);
417
+ }
418
+ .donut-swatch[data-slice="8"] {
419
+ background: var(--slice-8);
420
+ }
421
+ .donut-value {
422
+ color: var(--ink);
423
+ }
424
+ .donut-share {
425
+ color: var(--muted);
426
+ }
254
427
  .table-scroll {
255
428
  max-width: 100%;
256
429
  overflow: auto;
package/dist/cli.js CHANGED
@@ -66,6 +66,14 @@ ${d.hint}` : ""}`).join(`
66
66
  import path2 from "node:path";
67
67
  import { open, realpath, stat } from "node:fs/promises";
68
68
 
69
+ // src/shared/payload.ts
70
+ var funnelStatuses = {
71
+ "on-target": "On target",
72
+ "below-target": "Below target",
73
+ "at-risk": "At risk",
74
+ neutral: "Neutral"
75
+ };
76
+
69
77
  // src/shared/limits.ts
70
78
  var limits = {
71
79
  markdownBytes: 1024 ** 2,
@@ -74,6 +82,8 @@ var limits = {
74
82
  datasets: 64,
75
83
  rows: 1e5,
76
84
  chartRows: 1e4,
85
+ donutSlices: 8,
86
+ funnelStages: 20,
77
87
  tableRows: 1000,
78
88
  tableColumns: 40
79
89
  };
@@ -207,13 +217,15 @@ class DataResolver {
207
217
  for (const block of blocks) {
208
218
  if (block.type !== "component")
209
219
  continue;
210
- if (block.name === "line" || block.name === "area" || block.name === "bar" || block.name === "table") {
220
+ if (block.name === "line" || block.name === "area" || block.name === "bar" || block.name === "donut" || block.name === "funnel" || block.name === "table") {
211
221
  const rows = await this.load(block.props.data, block);
212
222
  if (rows) {
213
223
  if (!rows.length)
214
224
  this.diagnostics.add("W_DATA_EMPTY", `${block.props.data} is empty; a no-data state will be shown.`, block.position, "Field validation requires at least one record.", "warning");
215
225
  if (block.name === "table")
216
226
  this.resolveTable(block, rows);
227
+ else if (block.name === "donut" || block.name === "funnel")
228
+ this.resolveParts(block, rows);
217
229
  else
218
230
  this.resolveChart(block, rows);
219
231
  }
@@ -265,6 +277,71 @@ class DataResolver {
265
277
  }
266
278
  block.rows = rows.map((row) => Object.fromEntries([...new Set([x, y])].map((key) => [key, row[key]])));
267
279
  }
280
+ resolveParts(block, rows) {
281
+ const props = block.props;
282
+ const maximum = block.name === "donut" ? limits.donutSlices : limits.funnelStages;
283
+ const fields = block.name === "donut" ? [block.props.category, props.value] : [
284
+ block.props.stage,
285
+ props.value,
286
+ block.props.total,
287
+ block.props.share,
288
+ ...block.props.status ? [block.props.status] : []
289
+ ];
290
+ const fail = (code, message, hint) => {
291
+ this.diagnostics.add(code, `${props.data}: ${message}`, block.position, hint);
292
+ };
293
+ if (rows.length > maximum) {
294
+ fail("E_DATA_LIMIT", `${block.name} supports at most ${maximum} rows.`, "Prepare a smaller summary; plotleaf does not group or truncate data.");
295
+ return;
296
+ }
297
+ if (fields.some((field) => !this.field(rows, field, block)))
298
+ return;
299
+ const seen = new Set;
300
+ let sum = 0;
301
+ for (const [index, row] of rows.entries()) {
302
+ const name = row[fields[0]];
303
+ const value = row[props.value];
304
+ if (typeof name !== "string" || !name.trim() || name.length > 1000) {
305
+ fail("E_DATA_TYPE", `Row ${index + 1}: ${JSON.stringify(fields[0])} must be a nonblank string of at most 1000 characters.`, "Supply a short, explicit label for every category or stage.");
306
+ return;
307
+ }
308
+ if (seen.has(name)) {
309
+ fail("E_DATA_ORDER", `Row ${index + 1}: category/stage labels must be unique.`, "Prepare distinct labels; source order is preserved.");
310
+ return;
311
+ }
312
+ seen.add(name);
313
+ if (typeof value !== "number" || value < 0) {
314
+ fail("E_DATA_TYPE", `Row ${index + 1}: ${JSON.stringify(props.value)} must be a nonnegative number.`, "Use finite JSON numbers, not nulls or numeric strings.");
315
+ return;
316
+ }
317
+ if (block.name === "donut") {
318
+ sum += value;
319
+ if (!Number.isFinite(sum)) {
320
+ fail("E_DATA_VALUE", "The donut total exceeds the finite numeric range.", "Prepare values in smaller units before rendering.");
321
+ return;
322
+ }
323
+ } else {
324
+ const total = row[block.props.total];
325
+ const share = row[block.props.share];
326
+ if (typeof total !== "number" || total <= 0 || value > total || typeof share !== "number" || share < 0 || share > 1) {
327
+ fail("E_DATA_VALUE", `Row ${index + 1}: total must be positive, value must not exceed total, and share must be a number from 0 to 1.`, "Supply each stage's denominator and prepared ratio explicitly; use an empty array when no baseline exists.");
328
+ return;
329
+ }
330
+ if (Math.abs(share - value / total) > 0.0005 + Number.EPSILON) {
331
+ fail("E_DATA_VALUE", `Row ${index + 1}: share does not match value / total.`, "Prepare a ratio consistent with the displayed values; rounding to 0.1 percentage point is supported.");
332
+ return;
333
+ }
334
+ if (block.props.status) {
335
+ const status = row[block.props.status];
336
+ if (typeof status !== "string" || !Object.hasOwn(funnelStatuses, status)) {
337
+ fail("E_DATA_VALUE", `Row ${index + 1}: status must be ${Object.keys(funnelStatuses).join(", ")}.`, "Supply business status explicitly, or omit the status property for neutral styling.");
338
+ return;
339
+ }
340
+ }
341
+ }
342
+ }
343
+ block.rows = rows.map((row) => Object.fromEntries(fields.map((field) => [field, row[field]])));
344
+ }
268
345
  resolveTable(block, rows) {
269
346
  if (rows.length > limits.tableRows) {
270
347
  this.diagnostics.add("E_DATA_LIMIT", `Table exceeds ${limits.tableRows} rows.`, block.position, "Provide a prepared summary table; rows are never silently truncated.");
@@ -330,7 +407,16 @@ var line = z.strictObject({
330
407
  var registry = {
331
408
  grid: {
332
409
  forms: ["container"],
333
- children: ["metric", "line", "area", "bar", "table", "callout"],
410
+ children: [
411
+ "metric",
412
+ "line",
413
+ "area",
414
+ "bar",
415
+ "donut",
416
+ "funnel",
417
+ "table",
418
+ "callout"
419
+ ],
334
420
  description: "A responsive row of one to four components.",
335
421
  schema: z.strictObject({
336
422
  columns: z.number().int().min(1).max(4).default(2)
@@ -374,6 +460,48 @@ var registry = {
374
460
  schema: z.strictObject(chart),
375
461
  example: '::bar{data="./errors.json" x="category" y="count" title="Errors by category"}'
376
462
  },
463
+ donut: {
464
+ forms: ["leaf"],
465
+ children: [],
466
+ description: "A part-to-whole donut with a readable legend and native data table.",
467
+ schema: z.strictObject({
468
+ data: text,
469
+ category: text,
470
+ value: text,
471
+ title: text,
472
+ format: format.default("number")
473
+ }),
474
+ dataRules: {
475
+ category: "Unique nonblank strings of at most 1000 characters; source order is preserved.",
476
+ value: "Finite nonnegative numbers with a finite sum. Nulls and numeric strings are invalid. All-zero values show an empty state.",
477
+ maximumRows: limits.donutSlices
478
+ },
479
+ example: '::donut{data="./plans.json" category="plan" value="teams" title="Teams by plan"}'
480
+ },
481
+ funnel: {
482
+ forms: ["leaf"],
483
+ children: [],
484
+ description: "Ordered stage/progress rows with prepared values, denominators, shares and optional explicit status. Works without JavaScript.",
485
+ schema: z.strictObject({
486
+ data: text,
487
+ stage: text,
488
+ value: text,
489
+ total: text,
490
+ share: text,
491
+ status: text.optional(),
492
+ title: text,
493
+ format: format.default("number")
494
+ }),
495
+ dataRules: {
496
+ stage: "Unique nonblank strings of at most 1000 characters; source order is preserved.",
497
+ value: "Finite nonnegative number no greater than its row's total.",
498
+ total: "Finite positive denominator supplied per row. No denominator is inferred from adjacent stages.",
499
+ share: "Prepared ratio from 0 to 1, matching value / total to within 0.0005 (rounding to 0.1 percentage point).",
500
+ status: Object.keys(funnelStatuses),
501
+ maximumRows: limits.funnelStages
502
+ },
503
+ example: '::funnel{data="./funnel.json" stage="stage" value="teams" total="baseline" share="share" status="status" title="Funnel by stage"}'
504
+ },
377
505
  table: {
378
506
  forms: ["leaf", "container"],
379
507
  children: ["column"],
@@ -432,7 +560,8 @@ function componentSpec() {
432
560
  forms: entry.forms,
433
561
  children: entry.children,
434
562
  props: z.toJSONSchema(entry.schema),
435
- example: entry.example
563
+ example: entry.example,
564
+ ..."dataRules" in entry ? { dataRules: entry.dataRules } : {}
436
565
  }
437
566
  ])),
438
567
  rules: [
@@ -444,6 +573,8 @@ function componentSpec() {
444
573
  "Dataset fields are literal own-property keys, not expressions.",
445
574
  "Charts preserve source order; date/number x values must increase.",
446
575
  "Line/area null y values create gaps; bar null y values are invalid.",
576
+ "Donut category/value fields must differ; category count is bounded, and all-zero values do not create equal slices.",
577
+ "Funnel fields must differ; shares and denominators are supplied, validated and displayed per row, with no sorting or status inference.",
447
578
  "Filters, maps, images, nested data and executable components are not supported in this release."
448
579
  ]
449
580
  };
@@ -467,6 +598,11 @@ function decodeProps(name, attributes) {
467
598
  };
468
599
  const props = result.data;
469
600
  const errors = [];
601
+ if (name === "donut" || name === "funnel") {
602
+ const fields = Object.entries(props).filter(([key, value]) => ["category", "stage", "value", "total", "share", "status"].includes(key) && value !== undefined).map(([, value]) => value);
603
+ if (new Set(fields).size !== fields.length)
604
+ errors.push(`${name} field properties must reference different dataset fields.`);
605
+ }
470
606
  if ("format" in props && props.format?.startsWith("currency:") && !Intl.supportedValuesOf("currency").includes(props.format.slice(9)))
471
607
  errors.push(`Unknown ISO currency code ${props.format.slice(9)}.`);
472
608
  if (name === "metric" && !props.comparison && (attributes.direction !== undefined || attributes.sentiment !== undefined))
@@ -732,7 +868,8 @@ var serializer = unified2().use(rehypeSanitize, {
732
868
  "figure",
733
869
  "figcaption",
734
870
  "button",
735
- "caption"
871
+ "caption",
872
+ "meter"
736
873
  ],
737
874
  attributes: {
738
875
  ...defaultSchema.attributes,
@@ -751,10 +888,16 @@ var serializer = unified2().use(rehypeSanitize, {
751
888
  "dataRaw",
752
889
  "dataColumns",
753
890
  "dataSentiment",
754
- "dataTone"
891
+ "dataTone",
892
+ "dataStatus",
893
+ "dataSlice"
755
894
  ],
756
895
  button: ["type", "disabled", "ariaLabel", "dataColumn"],
757
896
  section: ["className", "ariaLabel"],
897
+ meter: ["min", "max", "value", "className", "ariaLabel"],
898
+ ul: ["className", "role"],
899
+ ol: ["start", "className", "role"],
900
+ li: ["className", "dataStatus"],
758
901
  th: ["scope", "ariaSort", "className"],
759
902
  td: ["className", "dataRaw"]
760
903
  }
@@ -812,6 +955,15 @@ function table(rows, columns, title, id, sortable = true) {
812
955
  ])
813
956
  ]);
814
957
  }
958
+ function dataDisclosure(rows, columns, title, id) {
959
+ return h("details", { className: ["chart-data"] }, [
960
+ h("summary", {}, [
961
+ t("View data"),
962
+ h("span", { className: ["visually-hidden"] }, [t(` for ${title}`)])
963
+ ]),
964
+ table(rows, columns, title, `${id}-data`, false)
965
+ ]);
966
+ }
815
967
  function renderContent(report) {
816
968
  let sequence = 0;
817
969
  const payload = { charts: [] };
@@ -913,6 +1065,10 @@ function renderContent(report) {
913
1065
  ];
914
1066
  case "column":
915
1067
  return [];
1068
+ case "funnel":
1069
+ return renderFunnel(block, id);
1070
+ case "donut":
1071
+ return renderDonut(block, id);
916
1072
  case "line":
917
1073
  case "area":
918
1074
  case "bar":
@@ -920,6 +1076,156 @@ function renderContent(report) {
920
1076
  }
921
1077
  });
922
1078
  }
1079
+ function renderFunnel(block, id) {
1080
+ const { stage, value, total, share, status, title, format } = block.props;
1081
+ const rows = block.rows ?? [];
1082
+ const columns = [
1083
+ { field: stage, label: label(stage), sign: "auto", semantics: "neutral" },
1084
+ {
1085
+ field: value,
1086
+ label: label(value),
1087
+ format,
1088
+ sign: "auto",
1089
+ semantics: "neutral"
1090
+ },
1091
+ {
1092
+ field: total,
1093
+ label: label(total),
1094
+ format,
1095
+ sign: "auto",
1096
+ semantics: "neutral"
1097
+ },
1098
+ {
1099
+ field: share,
1100
+ label: "Share",
1101
+ format: "percent",
1102
+ sign: "auto",
1103
+ semantics: "neutral"
1104
+ },
1105
+ ...status ? [
1106
+ {
1107
+ field: status,
1108
+ label: "Status",
1109
+ sign: "auto",
1110
+ semantics: "neutral"
1111
+ }
1112
+ ] : []
1113
+ ];
1114
+ return [
1115
+ h("figure", { className: ["chart", "funnel"] }, [
1116
+ h("figcaption", { id: `${id}-title` }, [t(title)]),
1117
+ ...rows.length ? [
1118
+ h("ol", { className: ["funnel-stages"], role: "list" }, rows.map((row) => {
1119
+ const state = status ? row[status] : "neutral";
1120
+ const rate = formatValue(row[share], "percent");
1121
+ const amount = formatValue(row[value], format, "auto", true);
1122
+ const denominator = formatValue(row[total], format, "auto", true);
1123
+ return h("li", { className: ["funnel-stage"], dataStatus: state }, [
1124
+ h("div", { className: ["funnel-heading"] }, [
1125
+ h("span", { className: ["funnel-label"] }, [
1126
+ t(row[stage])
1127
+ ]),
1128
+ h("span", { className: ["funnel-values"] }, [
1129
+ h("strong", {}, [t(rate)]),
1130
+ h("span", {}, [t(`${amount} / ${denominator}`)])
1131
+ ])
1132
+ ]),
1133
+ h("meter", {
1134
+ min: "0",
1135
+ max: "1",
1136
+ value: String(row[share]),
1137
+ ariaLabel: `${row[stage]}: ${rate}, ${formatValue(row[value], format)} of ${formatValue(row[total], format)}${status ? `, ${funnelStatuses[state]}` : ""}`
1138
+ }, [t(rate)]),
1139
+ ...status ? [
1140
+ h("span", { className: ["funnel-status"] }, [
1141
+ t(funnelStatuses[state])
1142
+ ])
1143
+ ] : []
1144
+ ]);
1145
+ }))
1146
+ ] : [
1147
+ h("p", { className: ["empty-state"] }, [
1148
+ t("No data to display.")
1149
+ ])
1150
+ ],
1151
+ ...rows.length ? [
1152
+ dataDisclosure(rows.map((row) => status ? {
1153
+ ...row,
1154
+ [status]: funnelStatuses[row[status]]
1155
+ } : row), columns, title, id)
1156
+ ] : []
1157
+ ])
1158
+ ];
1159
+ }
1160
+ function renderDonut(block, id) {
1161
+ const { category, value, title, format } = block.props;
1162
+ const rows = block.rows ?? [];
1163
+ const total = rows.reduce((sum, row) => sum + row[value], 0);
1164
+ if (total > 0)
1165
+ payload.charts.push({
1166
+ id,
1167
+ kind: "donut",
1168
+ title,
1169
+ format,
1170
+ rows: rows.map((row) => [
1171
+ row[category],
1172
+ row[value]
1173
+ ])
1174
+ });
1175
+ return [
1176
+ h("figure", { className: ["chart", "donut"] }, [
1177
+ h("figcaption", { id: `${id}-title` }, [t(title)]),
1178
+ ...total > 0 ? [
1179
+ h("div", { className: ["donut-layout"] }, [
1180
+ h("div", {
1181
+ className: ["chart-plot", "donut-plot"],
1182
+ id,
1183
+ dataPlotleafChart: "",
1184
+ role: "img",
1185
+ ariaLabel: `${title}. Values and shares follow in the legend and data table.`
1186
+ }),
1187
+ h("ul", { className: ["donut-legend"], role: "list" }, rows.map((row, index) => h("li", {}, [
1188
+ h("span", { className: ["donut-key"] }, [
1189
+ h("span", {
1190
+ className: ["donut-swatch"],
1191
+ dataSlice: index + 1,
1192
+ ariaHidden: "true"
1193
+ }),
1194
+ h("span", {}, [t(row[category])])
1195
+ ]),
1196
+ h("span", { className: ["donut-value"] }, [
1197
+ t(formatValue(row[value], format))
1198
+ ]),
1199
+ h("span", { className: ["donut-share"] }, [
1200
+ t(formatValue(row[value] / total, "percent"))
1201
+ ])
1202
+ ])))
1203
+ ])
1204
+ ] : [
1205
+ h("p", { className: ["empty-state"] }, [
1206
+ t(rows.length ? "No non-zero values to display." : "No data to display.")
1207
+ ])
1208
+ ],
1209
+ ...rows.length ? [
1210
+ dataDisclosure(rows, [
1211
+ {
1212
+ field: category,
1213
+ label: label(category),
1214
+ sign: "auto",
1215
+ semantics: "neutral"
1216
+ },
1217
+ {
1218
+ field: value,
1219
+ label: label(value),
1220
+ format,
1221
+ sign: "auto",
1222
+ semantics: "neutral"
1223
+ }
1224
+ ], title, id)
1225
+ ] : []
1226
+ ])
1227
+ ];
1228
+ }
923
1229
  function renderChart(block, id) {
924
1230
  const { x, y, title, format } = block.props;
925
1231
  const xType = block.name === "bar" ? "category" : block.props.xType;
@@ -964,17 +1270,7 @@ function renderContent(report) {
964
1270
  t("No data to display.")
965
1271
  ])
966
1272
  ],
967
- ...rows.length ? [
968
- h("details", { className: ["chart-data"] }, [
969
- h("summary", {}, [
970
- t("View data"),
971
- h("span", { className: ["visually-hidden"] }, [
972
- t(` for ${title}`)
973
- ])
974
- ]),
975
- table(rows, columns, title, `${id}-data`, false)
976
- ])
977
- ] : []
1273
+ ...rows.length ? [dataDisclosure(rows, columns, title, id)] : []
978
1274
  ])
979
1275
  ];
980
1276
  }
@@ -1235,10 +1531,18 @@ data: ${revision}
1235
1531
  // package.json
1236
1532
  var package_default = {
1237
1533
  name: "plotleaf",
1238
- version: "0.1.0",
1534
+ version: "0.2.0",
1239
1535
  description: "Beautiful, interactive reports from Markdown and JSON. Built for agents.",
1240
1536
  type: "module",
1241
1537
  license: "MIT",
1538
+ repository: {
1539
+ type: "git",
1540
+ url: "git+https://github.com/LoueeD/plotleaf.git"
1541
+ },
1542
+ homepage: "https://github.com/LoueeD/plotleaf#readme",
1543
+ bugs: {
1544
+ url: "https://github.com/LoueeD/plotleaf/issues"
1545
+ },
1242
1546
  bin: {
1243
1547
  plotleaf: "./dist/cli.js"
1244
1548
  },
@@ -1257,6 +1561,8 @@ var package_default = {
1257
1561
  "examples/storefront/*.json",
1258
1562
  "examples/adoption/*.md",
1259
1563
  "examples/adoption/*.json",
1564
+ "examples/onboarding/*.md",
1565
+ "examples/onboarding/*.json",
1260
1566
  "skills/plotleaf/SKILL.md",
1261
1567
  "README.md",
1262
1568
  "LICENSE",
@@ -0,0 +1,30 @@
1
+ [
2
+ {
3
+ "stage": "Visited",
4
+ "teams": 41200,
5
+ "baseline": 41200,
6
+ "share": 1.0,
7
+ "status": "on-target"
8
+ },
9
+ {
10
+ "stage": "Signed up",
11
+ "teams": 12480,
12
+ "baseline": 41200,
13
+ "share": 0.3029126213592233,
14
+ "status": "on-target"
15
+ },
16
+ {
17
+ "stage": "Activated",
18
+ "teams": 7612,
19
+ "baseline": 41200,
20
+ "share": 0.18475728155339805,
21
+ "status": "below-target"
22
+ },
23
+ {
24
+ "stage": "Converted",
25
+ "teams": 1284,
26
+ "baseline": 41200,
27
+ "share": 0.03116504854368932,
28
+ "status": "at-risk"
29
+ }
30
+ ]
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "plan": "Pro",
4
+ "teams": 5100
5
+ },
6
+ {
7
+ "plan": "Team",
8
+ "teams": 1900
9
+ },
10
+ {
11
+ "plan": "Free",
12
+ "teams": 612
13
+ }
14
+ ]