@robosystems/report-components 0.2.10 → 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/dist/index.js CHANGED
@@ -1,9 +1,109 @@
1
- import { formatDate, BLOCK_TITLES, numericKindOf, humanizeDuration, formatValue, formatPeriod, isExternalFactUrl, BLOCK_ORDER, humanize, qname, currencySymbolFor } from './chunk-P3GQDCL2.js';
2
- export { BLOCK_ORDER, BLOCK_TITLES, NS, currencySymbolFor, cypherAdapter, fetchSecReportShell, fetchSecSection, formatDate, formatMoney, formatPeriod, formatValue, humanize, humanizeDuration, isExternalFactUrl, jsonldFileAdapter, mergeSecSections, numericKindOf, parseJsonld, parseStore, parseStructureDefinition, parseStructureName, qname, stripRoleSuffix } from './chunk-P3GQDCL2.js';
1
+ import { formatMoney, formatDate, BLOCK_TITLES, numericKindOf, humanizeDuration, formatValue, formatPeriod, isExternalFactUrl, BLOCK_ORDER, humanize, qname, currencySymbolFor } from './chunk-ZTYXTMAT.js';
2
+ export { BLOCK_ORDER, BLOCK_TITLES, NS, currencySymbolFor, cypherAdapter, fetchSecReportShell, fetchSecSection, formatDate, formatMoney, formatPeriod, formatValue, humanize, humanizeDuration, isExternalFactUrl, jsonldFileAdapter, mergeSecSections, numericKindOf, parseJsonld, parseStore, parseStructureDefinition, parseStructureName, qname, stripRoleSuffix } from './chunk-ZTYXTMAT.js';
3
3
  import { marked } from 'marked';
4
- import { useState, useEffect, useMemo, useRef } from 'react';
4
+ import { useState, useEffect, useMemo, useRef, useId } from 'react';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
 
7
+ // src/chart/format.ts
8
+ var DASH = "\u2014";
9
+ function formatMetricValue(itemType, value) {
10
+ if (value === null || value === void 0 || Number.isNaN(value)) return DASH;
11
+ switch (itemType) {
12
+ case "monetary":
13
+ return formatMoney(value);
14
+ case "percent":
15
+ return `${(value * 100).toLocaleString("en-US", { maximumFractionDigits: 2 })}%`;
16
+ case "multiple":
17
+ return `${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}\xD7`;
18
+ case "days":
19
+ return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
20
+ case "ratio":
21
+ return value.toLocaleString("en-US", {
22
+ minimumFractionDigits: 2,
23
+ maximumFractionDigits: 2
24
+ });
25
+ default:
26
+ return value.toLocaleString("en-US", { maximumFractionDigits: 4 });
27
+ }
28
+ }
29
+ function formatMetricCompact(itemType, value) {
30
+ if (value === null || value === void 0 || Number.isNaN(value)) return "";
31
+ if (itemType === "monetary") {
32
+ const abs = Math.abs(value);
33
+ const sign = value < 0 ? "-" : "";
34
+ if (abs >= 1e9) return `${sign}$${(abs / 1e9).toFixed(abs >= 1e10 ? 0 : 1)}B`;
35
+ if (abs >= 1e6) return `${sign}$${(abs / 1e6).toFixed(abs >= 1e7 ? 0 : 1)}M`;
36
+ if (abs >= 1e3) return `${sign}$${(abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1)}K`;
37
+ return `${sign}$${abs.toFixed(0)}`;
38
+ }
39
+ if (itemType === "percent") {
40
+ return `${(value * 100).toLocaleString("en-US", { maximumFractionDigits: 1 })}%`;
41
+ }
42
+ if (itemType === "multiple") {
43
+ return `${value.toLocaleString("en-US", { maximumFractionDigits: 1 })}\xD7`;
44
+ }
45
+ if (itemType === "days") {
46
+ return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
47
+ }
48
+ return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
49
+ }
50
+
51
+ // src/chart/join.ts
52
+ function joinChartSeries(panel, rendering) {
53
+ const rowByElement = new Map(rendering.rows.map((r) => [r.elementId, r]));
54
+ const periods = rendering.periods;
55
+ const out = [];
56
+ for (const series of panel.series) {
57
+ const row = rowByElement.get(series.elementId);
58
+ if (!row) continue;
59
+ const points = periods.map((p, i) => ({
60
+ x: p.end,
61
+ y: i < row.values.length ? row.values[i] : null
62
+ }));
63
+ out.push({
64
+ key: series.key,
65
+ label: series.label,
66
+ itemType: panel.itemType,
67
+ points
68
+ });
69
+ }
70
+ return out;
71
+ }
72
+
73
+ // src/chart/palette.ts
74
+ var SERIES_COLORS_LIGHT = [
75
+ "#2a78d6",
76
+ // 1 blue
77
+ "#eb6834",
78
+ // 2 orange
79
+ "#1baf7a",
80
+ // 3 aqua
81
+ "#eda100",
82
+ // 4 yellow
83
+ "#e87ba4",
84
+ // 5 magenta
85
+ "#008300",
86
+ // 6 green
87
+ "#4a3aa7",
88
+ // 7 violet
89
+ "#e34948"
90
+ // 8 red
91
+ ];
92
+ var SERIES_COLORS_DARK = [
93
+ "#3987e5",
94
+ "#d95926",
95
+ "#199e70",
96
+ "#c98500",
97
+ "#d55181",
98
+ "#008300",
99
+ "#9085e9",
100
+ "#e66767"
101
+ ];
102
+ function seriesColor(index) {
103
+ const slot = Math.min(index, SERIES_COLORS_LIGHT.length - 1);
104
+ return `var(--rs-chart-series-${slot + 1}, ${SERIES_COLORS_LIGHT[slot]})`;
105
+ }
106
+
7
107
  // src/project.ts
8
108
  function fallbackElement(id) {
9
109
  return {
@@ -1190,7 +1290,394 @@ function ReportView({ report, inspect = true, dimensionAxis = "rows" }) {
1190
1290
  )
1191
1291
  ] });
1192
1292
  }
1293
+ var MARGIN = { top: 16, right: 104, bottom: 30, left: 60 };
1294
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1295
+ function shortMonth(iso) {
1296
+ const m = /^(\d{4})-(\d{2})/.exec(iso);
1297
+ if (!m) return iso;
1298
+ return `${MONTHS[Number(m[2]) - 1] ?? m[2]} '${m[1].slice(2)}`;
1299
+ }
1300
+ function niceTicks(min, max, count = 4) {
1301
+ if (!(max > min)) return [min];
1302
+ const span = max - min;
1303
+ const step0 = span / count;
1304
+ const mag = Math.pow(10, Math.floor(Math.log10(step0)));
1305
+ const norm = step0 / mag;
1306
+ const step = (norm >= 5 ? 10 : norm >= 2 ? 5 : norm >= 1 ? 2 : 1) * mag;
1307
+ const start = Math.ceil(min / step) * step;
1308
+ const ticks = [];
1309
+ for (let v = start; v <= max + step * 1e-6; v += step) ticks.push(Number(v.toFixed(10)));
1310
+ return ticks;
1311
+ }
1312
+ var styles5 = {
1313
+ wrap: {
1314
+ fontFamily: "var(--rs-font-sans, ui-sans-serif, system-ui, sans-serif)",
1315
+ color: "var(--rs-text, #111827)"
1316
+ },
1317
+ title: {
1318
+ fontFamily: "var(--rs-font-heading, var(--rs-font-sans, sans-serif))",
1319
+ fontSize: "0.9rem",
1320
+ fontWeight: 600,
1321
+ margin: "0 0 0.5rem"
1322
+ },
1323
+ legend: {
1324
+ display: "flex",
1325
+ flexWrap: "wrap",
1326
+ gap: "0.25rem 1rem",
1327
+ marginBottom: "0.5rem",
1328
+ fontSize: "0.78rem",
1329
+ color: "var(--rs-muted, #6b7280)"
1330
+ },
1331
+ legendItem: { display: "inline-flex", alignItems: "center", gap: "0.4rem" },
1332
+ legendKey: { display: "inline-block", width: 16, height: 2, borderRadius: 1 },
1333
+ empty: {
1334
+ padding: "2rem",
1335
+ textAlign: "center",
1336
+ color: "var(--rs-muted, #6b7280)",
1337
+ fontSize: "0.85rem"
1338
+ }
1339
+ };
1340
+ function TimeSeriesChart({
1341
+ series,
1342
+ itemType,
1343
+ title,
1344
+ width = 720,
1345
+ height = 280
1346
+ }) {
1347
+ const svgRef = useRef(null);
1348
+ const [active, setActive] = useState(null);
1349
+ const clipId = useId();
1350
+ const family = itemType ?? series[0]?.itemType ?? null;
1351
+ const periodCount = series[0]?.points.length ?? 0;
1352
+ const model = useMemo(() => {
1353
+ const values = [];
1354
+ for (const s of series)
1355
+ for (const p of s.points) if (p.y !== null && !Number.isNaN(p.y)) values.push(p.y);
1356
+ if (!values.length) return null;
1357
+ let min2 = Math.min(...values);
1358
+ let max2 = Math.max(...values);
1359
+ if (min2 === max2) {
1360
+ const pad = Math.abs(min2) * 0.1 || 1;
1361
+ min2 -= pad;
1362
+ max2 += pad;
1363
+ } else {
1364
+ const pad = (max2 - min2) * 0.08;
1365
+ min2 -= pad;
1366
+ max2 += pad;
1367
+ }
1368
+ return { min: min2, max: max2 };
1369
+ }, [series]);
1370
+ if (!model || periodCount === 0) {
1371
+ return /* @__PURE__ */ jsxs("div", { style: styles5.wrap, children: [
1372
+ title && /* @__PURE__ */ jsx("p", { style: styles5.title, children: title }),
1373
+ /* @__PURE__ */ jsx("p", { style: styles5.empty, children: "No data to plot for this period range." })
1374
+ ] });
1375
+ }
1376
+ const innerW = width - MARGIN.left - MARGIN.right;
1377
+ const innerH = height - MARGIN.top - MARGIN.bottom;
1378
+ const { min, max } = model;
1379
+ const xAt = (i) => periodCount === 1 ? MARGIN.left + innerW / 2 : MARGIN.left + i / (periodCount - 1) * innerW;
1380
+ const yAt = (v) => MARGIN.top + innerH - (v - min) / (max - min) * innerH;
1381
+ const yTicks = niceTicks(min, max, 4);
1382
+ const xEvery = Math.max(1, Math.ceil(periodCount / 6));
1383
+ const periods = series[0].points;
1384
+ const showEndLabels = series.length <= 4;
1385
+ const placedLabelY = [];
1386
+ const showLegend = series.length >= 2;
1387
+ const ariaLabel = `${title ? `${title} \u2014 ` : ""}line chart of ${series.length} metric${series.length === 1 ? "" : "s"} over ${periodCount} periods`;
1388
+ function handleMove(e) {
1389
+ const svg = svgRef.current;
1390
+ const ctm = svg?.getScreenCTM?.();
1391
+ if (!svg || !ctm) return;
1392
+ const pt = new DOMPoint(e.clientX, e.clientY).matrixTransform(ctm.inverse());
1393
+ if (periodCount === 1) {
1394
+ setActive(0);
1395
+ return;
1396
+ }
1397
+ const frac = (pt.x - MARGIN.left) / innerW;
1398
+ const idx = Math.round(frac * (periodCount - 1));
1399
+ setActive(Math.max(0, Math.min(periodCount - 1, idx)));
1400
+ }
1401
+ function handleKey(e) {
1402
+ if (e.key === "ArrowRight") {
1403
+ e.preventDefault();
1404
+ setActive((a) => Math.min(periodCount - 1, (a ?? -1) + 1));
1405
+ } else if (e.key === "ArrowLeft") {
1406
+ e.preventDefault();
1407
+ setActive((a) => Math.max(0, (a ?? periodCount) - 1));
1408
+ } else if (e.key === "Escape") {
1409
+ setActive(null);
1410
+ }
1411
+ }
1412
+ return /* @__PURE__ */ jsxs("div", { style: styles5.wrap, children: [
1413
+ title && /* @__PURE__ */ jsx("p", { style: styles5.title, children: title }),
1414
+ showLegend && /* @__PURE__ */ jsx("div", { style: styles5.legend, children: series.map((s, i) => /* @__PURE__ */ jsxs("span", { style: styles5.legendItem, children: [
1415
+ /* @__PURE__ */ jsx("span", { style: { ...styles5.legendKey, background: seriesColor(i) } }),
1416
+ s.label
1417
+ ] }, s.key)) }),
1418
+ /* @__PURE__ */ jsxs(
1419
+ "svg",
1420
+ {
1421
+ ref: svgRef,
1422
+ viewBox: `0 0 ${width} ${height}`,
1423
+ width: "100%",
1424
+ role: "img",
1425
+ "aria-label": ariaLabel,
1426
+ tabIndex: 0,
1427
+ style: { display: "block", outline: "none", touchAction: "none" },
1428
+ onPointerMove: handleMove,
1429
+ onPointerLeave: () => setActive(null),
1430
+ onKeyDown: handleKey,
1431
+ children: [
1432
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: MARGIN.left, y: MARGIN.top, width: innerW, height: innerH }) }) }),
1433
+ yTicks.map((t) => /* @__PURE__ */ jsxs("g", { children: [
1434
+ /* @__PURE__ */ jsx(
1435
+ "line",
1436
+ {
1437
+ x1: MARGIN.left,
1438
+ x2: MARGIN.left + innerW,
1439
+ y1: yAt(t),
1440
+ y2: yAt(t),
1441
+ stroke: "var(--rs-border, #e5e7eb)",
1442
+ strokeWidth: 1
1443
+ }
1444
+ ),
1445
+ /* @__PURE__ */ jsx(
1446
+ "text",
1447
+ {
1448
+ x: MARGIN.left - 8,
1449
+ y: yAt(t),
1450
+ textAnchor: "end",
1451
+ dominantBaseline: "middle",
1452
+ fontSize: 11,
1453
+ fill: "var(--rs-muted, #6b7280)",
1454
+ style: { fontVariantNumeric: "tabular-nums" },
1455
+ children: formatMetricCompact(family, t)
1456
+ }
1457
+ )
1458
+ ] }, `y${t}`)),
1459
+ periods.map(
1460
+ (p, i) => i % xEvery === 0 || i === periodCount - 1 ? /* @__PURE__ */ jsx(
1461
+ "text",
1462
+ {
1463
+ x: xAt(i),
1464
+ y: height - MARGIN.bottom + 18,
1465
+ textAnchor: "middle",
1466
+ fontSize: 11,
1467
+ fill: "var(--rs-muted, #6b7280)",
1468
+ children: shortMonth(p.x)
1469
+ },
1470
+ `x${p.x}`
1471
+ ) : null
1472
+ ),
1473
+ /* @__PURE__ */ jsx(
1474
+ "line",
1475
+ {
1476
+ x1: MARGIN.left,
1477
+ x2: MARGIN.left + innerW,
1478
+ y1: MARGIN.top + innerH,
1479
+ y2: MARGIN.top + innerH,
1480
+ stroke: "var(--rs-border-strong, #9ca3af)",
1481
+ strokeWidth: 1
1482
+ }
1483
+ ),
1484
+ active !== null && /* @__PURE__ */ jsx(
1485
+ "line",
1486
+ {
1487
+ x1: xAt(active),
1488
+ x2: xAt(active),
1489
+ y1: MARGIN.top,
1490
+ y2: MARGIN.top + innerH,
1491
+ stroke: "var(--rs-border-strong, #9ca3af)",
1492
+ strokeWidth: 1
1493
+ }
1494
+ ),
1495
+ /* @__PURE__ */ jsx("g", { clipPath: `url(#${clipId})`, children: series.map((s, si) => {
1496
+ const color = seriesColor(si);
1497
+ const segs = [];
1498
+ let pen = false;
1499
+ s.points.forEach((pt, i) => {
1500
+ if (pt.y === null || Number.isNaN(pt.y)) {
1501
+ pen = false;
1502
+ return;
1503
+ }
1504
+ segs.push(`${pen ? "L" : "M"}${xAt(i)},${yAt(pt.y)}`);
1505
+ pen = true;
1506
+ });
1507
+ return /* @__PURE__ */ jsx(
1508
+ "path",
1509
+ {
1510
+ d: segs.join(" "),
1511
+ fill: "none",
1512
+ stroke: color,
1513
+ strokeWidth: 2,
1514
+ strokeLinejoin: "round",
1515
+ strokeLinecap: "round"
1516
+ },
1517
+ s.key
1518
+ );
1519
+ }) }),
1520
+ series.map((s, si) => {
1521
+ const color = seriesColor(si);
1522
+ let endIdx = -1;
1523
+ for (let i = s.points.length - 1; i >= 0; i--) {
1524
+ const y = s.points[i].y;
1525
+ if (y !== null && !Number.isNaN(y)) {
1526
+ endIdx = i;
1527
+ break;
1528
+ }
1529
+ }
1530
+ if (endIdx < 0) return null;
1531
+ const ey = yAt(s.points[endIdx].y);
1532
+ const ex = xAt(endIdx);
1533
+ let label = null;
1534
+ if (showEndLabels && endIdx === periodCount - 1) {
1535
+ const collides = placedLabelY.some((py) => Math.abs(py - ey) < 13);
1536
+ if (!collides) {
1537
+ placedLabelY.push(ey);
1538
+ label = formatMetricValue(family, s.points[endIdx].y);
1539
+ }
1540
+ }
1541
+ return /* @__PURE__ */ jsxs("g", { children: [
1542
+ /* @__PURE__ */ jsx(
1543
+ "circle",
1544
+ {
1545
+ cx: ex,
1546
+ cy: ey,
1547
+ r: 4,
1548
+ fill: color,
1549
+ stroke: "var(--rs-surface, #ffffff)",
1550
+ strokeWidth: 2
1551
+ }
1552
+ ),
1553
+ label && /* @__PURE__ */ jsx(
1554
+ "text",
1555
+ {
1556
+ x: ex + 8,
1557
+ y: ey,
1558
+ dominantBaseline: "middle",
1559
+ fontSize: 11,
1560
+ fill: "var(--rs-text, #111827)",
1561
+ style: { fontVariantNumeric: "tabular-nums" },
1562
+ children: label
1563
+ }
1564
+ )
1565
+ ] }, `end${s.key}`);
1566
+ }),
1567
+ active !== null && /* @__PURE__ */ jsx(
1568
+ ActiveReadout,
1569
+ {
1570
+ active,
1571
+ series,
1572
+ family,
1573
+ xAt,
1574
+ yAt,
1575
+ width,
1576
+ periodLabel: periods[active] ? shortMonth(periods[active].x) : "",
1577
+ plotTop: MARGIN.top
1578
+ }
1579
+ )
1580
+ ]
1581
+ }
1582
+ )
1583
+ ] });
1584
+ }
1585
+ function ActiveReadout({
1586
+ active,
1587
+ series,
1588
+ family,
1589
+ xAt,
1590
+ yAt,
1591
+ width,
1592
+ periodLabel,
1593
+ plotTop
1594
+ }) {
1595
+ const rows = series.map((s, i) => ({
1596
+ label: s.label,
1597
+ color: seriesColor(i),
1598
+ value: s.points[active]?.y ?? null,
1599
+ y: s.points[active]?.y
1600
+ }));
1601
+ const rowH = 16;
1602
+ const boxW = 176;
1603
+ const boxH = 22 + rows.length * rowH;
1604
+ const cx = xAt(active);
1605
+ const bx = cx + 12 + boxW > width ? cx - 12 - boxW : cx + 12;
1606
+ const by = plotTop + 4;
1607
+ return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
1608
+ rows.map(
1609
+ (r) => r.y !== null && r.y !== void 0 && !Number.isNaN(r.y) ? /* @__PURE__ */ jsx(
1610
+ "circle",
1611
+ {
1612
+ cx,
1613
+ cy: yAt(r.y),
1614
+ r: 4,
1615
+ fill: r.color,
1616
+ stroke: "var(--rs-surface, #ffffff)",
1617
+ strokeWidth: 2
1618
+ },
1619
+ `ap${r.label}`
1620
+ ) : null
1621
+ ),
1622
+ /* @__PURE__ */ jsx(
1623
+ "rect",
1624
+ {
1625
+ x: bx,
1626
+ y: by,
1627
+ width: boxW,
1628
+ height: boxH,
1629
+ rx: 6,
1630
+ fill: "var(--rs-surface, #ffffff)",
1631
+ stroke: "var(--rs-border, #e5e7eb)",
1632
+ strokeWidth: 1
1633
+ }
1634
+ ),
1635
+ /* @__PURE__ */ jsx("text", { x: bx + 10, y: by + 15, fontSize: 11, fontWeight: 600, fill: "var(--rs-muted, #6b7280)", children: periodLabel }),
1636
+ rows.map((r, i) => {
1637
+ const ry = by + 22 + i * rowH + 11;
1638
+ return /* @__PURE__ */ jsxs("g", { children: [
1639
+ /* @__PURE__ */ jsx(
1640
+ "line",
1641
+ {
1642
+ x1: bx + 10,
1643
+ x2: bx + 24,
1644
+ y1: ry,
1645
+ y2: ry,
1646
+ stroke: r.color,
1647
+ strokeWidth: 2,
1648
+ strokeLinecap: "round"
1649
+ }
1650
+ ),
1651
+ /* @__PURE__ */ jsx(
1652
+ "text",
1653
+ {
1654
+ x: bx + 30,
1655
+ y: ry,
1656
+ dominantBaseline: "middle",
1657
+ fontSize: 11,
1658
+ fill: "var(--rs-muted, #6b7280)",
1659
+ children: r.label
1660
+ }
1661
+ ),
1662
+ /* @__PURE__ */ jsx(
1663
+ "text",
1664
+ {
1665
+ x: bx + boxW - 10,
1666
+ y: ry,
1667
+ dominantBaseline: "middle",
1668
+ textAnchor: "end",
1669
+ fontSize: 11,
1670
+ fontWeight: 600,
1671
+ fill: "var(--rs-text, #111827)",
1672
+ style: { fontVariantNumeric: "tabular-nums" },
1673
+ children: formatMetricValue(family, r.value)
1674
+ }
1675
+ )
1676
+ ] }, `tr${r.label}`);
1677
+ })
1678
+ ] });
1679
+ }
1193
1680
 
1194
- export { ExternalTextBlock, FactInspector, ReportView, StatementTable, buildPivot, buildPivots, calcSubtotals, defaultPivotConfig, footCheck, pivotDimensionsOn, presentationOrder, reportSections, sliceReportSection };
1681
+ export { ExternalTextBlock, FactInspector, ReportView, SERIES_COLORS_DARK, SERIES_COLORS_LIGHT, StatementTable, TimeSeriesChart, buildPivot, buildPivots, calcSubtotals, defaultPivotConfig, footCheck, formatMetricCompact, formatMetricValue, joinChartSeries, pivotDimensionsOn, presentationOrder, reportSections, seriesColor, sliceReportSection };
1195
1682
  //# sourceMappingURL=index.js.map
1196
1683
  //# sourceMappingURL=index.js.map