@epilot/spark-ui 0.20.0 → 0.20.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.
Files changed (104) hide show
  1. package/dist/components/accordion.cjs +2 -2
  2. package/dist/components/accordion.d.ts +1 -1
  3. package/dist/components/accordion.js +1 -1
  4. package/dist/components/alert-dialog.cjs +1 -1
  5. package/dist/components/alert-dialog.js +1 -1
  6. package/dist/components/autocomplete.cjs +1 -1
  7. package/dist/components/autocomplete.js +1 -1
  8. package/dist/components/bar-chart-with-total.d.cts +1 -1
  9. package/dist/components/bar-chart-with-total.d.ts +1 -1
  10. package/dist/components/bar-chart.cjs +1 -1
  11. package/dist/components/bar-chart.js +2 -2
  12. package/dist/components/calendar.cjs +1 -1
  13. package/dist/components/calendar.js +1 -1
  14. package/dist/components/callout.cjs +1 -1
  15. package/dist/components/callout.d.cts +1 -1
  16. package/dist/components/callout.d.ts +2 -2
  17. package/dist/components/callout.js +1 -1
  18. package/dist/components/dialog.cjs +1 -1
  19. package/dist/components/dialog.js +1 -1
  20. package/dist/components/dropdown-menu.cjs +1 -1
  21. package/dist/components/dropdown-menu.js +1 -1
  22. package/dist/components/empty-state.d.ts +1 -1
  23. package/dist/components/energy-breakdown.cjs +80 -0
  24. package/dist/components/energy-breakdown.cjs.map +1 -0
  25. package/dist/components/energy-breakdown.d.cts +24 -0
  26. package/dist/components/energy-breakdown.d.ts +24 -0
  27. package/dist/components/energy-breakdown.js +79 -0
  28. package/dist/components/energy-breakdown.js.map +1 -0
  29. package/dist/components/energy-history-chart.cjs +63 -0
  30. package/dist/components/energy-history-chart.cjs.map +1 -0
  31. package/dist/components/energy-history-chart.d.cts +64 -0
  32. package/dist/components/energy-history-chart.d.ts +64 -0
  33. package/dist/components/energy-history-chart.js +62 -0
  34. package/dist/components/energy-history-chart.js.map +1 -0
  35. package/dist/components/entity-card.d.ts +1 -1
  36. package/dist/components/input-date.cjs +1 -1
  37. package/dist/components/input-date.js +1 -1
  38. package/dist/components/input-group.d.cts +1 -1
  39. package/dist/components/input-group.d.ts +1 -1
  40. package/dist/components/line-chart.cjs +3 -3
  41. package/dist/components/line-chart.js +4 -4
  42. package/dist/components/message-scroller.cjs +2 -2
  43. package/dist/components/message-scroller.js +2 -2
  44. package/dist/components/number-flow.d.cts +2 -12
  45. package/dist/components/number-flow.d.ts +2 -12
  46. package/dist/components/page-header.cjs +1 -1
  47. package/dist/components/page-header.js +1 -1
  48. package/dist/components/pill.cjs +1 -1
  49. package/dist/components/pill.js +1 -1
  50. package/dist/components/power-history-chart.cjs +498 -0
  51. package/dist/components/power-history-chart.cjs.map +1 -0
  52. package/dist/components/power-history-chart.d.cts +71 -0
  53. package/dist/components/power-history-chart.d.ts +71 -0
  54. package/dist/components/power-history-chart.js +496 -0
  55. package/dist/components/power-history-chart.js.map +1 -0
  56. package/dist/components/power-network.cjs +559 -0
  57. package/dist/components/power-network.cjs.map +1 -0
  58. package/dist/components/power-network.d.cts +80 -0
  59. package/dist/components/power-network.d.ts +80 -0
  60. package/dist/components/power-network.js +553 -0
  61. package/dist/components/power-network.js.map +1 -0
  62. package/dist/components/sankey-chart.cjs +11 -1
  63. package/dist/components/sankey-chart.cjs.map +1 -1
  64. package/dist/components/sankey-chart.d.cts +3 -2
  65. package/dist/components/sankey-chart.d.ts +3 -2
  66. package/dist/components/sankey-chart.js +11 -2
  67. package/dist/components/sankey-chart.js.map +1 -1
  68. package/dist/components/schedule-list.cjs +1 -1
  69. package/dist/components/schedule-list.js +1 -1
  70. package/dist/components/select.cjs +1 -1
  71. package/dist/components/select.js +1 -1
  72. package/dist/components/sheet.cjs +1 -1
  73. package/dist/components/sheet.js +1 -1
  74. package/dist/components/solar-forecast-chart.cjs +108 -0
  75. package/dist/components/solar-forecast-chart.cjs.map +1 -0
  76. package/dist/components/solar-forecast-chart.d.cts +77 -0
  77. package/dist/components/solar-forecast-chart.d.ts +77 -0
  78. package/dist/components/solar-forecast-chart.js +103 -0
  79. package/dist/components/solar-forecast-chart.js.map +1 -0
  80. package/dist/components/stepper.cjs +1 -1
  81. package/dist/components/stepper.js +1 -1
  82. package/dist/components/table.cjs +1 -1
  83. package/dist/components/table.js +1 -1
  84. package/dist/components/tabs.cjs +1 -1
  85. package/dist/components/tabs.js +1 -1
  86. package/dist/components/toast.cjs +1 -1
  87. package/dist/components/toast.js +1 -1
  88. package/dist/components/tooltip.cjs +1 -1
  89. package/dist/components/tooltip.js +1 -1
  90. package/dist/number-flow-Bch81ixZ.d.cts +14 -0
  91. package/dist/number-flow-Bch81ixZ.d.ts +14 -0
  92. package/dist/utils/build-sankey-nodes.cjs +72 -0
  93. package/dist/utils/build-sankey-nodes.cjs.map +1 -0
  94. package/dist/utils/build-sankey-nodes.d.cts +27 -0
  95. package/dist/utils/build-sankey-nodes.d.ts +27 -0
  96. package/dist/utils/build-sankey-nodes.js +71 -0
  97. package/dist/utils/build-sankey-nodes.js.map +1 -0
  98. package/dist/utils/format.cjs +13 -0
  99. package/dist/utils/format.cjs.map +1 -1
  100. package/dist/utils/format.d.cts +7 -1
  101. package/dist/utils/format.d.ts +7 -1
  102. package/dist/utils/format.js +12 -1
  103. package/dist/utils/format.js.map +1 -1
  104. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"sankey-chart.cjs","names":["SectionTitle","Sankey","Group","NumberFlow"],"sources":["../../src/components/sankey-chart.tsx"],"sourcesContent":["import { Group } from '@visx/group'\nimport {\n Sankey,\n sankeyJustify,\n type SankeyLink,\n type SankeyNode,\n} from '@visx/sankey'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '../utils/cn'\nimport NumberFlow, { type Format } from './number-flow'\nimport { SectionTitle } from './typography'\n\nexport interface SankeyChartNode {\n id: string\n label: string\n /** Optional category — used to colour the node and any outgoing links. */\n category?: string\n /** Override the colour resolved from `category`. */\n color?: string\n}\n\nexport interface SankeyChartLink {\n source: string\n target: string\n value: number\n /** Override the colour of the link. Defaults to the source node colour. */\n color?: string\n}\n\nexport interface SankeyChartProps {\n nodes: SankeyChartNode[]\n links: SankeyChartLink[]\n /** Resolves a category to a colour. */\n categoryColors?: Record<string, string>\n /** Fallback colour for nodes/links without a category. */\n defaultColor?: string\n /** Suffix appended to each animated value label (e.g. \" W\"). */\n valueSuffix?: string\n /** NumberFlow `format` for value labels. */\n valueFormat?: Format\n height?: number\n nodeWidth?: number\n nodePadding?: number\n /** Reserved horizontal space on the right for labels. */\n labelPadding?: number\n /**\n * Node ids kept at their natural (computed) column instead of being pushed to\n * the last column. By default `sankeyJustify` sends every node with no\n * outgoing link to the right edge; use this to hold a terminal node (e.g. grid\n * feed-in) in a middle column alongside its siblings.\n */\n keepAtDepthIds?: readonly string[]\n title?: string\n className?: string\n}\n\ntype SankeyDatumNode = SankeyChartNode & {\n /** Computed by d3-sankey; present after layout. */\n}\n\ntype SankeyDatumLink = SankeyChartLink\n\nconst DEFAULT_HEIGHT = 260\nconst DEFAULT_NODE_WIDTH = 12\nconst DEFAULT_NODE_PADDING = 10\nconst DEFAULT_LABEL_PADDING = 96\nconst LEFT_INSET = 4\nconst TOP_INSET = 8\nconst LABEL_GAP = 8\nconst LABEL_STACK_HEIGHT = 26\nconst DEFAULT_COLOR = 'var(--neutral-a9)'\n\nfunction resolveNodeColor(\n node: SankeyChartNode,\n categoryColors: Record<string, string> | undefined,\n defaultColor: string,\n) {\n if (node.color) return node.color\n if (node.category && categoryColors?.[node.category]) {\n return categoryColors[node.category] as string\n }\n return defaultColor\n}\n\nfunction SankeyChart({\n nodes,\n links,\n categoryColors,\n defaultColor = DEFAULT_COLOR,\n valueSuffix,\n valueFormat,\n height = DEFAULT_HEIGHT,\n nodeWidth = DEFAULT_NODE_WIDTH,\n nodePadding = DEFAULT_NODE_PADDING,\n labelPadding = DEFAULT_LABEL_PADDING,\n keepAtDepthIds,\n title,\n className,\n}: SankeyChartProps): React.ReactElement {\n const containerRef = useRef<HTMLDivElement>(null)\n const [width, setWidth] = useState(0)\n\n useEffect(() => {\n const element = containerRef.current\n if (!element) return\n\n const resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0]\n if (!entry) return\n setWidth(entry.contentRect.width)\n })\n\n resizeObserver.observe(element)\n return () => resizeObserver.disconnect()\n }, [])\n\n const nodeIds = useMemo(() => new Set(nodes.map((node) => node.id)), [nodes])\n\n // Drop zero-value links and any whose endpoints aren't in the current node\n // set (a stale link referencing a removed node makes d3-sankey throw\n // `missing: <id>`).\n const visibleLinks = useMemo(\n () =>\n links.filter(\n (link) =>\n link.value > 0 &&\n nodeIds.has(link.source) &&\n nodeIds.has(link.target),\n ),\n [links, nodeIds],\n )\n\n const visibleNodes = useMemo(() => {\n const referencedIds = new Set<string>()\n for (const link of visibleLinks) {\n referencedIds.add(link.source)\n referencedIds.add(link.target)\n }\n return nodes.filter((node) => referencedIds.has(node.id))\n }, [nodes, visibleLinks])\n\n const colorById = useMemo(() => {\n const map = new Map<string, string>()\n for (const node of nodes) {\n map.set(node.id, resolveNodeColor(node, categoryColors, defaultColor))\n }\n return map\n }, [categoryColors, defaultColor, nodes])\n\n const orderById = useMemo(() => {\n const map = new Map<string, number>()\n visibleNodes.forEach((node, index) => map.set(node.id, index))\n return map\n }, [visibleNodes])\n\n const nodeSort = useMemo(\n () =>\n (\n a: SankeyNode<SankeyDatumNode, SankeyDatumLink>,\n b: SankeyNode<SankeyDatumNode, SankeyDatumLink>,\n ) =>\n (orderById.get(a.id) ?? 0) - (orderById.get(b.id) ?? 0),\n [orderById],\n )\n\n const graph = useMemo(\n () => ({\n nodes: visibleNodes.map((node) => ({ ...node })),\n links: visibleLinks.map((link) => ({ ...link })),\n }),\n [visibleNodes, visibleLinks],\n )\n\n const nodeAlign = useMemo(() => {\n const keep = new Set(keepAtDepthIds)\n return (node: SankeyNode<SankeyDatumNode, SankeyDatumLink>, n: number) =>\n keep.has(node.id) ? (node.depth ?? 0) : sankeyJustify(node, n)\n }, [keepAtDepthIds])\n\n const innerWidth = Math.max(0, width - labelPadding - LEFT_INSET)\n const innerHeight = Math.max(0, height - TOP_INSET * 2)\n const halfLabelStack = LABEL_STACK_HEIGHT / 2\n const labelWidth = Math.max(40, labelPadding - LABEL_GAP - 4)\n\n return (\n <div className={cn('flex w-full flex-col gap-2 py-2', className)}>\n {title && <SectionTitle>{title}</SectionTitle>}\n\n <div ref={containerRef} className=\"relative w-full\" style={{ height }}>\n {width > 0 && innerWidth > 0 && (\n <Sankey<SankeyDatumNode, SankeyDatumLink>\n root={graph}\n size={[innerWidth, innerHeight]}\n nodeWidth={nodeWidth}\n nodePadding={nodePadding}\n nodeAlign={nodeAlign}\n nodeId={(node) => node.id}\n nodeSort={nodeSort}\n >\n {({ graph: laidOut, createPath }) => (\n <>\n <svg\n aria-hidden=\"true\"\n className=\"block\"\n height={height}\n width={width}\n >\n <Group left={LEFT_INSET} top={TOP_INSET}>\n <Group className=\"visx-sankey-links\">\n {laidOut.links.map((link, index) => {\n const sourceId =\n typeof link.source === 'object'\n ? (\n link.source as SankeyNode<\n SankeyDatumNode,\n SankeyDatumLink\n >\n ).id\n : String(link.source)\n const stroke =\n link.color ?? colorById.get(sourceId) ?? defaultColor\n\n return (\n <path\n key={`sankey-link-${index}`}\n d={createPath(link) ?? ''}\n fill=\"none\"\n stroke={stroke}\n strokeOpacity={0.32}\n strokeWidth={Math.max(1, link.width ?? 0)}\n />\n )\n })}\n </Group>\n\n <Group className=\"visx-sankey-nodes\">\n {laidOut.nodes.map((node, index) => {\n if (\n node.x0 === undefined ||\n node.x1 === undefined ||\n node.y0 === undefined ||\n node.y1 === undefined\n ) {\n return null\n }\n\n const w = node.x1 - node.x0\n const h = node.y1 - node.y0\n const fill = colorById.get(node.id) ?? defaultColor\n\n return (\n <rect\n key={`sankey-node-${index}`}\n x={node.x0}\n y={node.y0}\n width={w}\n height={Math.max(1, h)}\n fill={fill}\n rx={2}\n />\n )\n })}\n </Group>\n </Group>\n </svg>\n\n <div className=\"pointer-events-none absolute inset-0 [-webkit-text-size-adjust:100%] [text-size-adjust:100%]\">\n {laidOut.nodes.map((node, index) => {\n if (\n node.x0 === undefined ||\n node.x1 === undefined ||\n node.y0 === undefined ||\n node.y1 === undefined\n ) {\n return null\n }\n\n const h = node.y1 - node.y0\n const nodeCenterY = TOP_INSET + node.y0 + h / 2\n const labelCenterY = Math.min(\n Math.max(nodeCenterY, halfLabelStack),\n height - halfLabelStack,\n )\n\n return (\n <div\n key={`sankey-label-${node.id ?? index}`}\n className=\"absolute flex -translate-y-1/2 flex-col items-start gap-1\"\n style={{\n left: LEFT_INSET + node.x1 + LABEL_GAP,\n top: labelCenterY,\n width: labelWidth,\n }}\n >\n <span className=\"text-[11px] leading-none font-semibold whitespace-nowrap text-gray\">\n {node.label}\n </span>\n <NumberFlow\n value={node.value ?? 0}\n {...(valueSuffix !== undefined && {\n suffix: valueSuffix,\n })}\n {...(valueFormat !== undefined && {\n format: valueFormat,\n })}\n className=\"block min-h-[10px] text-[10px] leading-none text-gray-light tabular-nums\"\n />\n </div>\n )\n })}\n </div>\n </>\n )}\n </Sankey>\n )}\n </div>\n </div>\n )\n}\n\nexport type { SankeyLink, SankeyNode }\nexport { SankeyChart }\n"],"mappings":";;;;;;;;;;AA8DA,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAC7B,MAAM,wBAAwB;AAC9B,MAAM,aAAa;AACnB,MAAM,YAAY;AAClB,MAAM,YAAY;AAClB,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;AAEtB,SAAS,iBACP,MACA,gBACA,cACA;CACA,IAAI,KAAK,OAAO,OAAO,KAAK;CAC5B,IAAI,KAAK,YAAY,iBAAiB,KAAK,WACzC,OAAO,eAAe,KAAK;CAE7B,OAAO;AACT;AAEA,SAAS,YAAY,EACnB,OACA,OACA,gBACA,eAAe,eACf,aACA,aACA,SAAS,gBACT,YAAY,oBACZ,cAAc,sBACd,eAAe,uBACf,gBACA,OACA,aACuC;CACvC,MAAM,gBAAA,GAAA,MAAA,QAAsC,IAAI;CAChD,MAAM,CAAC,OAAO,aAAA,GAAA,MAAA,UAAqB,CAAC;CAEpC,CAAA,GAAA,MAAA,iBAAgB;EACd,MAAM,UAAU,aAAa;EAC7B,IAAI,CAAC,SAAS;EAEd,MAAM,iBAAiB,IAAI,gBAAgB,YAAY;GACrD,MAAM,QAAQ,QAAQ;GACtB,IAAI,CAAC,OAAO;GACZ,SAAS,MAAM,YAAY,KAAK;EAClC,CAAC;EAED,eAAe,QAAQ,OAAO;EAC9B,aAAa,eAAe,WAAW;CACzC,GAAG,CAAC,CAAC;CAEL,MAAM,WAAA,GAAA,MAAA,eAAwB,IAAI,IAAI,MAAM,KAAK,SAAS,KAAK,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC;CAK5E,MAAM,gBAAA,GAAA,MAAA,eAEF,MAAM,QACH,SACC,KAAK,QAAQ,KACb,QAAQ,IAAI,KAAK,MAAM,KACvB,QAAQ,IAAI,KAAK,MAAM,CAC3B,GACF,CAAC,OAAO,OAAO,CACjB;CAEA,MAAM,gBAAA,GAAA,MAAA,eAA6B;EACjC,MAAM,gCAAgB,IAAI,IAAY;EACtC,KAAK,MAAM,QAAQ,cAAc;GAC/B,cAAc,IAAI,KAAK,MAAM;GAC7B,cAAc,IAAI,KAAK,MAAM;EAC/B;EACA,OAAO,MAAM,QAAQ,SAAS,cAAc,IAAI,KAAK,EAAE,CAAC;CAC1D,GAAG,CAAC,OAAO,YAAY,CAAC;CAExB,MAAM,aAAA,GAAA,MAAA,eAA0B;EAC9B,MAAM,sBAAM,IAAI,IAAoB;EACpC,KAAK,MAAM,QAAQ,OACjB,IAAI,IAAI,KAAK,IAAI,iBAAiB,MAAM,gBAAgB,YAAY,CAAC;EAEvE,OAAO;CACT,GAAG;EAAC;EAAgB;EAAc;CAAK,CAAC;CAExC,MAAM,aAAA,GAAA,MAAA,eAA0B;EAC9B,MAAM,sBAAM,IAAI,IAAoB;EACpC,aAAa,SAAS,MAAM,UAAU,IAAI,IAAI,KAAK,IAAI,KAAK,CAAC;EAC7D,OAAO;CACT,GAAG,CAAC,YAAY,CAAC;CAEjB,MAAM,YAAA,GAAA,MAAA,gBAGA,GACA,OAEC,UAAU,IAAI,EAAE,EAAE,KAAK,MAAM,UAAU,IAAI,EAAE,EAAE,KAAK,IACzD,CAAC,SAAS,CACZ;CAEA,MAAM,SAAA,GAAA,MAAA,gBACG;EACL,OAAO,aAAa,KAAK,UAAU,EAAE,GAAG,KAAK,EAAE;EAC/C,OAAO,aAAa,KAAK,UAAU,EAAE,GAAG,KAAK,EAAE;CACjD,IACA,CAAC,cAAc,YAAY,CAC7B;CAEA,MAAM,aAAA,GAAA,MAAA,eAA0B;EAC9B,MAAM,OAAO,IAAI,IAAI,cAAc;EACnC,QAAQ,MAAoD,MAC1D,KAAK,IAAI,KAAK,EAAE,IAAK,KAAK,SAAS,KAAA,GAAA,aAAA,eAAmB,MAAM,CAAC;CACjE,GAAG,CAAC,cAAc,CAAC;CAEnB,MAAM,aAAa,KAAK,IAAI,GAAG,QAAQ,eAAe,UAAU;CAChE,MAAM,cAAc,KAAK,IAAI,GAAG,SAAS,YAAY,CAAC;CACtD,MAAM,iBAAiB,qBAAqB;CAC5C,MAAM,aAAa,KAAK,IAAI,IAAI,eAAe,YAAY,CAAC;CAE5D,OACE,iBAAA,GAAA,kBAAA,MAAC,OAAD;EAAK,YAAA,GAAA,OAAA,IAAc,mCAAmC,SAAS;YAA/D,CACG,SAAS,iBAAA,GAAA,kBAAA,KAACA,8BAAAA,cAAD,EAAA,UAAe,MAAoB,CAAA,GAE7C,iBAAA,GAAA,kBAAA,KAAC,OAAD;GAAK,KAAK;GAAc,WAAU;GAAkB,OAAO,EAAE,OAAO;aACjE,QAAQ,KAAK,aAAa,KACzB,iBAAA,GAAA,kBAAA,KAACC,aAAAA,QAAD;IACE,MAAM;IACN,MAAM,CAAC,YAAY,WAAW;IACnB;IACE;IACF;IACX,SAAS,SAAS,KAAK;IACb;eAER,EAAE,OAAO,SAAS,iBAClB,iBAAA,GAAA,kBAAA,MAAA,kBAAA,UAAA,EAAA,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAC,OAAD;KACE,eAAY;KACZ,WAAU;KACF;KACD;eAEP,iBAAA,GAAA,kBAAA,MAACC,YAAAA,OAAD;MAAO,MAAM;MAAY,KAAK;gBAA9B,CACE,iBAAA,GAAA,kBAAA,KAACA,YAAAA,OAAD;OAAO,WAAU;iBACd,QAAQ,MAAM,KAAK,MAAM,UAAU;QAClC,MAAM,WACJ,OAAO,KAAK,WAAW,WAEjB,KAAK,OAIL,KACF,OAAO,KAAK,MAAM;QACxB,MAAM,SACJ,KAAK,SAAS,UAAU,IAAI,QAAQ,KAAK;QAE3C,OACE,iBAAA,GAAA,kBAAA,KAAC,QAAD;SAEE,GAAG,WAAW,IAAI,KAAK;SACvB,MAAK;SACG;SACR,eAAe;SACf,aAAa,KAAK,IAAI,GAAG,KAAK,SAAS,CAAC;QACzC,GANM,eAAe,OAMrB;OAEL,CAAC;MACI,CAAA,GAEP,iBAAA,GAAA,kBAAA,KAACA,YAAAA,OAAD;OAAO,WAAU;iBACd,QAAQ,MAAM,KAAK,MAAM,UAAU;QAClC,IACE,KAAK,OAAO,KAAA,KACZ,KAAK,OAAO,KAAA,KACZ,KAAK,OAAO,KAAA,KACZ,KAAK,OAAO,KAAA,GAEZ,OAAO;QAGT,MAAM,IAAI,KAAK,KAAK,KAAK;QACzB,MAAM,IAAI,KAAK,KAAK,KAAK;QACzB,MAAM,OAAO,UAAU,IAAI,KAAK,EAAE,KAAK;QAEvC,OACE,iBAAA,GAAA,kBAAA,KAAC,QAAD;SAEE,GAAG,KAAK;SACR,GAAG,KAAK;SACR,OAAO;SACP,QAAQ,KAAK,IAAI,GAAG,CAAC;SACf;SACN,IAAI;QACL,GAPM,eAAe,OAOrB;OAEL,CAAC;MACI,CAAA,CACF;;IACJ,CAAA,GAEL,iBAAA,GAAA,kBAAA,KAAC,OAAD;KAAK,WAAU;eACZ,QAAQ,MAAM,KAAK,MAAM,UAAU;MAClC,IACE,KAAK,OAAO,KAAA,KACZ,KAAK,OAAO,KAAA,KACZ,KAAK,OAAO,KAAA,KACZ,KAAK,OAAO,KAAA,GAEZ,OAAO;MAGT,MAAM,IAAI,KAAK,KAAK,KAAK;MACzB,MAAM,cAAc,YAAY,KAAK,KAAK,IAAI;MAC9C,MAAM,eAAe,KAAK,IACxB,KAAK,IAAI,aAAa,cAAc,GACpC,SAAS,cACX;MAEA,OACE,iBAAA,GAAA,kBAAA,MAAC,OAAD;OAEE,WAAU;OACV,OAAO;QACL,MAAM,aAAa,KAAK,KAAK;QAC7B,KAAK;QACL,OAAO;OACT;iBAPF,CASE,iBAAA,GAAA,kBAAA,KAAC,QAAD;QAAM,WAAU;kBACb,KAAK;OACF,CAAA,GACN,iBAAA,GAAA,kBAAA,KAACC,gCAAD;QACE,OAAO,KAAK,SAAS;QACrB,GAAK,gBAAgB,KAAA,KAAa,EAChC,QAAQ,YACV;QACA,GAAK,gBAAgB,KAAA,KAAa,EAChC,QAAQ,YACV;QACA,WAAU;OACX,CAAA,CACE;SArBE,gBAAgB,KAAK,MAAM,OAqB7B;KAET,CAAC;IACE,CAAA,CACL,EAAA,CAAA;GAEE,CAAA;EAEP,CAAA,CACF;;AAET"}
1
+ {"version":3,"file":"sankey-chart.cjs","names":["SectionTitle","Sankey","Group","NumberFlow"],"sources":["../../src/components/sankey-chart.tsx"],"sourcesContent":["import { Group } from '@visx/group'\nimport {\n Sankey,\n sankeyJustify,\n type SankeyLink,\n type SankeyNode,\n} from '@visx/sankey'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '../utils/cn'\nimport NumberFlow, { type Format } from './number-flow'\nimport { SectionTitle } from './typography'\n\nconst SANKEY_CATEGORY_COLORS: Record<string, string> = {\n solar: 'var(--orange-a9)',\n battery: 'var(--green-a9)',\n grid: 'var(--blue-9)',\n gridFeedIn: 'var(--blue-7)',\n home: 'var(--spark-9)',\n floor: 'var(--neutral-a9)',\n device: 'var(--neutral-a8)',\n}\n\nexport interface SankeyChartNode {\n id: string\n label: string\n /** Optional category — used to colour the node and any outgoing links. */\n category?: string\n /** Override the colour resolved from `category`. */\n color?: string\n}\n\nexport interface SankeyChartLink {\n source: string\n target: string\n value: number\n /** Override the colour of the link. Defaults to the source node colour. */\n color?: string\n}\n\nexport interface SankeyChartProps {\n nodes: SankeyChartNode[]\n links: SankeyChartLink[]\n /** Resolves a category to a colour. */\n categoryColors?: Record<string, string>\n /** Fallback colour for nodes/links without a category. */\n defaultColor?: string\n /** Suffix appended to each animated value label (e.g. \" W\"). */\n valueSuffix?: string\n /** NumberFlow `format` for value labels. */\n valueFormat?: Format\n height?: number\n nodeWidth?: number\n nodePadding?: number\n /** Reserved horizontal space on the right for labels. */\n labelPadding?: number\n /**\n * Node ids kept at their natural (computed) column instead of being pushed to\n * the last column. By default `sankeyJustify` sends every node with no\n * outgoing link to the right edge; use this to hold a terminal node (e.g. grid\n * feed-in) in a middle column alongside its siblings.\n */\n keepAtDepthIds?: readonly string[]\n title?: string\n className?: string\n}\n\ntype SankeyDatumNode = SankeyChartNode & {\n /** Computed by d3-sankey; present after layout. */\n}\n\ntype SankeyDatumLink = SankeyChartLink\n\nconst DEFAULT_HEIGHT = 260\nconst DEFAULT_NODE_WIDTH = 12\nconst DEFAULT_NODE_PADDING = 10\nconst DEFAULT_LABEL_PADDING = 96\nconst LEFT_INSET = 4\nconst TOP_INSET = 8\nconst LABEL_GAP = 8\nconst LABEL_STACK_HEIGHT = 26\nconst DEFAULT_COLOR = 'var(--neutral-a9)'\n\nfunction resolveNodeColor(\n node: SankeyChartNode,\n categoryColors: Record<string, string> | undefined,\n defaultColor: string,\n) {\n if (node.color) return node.color\n if (node.category && categoryColors?.[node.category]) {\n return categoryColors[node.category] as string\n }\n return defaultColor\n}\n\nfunction SankeyChart({\n nodes,\n links,\n categoryColors = SANKEY_CATEGORY_COLORS,\n defaultColor = DEFAULT_COLOR,\n valueSuffix,\n valueFormat,\n height = DEFAULT_HEIGHT,\n nodeWidth = DEFAULT_NODE_WIDTH,\n nodePadding = DEFAULT_NODE_PADDING,\n labelPadding = DEFAULT_LABEL_PADDING,\n keepAtDepthIds,\n title,\n className,\n}: SankeyChartProps): React.ReactElement {\n const containerRef = useRef<HTMLDivElement>(null)\n const [width, setWidth] = useState(0)\n\n useEffect(() => {\n const element = containerRef.current\n if (!element) return\n\n const resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0]\n if (!entry) return\n setWidth(entry.contentRect.width)\n })\n\n resizeObserver.observe(element)\n return () => resizeObserver.disconnect()\n }, [])\n\n const nodeIds = useMemo(() => new Set(nodes.map((node) => node.id)), [nodes])\n\n // Drop zero-value links and any whose endpoints aren't in the current node\n // set (a stale link referencing a removed node makes d3-sankey throw\n // `missing: <id>`).\n const visibleLinks = useMemo(\n () =>\n links.filter(\n (link) =>\n link.value > 0 &&\n nodeIds.has(link.source) &&\n nodeIds.has(link.target),\n ),\n [links, nodeIds],\n )\n\n const visibleNodes = useMemo(() => {\n const referencedIds = new Set<string>()\n for (const link of visibleLinks) {\n referencedIds.add(link.source)\n referencedIds.add(link.target)\n }\n return nodes.filter((node) => referencedIds.has(node.id))\n }, [nodes, visibleLinks])\n\n const colorById = useMemo(() => {\n const map = new Map<string, string>()\n for (const node of nodes) {\n map.set(node.id, resolveNodeColor(node, categoryColors, defaultColor))\n }\n return map\n }, [categoryColors, defaultColor, nodes])\n\n const orderById = useMemo(() => {\n const map = new Map<string, number>()\n visibleNodes.forEach((node, index) => map.set(node.id, index))\n return map\n }, [visibleNodes])\n\n const nodeSort = useMemo(\n () =>\n (\n a: SankeyNode<SankeyDatumNode, SankeyDatumLink>,\n b: SankeyNode<SankeyDatumNode, SankeyDatumLink>,\n ) =>\n (orderById.get(a.id) ?? 0) - (orderById.get(b.id) ?? 0),\n [orderById],\n )\n\n const graph = useMemo(\n () => ({\n nodes: visibleNodes.map((node) => ({ ...node })),\n links: visibleLinks.map((link) => ({ ...link })),\n }),\n [visibleNodes, visibleLinks],\n )\n\n const nodeAlign = useMemo(() => {\n const keep = new Set(keepAtDepthIds)\n return (node: SankeyNode<SankeyDatumNode, SankeyDatumLink>, n: number) =>\n keep.has(node.id) ? (node.depth ?? 0) : sankeyJustify(node, n)\n }, [keepAtDepthIds])\n\n const innerWidth = Math.max(0, width - labelPadding - LEFT_INSET)\n const innerHeight = Math.max(0, height - TOP_INSET * 2)\n const halfLabelStack = LABEL_STACK_HEIGHT / 2\n const labelWidth = Math.max(40, labelPadding - LABEL_GAP - 4)\n\n return (\n <div className={cn('flex w-full flex-col gap-2 py-2', className)}>\n {title && <SectionTitle>{title}</SectionTitle>}\n\n <div ref={containerRef} className=\"relative w-full\" style={{ height }}>\n {width > 0 && innerWidth > 0 && (\n <Sankey<SankeyDatumNode, SankeyDatumLink>\n root={graph}\n size={[innerWidth, innerHeight]}\n nodeWidth={nodeWidth}\n nodePadding={nodePadding}\n nodeAlign={nodeAlign}\n nodeId={(node) => node.id}\n nodeSort={nodeSort}\n >\n {({ graph: laidOut, createPath }) => (\n <>\n <svg\n aria-hidden=\"true\"\n className=\"block\"\n height={height}\n width={width}\n >\n <Group left={LEFT_INSET} top={TOP_INSET}>\n <Group className=\"visx-sankey-links\">\n {laidOut.links.map((link, index) => {\n const sourceId =\n typeof link.source === 'object'\n ? (\n link.source as SankeyNode<\n SankeyDatumNode,\n SankeyDatumLink\n >\n ).id\n : String(link.source)\n const stroke =\n link.color ?? colorById.get(sourceId) ?? defaultColor\n\n return (\n <path\n key={`sankey-link-${index}`}\n d={createPath(link) ?? ''}\n fill=\"none\"\n stroke={stroke}\n strokeOpacity={0.32}\n strokeWidth={Math.max(1, link.width ?? 0)}\n />\n )\n })}\n </Group>\n\n <Group className=\"visx-sankey-nodes\">\n {laidOut.nodes.map((node, index) => {\n if (\n node.x0 === undefined ||\n node.x1 === undefined ||\n node.y0 === undefined ||\n node.y1 === undefined\n ) {\n return null\n }\n\n const w = node.x1 - node.x0\n const h = node.y1 - node.y0\n const fill = colorById.get(node.id) ?? defaultColor\n\n return (\n <rect\n key={`sankey-node-${index}`}\n x={node.x0}\n y={node.y0}\n width={w}\n height={Math.max(1, h)}\n fill={fill}\n rx={2}\n />\n )\n })}\n </Group>\n </Group>\n </svg>\n\n <div className=\"pointer-events-none absolute inset-0 [-webkit-text-size-adjust:100%] [text-size-adjust:100%]\">\n {laidOut.nodes.map((node, index) => {\n if (\n node.x0 === undefined ||\n node.x1 === undefined ||\n node.y0 === undefined ||\n node.y1 === undefined\n ) {\n return null\n }\n\n const h = node.y1 - node.y0\n const nodeCenterY = TOP_INSET + node.y0 + h / 2\n const labelCenterY = Math.min(\n Math.max(nodeCenterY, halfLabelStack),\n height - halfLabelStack,\n )\n\n return (\n <div\n key={`sankey-label-${node.id ?? index}`}\n className=\"absolute flex -translate-y-1/2 flex-col items-start gap-1\"\n style={{\n left: LEFT_INSET + node.x1 + LABEL_GAP,\n top: labelCenterY,\n width: labelWidth,\n }}\n >\n <span className=\"text-[11px] leading-none font-semibold whitespace-nowrap text-gray\">\n {node.label}\n </span>\n <NumberFlow\n value={node.value ?? 0}\n {...(valueSuffix !== undefined && {\n suffix: valueSuffix,\n })}\n {...(valueFormat !== undefined && {\n format: valueFormat,\n })}\n className=\"block min-h-[10px] text-[10px] leading-none text-gray-light tabular-nums\"\n />\n </div>\n )\n })}\n </div>\n </>\n )}\n </Sankey>\n )}\n </div>\n </div>\n )\n}\n\nexport type { SankeyLink, SankeyNode }\nexport { SankeyChart, SANKEY_CATEGORY_COLORS }\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { Format } from "./number-flow.cjs";
3
+ import { t as Format } from "../number-flow-Bch81ixZ.cjs";
4
4
  import { SankeyLink, SankeyNode } from "@visx/sankey";
5
5
 
6
6
  //#region src/components/sankey-chart.d.ts
7
+ declare const SANKEY_CATEGORY_COLORS: Record<string, string>;
7
8
  interface SankeyChartNode {
8
9
  id: string;
9
10
  label: string;
@@ -61,5 +62,5 @@ declare function SankeyChart({
61
62
  className
62
63
  }: SankeyChartProps): React.ReactElement;
63
64
  //#endregion
64
- export { SankeyChart, SankeyChartLink, SankeyChartNode, SankeyChartProps, type SankeyLink, type SankeyNode };
65
+ export { SANKEY_CATEGORY_COLORS, SankeyChart, SankeyChartLink, SankeyChartNode, SankeyChartProps, type SankeyLink, type SankeyNode };
65
66
  //# sourceMappingURL=sankey-chart.d.cts.map
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { Format } from "./number-flow.js";
3
+ import { t as Format } from "../number-flow-Bch81ixZ.js";
4
4
  import { SankeyLink, SankeyNode } from "@visx/sankey";
5
5
 
6
6
  //#region src/components/sankey-chart.d.ts
7
+ declare const SANKEY_CATEGORY_COLORS: Record<string, string>;
7
8
  interface SankeyChartNode {
8
9
  id: string;
9
10
  label: string;
@@ -61,5 +62,5 @@ declare function SankeyChart({
61
62
  className
62
63
  }: SankeyChartProps): React.ReactElement;
63
64
  //#endregion
64
- export { SankeyChart, SankeyChartLink, SankeyChartNode, SankeyChartProps, type SankeyLink, type SankeyNode };
65
+ export { SANKEY_CATEGORY_COLORS, SankeyChart, SankeyChartLink, SankeyChartNode, SankeyChartProps, type SankeyLink, type SankeyNode };
65
66
  //# sourceMappingURL=sankey-chart.d.ts.map
@@ -7,6 +7,15 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { Group } from "@visx/group";
8
8
  import { Sankey, sankeyJustify } from "@visx/sankey";
9
9
  //#region src/components/sankey-chart.tsx
10
+ const SANKEY_CATEGORY_COLORS = {
11
+ solar: "var(--orange-a9)",
12
+ battery: "var(--green-a9)",
13
+ grid: "var(--blue-9)",
14
+ gridFeedIn: "var(--blue-7)",
15
+ home: "var(--spark-9)",
16
+ floor: "var(--neutral-a9)",
17
+ device: "var(--neutral-a8)"
18
+ };
10
19
  const DEFAULT_HEIGHT = 260;
11
20
  const DEFAULT_NODE_WIDTH = 12;
12
21
  const DEFAULT_NODE_PADDING = 10;
@@ -21,7 +30,7 @@ function resolveNodeColor(node, categoryColors, defaultColor) {
21
30
  if (node.category && categoryColors?.[node.category]) return categoryColors[node.category];
22
31
  return defaultColor;
23
32
  }
24
- function SankeyChart({ nodes, links, categoryColors, defaultColor = DEFAULT_COLOR, valueSuffix, valueFormat, height = DEFAULT_HEIGHT, nodeWidth = DEFAULT_NODE_WIDTH, nodePadding = DEFAULT_NODE_PADDING, labelPadding = DEFAULT_LABEL_PADDING, keepAtDepthIds, title, className }) {
33
+ function SankeyChart({ nodes, links, categoryColors = SANKEY_CATEGORY_COLORS, defaultColor = DEFAULT_COLOR, valueSuffix, valueFormat, height = DEFAULT_HEIGHT, nodeWidth = DEFAULT_NODE_WIDTH, nodePadding = DEFAULT_NODE_PADDING, labelPadding = DEFAULT_LABEL_PADDING, keepAtDepthIds, title, className }) {
25
34
  const containerRef = useRef(null);
26
35
  const [width, setWidth] = useState(0);
27
36
  useEffect(() => {
@@ -156,6 +165,6 @@ function SankeyChart({ nodes, links, categoryColors, defaultColor = DEFAULT_COLO
156
165
  });
157
166
  }
158
167
  //#endregion
159
- export { SankeyChart };
168
+ export { SANKEY_CATEGORY_COLORS, SankeyChart };
160
169
 
161
170
  //# sourceMappingURL=sankey-chart.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sankey-chart.js","names":[],"sources":["../../src/components/sankey-chart.tsx"],"sourcesContent":["import { Group } from '@visx/group'\nimport {\n Sankey,\n sankeyJustify,\n type SankeyLink,\n type SankeyNode,\n} from '@visx/sankey'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '../utils/cn'\nimport NumberFlow, { type Format } from './number-flow'\nimport { SectionTitle } from './typography'\n\nexport interface SankeyChartNode {\n id: string\n label: string\n /** Optional category — used to colour the node and any outgoing links. */\n category?: string\n /** Override the colour resolved from `category`. */\n color?: string\n}\n\nexport interface SankeyChartLink {\n source: string\n target: string\n value: number\n /** Override the colour of the link. Defaults to the source node colour. */\n color?: string\n}\n\nexport interface SankeyChartProps {\n nodes: SankeyChartNode[]\n links: SankeyChartLink[]\n /** Resolves a category to a colour. */\n categoryColors?: Record<string, string>\n /** Fallback colour for nodes/links without a category. */\n defaultColor?: string\n /** Suffix appended to each animated value label (e.g. \" W\"). */\n valueSuffix?: string\n /** NumberFlow `format` for value labels. */\n valueFormat?: Format\n height?: number\n nodeWidth?: number\n nodePadding?: number\n /** Reserved horizontal space on the right for labels. */\n labelPadding?: number\n /**\n * Node ids kept at their natural (computed) column instead of being pushed to\n * the last column. By default `sankeyJustify` sends every node with no\n * outgoing link to the right edge; use this to hold a terminal node (e.g. grid\n * feed-in) in a middle column alongside its siblings.\n */\n keepAtDepthIds?: readonly string[]\n title?: string\n className?: string\n}\n\ntype SankeyDatumNode = SankeyChartNode & {\n /** Computed by d3-sankey; present after layout. */\n}\n\ntype SankeyDatumLink = SankeyChartLink\n\nconst DEFAULT_HEIGHT = 260\nconst DEFAULT_NODE_WIDTH = 12\nconst DEFAULT_NODE_PADDING = 10\nconst DEFAULT_LABEL_PADDING = 96\nconst LEFT_INSET = 4\nconst TOP_INSET = 8\nconst LABEL_GAP = 8\nconst LABEL_STACK_HEIGHT = 26\nconst DEFAULT_COLOR = 'var(--neutral-a9)'\n\nfunction resolveNodeColor(\n node: SankeyChartNode,\n categoryColors: Record<string, string> | undefined,\n defaultColor: string,\n) {\n if (node.color) return node.color\n if (node.category && categoryColors?.[node.category]) {\n return categoryColors[node.category] as string\n }\n return defaultColor\n}\n\nfunction SankeyChart({\n nodes,\n links,\n categoryColors,\n defaultColor = DEFAULT_COLOR,\n valueSuffix,\n valueFormat,\n height = DEFAULT_HEIGHT,\n nodeWidth = DEFAULT_NODE_WIDTH,\n nodePadding = DEFAULT_NODE_PADDING,\n labelPadding = DEFAULT_LABEL_PADDING,\n keepAtDepthIds,\n title,\n className,\n}: SankeyChartProps): React.ReactElement {\n const containerRef = useRef<HTMLDivElement>(null)\n const [width, setWidth] = useState(0)\n\n useEffect(() => {\n const element = containerRef.current\n if (!element) return\n\n const resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0]\n if (!entry) return\n setWidth(entry.contentRect.width)\n })\n\n resizeObserver.observe(element)\n return () => resizeObserver.disconnect()\n }, [])\n\n const nodeIds = useMemo(() => new Set(nodes.map((node) => node.id)), [nodes])\n\n // Drop zero-value links and any whose endpoints aren't in the current node\n // set (a stale link referencing a removed node makes d3-sankey throw\n // `missing: <id>`).\n const visibleLinks = useMemo(\n () =>\n links.filter(\n (link) =>\n link.value > 0 &&\n nodeIds.has(link.source) &&\n nodeIds.has(link.target),\n ),\n [links, nodeIds],\n )\n\n const visibleNodes = useMemo(() => {\n const referencedIds = new Set<string>()\n for (const link of visibleLinks) {\n referencedIds.add(link.source)\n referencedIds.add(link.target)\n }\n return nodes.filter((node) => referencedIds.has(node.id))\n }, [nodes, visibleLinks])\n\n const colorById = useMemo(() => {\n const map = new Map<string, string>()\n for (const node of nodes) {\n map.set(node.id, resolveNodeColor(node, categoryColors, defaultColor))\n }\n return map\n }, [categoryColors, defaultColor, nodes])\n\n const orderById = useMemo(() => {\n const map = new Map<string, number>()\n visibleNodes.forEach((node, index) => map.set(node.id, index))\n return map\n }, [visibleNodes])\n\n const nodeSort = useMemo(\n () =>\n (\n a: SankeyNode<SankeyDatumNode, SankeyDatumLink>,\n b: SankeyNode<SankeyDatumNode, SankeyDatumLink>,\n ) =>\n (orderById.get(a.id) ?? 0) - (orderById.get(b.id) ?? 0),\n [orderById],\n )\n\n const graph = useMemo(\n () => ({\n nodes: visibleNodes.map((node) => ({ ...node })),\n links: visibleLinks.map((link) => ({ ...link })),\n }),\n [visibleNodes, visibleLinks],\n )\n\n const nodeAlign = useMemo(() => {\n const keep = new Set(keepAtDepthIds)\n return (node: SankeyNode<SankeyDatumNode, SankeyDatumLink>, n: number) =>\n keep.has(node.id) ? (node.depth ?? 0) : sankeyJustify(node, n)\n }, [keepAtDepthIds])\n\n const innerWidth = Math.max(0, width - labelPadding - LEFT_INSET)\n const innerHeight = Math.max(0, height - TOP_INSET * 2)\n const halfLabelStack = LABEL_STACK_HEIGHT / 2\n const labelWidth = Math.max(40, labelPadding - LABEL_GAP - 4)\n\n return (\n <div className={cn('flex w-full flex-col gap-2 py-2', className)}>\n {title && <SectionTitle>{title}</SectionTitle>}\n\n <div ref={containerRef} className=\"relative w-full\" style={{ height }}>\n {width > 0 && innerWidth > 0 && (\n <Sankey<SankeyDatumNode, SankeyDatumLink>\n root={graph}\n size={[innerWidth, innerHeight]}\n nodeWidth={nodeWidth}\n nodePadding={nodePadding}\n nodeAlign={nodeAlign}\n nodeId={(node) => node.id}\n nodeSort={nodeSort}\n >\n {({ graph: laidOut, createPath }) => (\n <>\n <svg\n aria-hidden=\"true\"\n className=\"block\"\n height={height}\n width={width}\n >\n <Group left={LEFT_INSET} top={TOP_INSET}>\n <Group className=\"visx-sankey-links\">\n {laidOut.links.map((link, index) => {\n const sourceId =\n typeof link.source === 'object'\n ? (\n link.source as SankeyNode<\n SankeyDatumNode,\n SankeyDatumLink\n >\n ).id\n : String(link.source)\n const stroke =\n link.color ?? colorById.get(sourceId) ?? defaultColor\n\n return (\n <path\n key={`sankey-link-${index}`}\n d={createPath(link) ?? ''}\n fill=\"none\"\n stroke={stroke}\n strokeOpacity={0.32}\n strokeWidth={Math.max(1, link.width ?? 0)}\n />\n )\n })}\n </Group>\n\n <Group className=\"visx-sankey-nodes\">\n {laidOut.nodes.map((node, index) => {\n if (\n node.x0 === undefined ||\n node.x1 === undefined ||\n node.y0 === undefined ||\n node.y1 === undefined\n ) {\n return null\n }\n\n const w = node.x1 - node.x0\n const h = node.y1 - node.y0\n const fill = colorById.get(node.id) ?? defaultColor\n\n return (\n <rect\n key={`sankey-node-${index}`}\n x={node.x0}\n y={node.y0}\n width={w}\n height={Math.max(1, h)}\n fill={fill}\n rx={2}\n />\n )\n })}\n </Group>\n </Group>\n </svg>\n\n <div className=\"pointer-events-none absolute inset-0 [-webkit-text-size-adjust:100%] [text-size-adjust:100%]\">\n {laidOut.nodes.map((node, index) => {\n if (\n node.x0 === undefined ||\n node.x1 === undefined ||\n node.y0 === undefined ||\n node.y1 === undefined\n ) {\n return null\n }\n\n const h = node.y1 - node.y0\n const nodeCenterY = TOP_INSET + node.y0 + h / 2\n const labelCenterY = Math.min(\n Math.max(nodeCenterY, halfLabelStack),\n height - halfLabelStack,\n )\n\n return (\n <div\n key={`sankey-label-${node.id ?? index}`}\n className=\"absolute flex -translate-y-1/2 flex-col items-start gap-1\"\n style={{\n left: LEFT_INSET + node.x1 + LABEL_GAP,\n top: labelCenterY,\n width: labelWidth,\n }}\n >\n <span className=\"text-[11px] leading-none font-semibold whitespace-nowrap text-gray\">\n {node.label}\n </span>\n <NumberFlow\n value={node.value ?? 0}\n {...(valueSuffix !== undefined && {\n suffix: valueSuffix,\n })}\n {...(valueFormat !== undefined && {\n format: valueFormat,\n })}\n className=\"block min-h-[10px] text-[10px] leading-none text-gray-light tabular-nums\"\n />\n </div>\n )\n })}\n </div>\n </>\n )}\n </Sankey>\n )}\n </div>\n </div>\n )\n}\n\nexport type { SankeyLink, SankeyNode }\nexport { SankeyChart }\n"],"mappings":"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"}
1
+ {"version":3,"file":"sankey-chart.js","names":[],"sources":["../../src/components/sankey-chart.tsx"],"sourcesContent":["import { Group } from '@visx/group'\nimport {\n Sankey,\n sankeyJustify,\n type SankeyLink,\n type SankeyNode,\n} from '@visx/sankey'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '../utils/cn'\nimport NumberFlow, { type Format } from './number-flow'\nimport { SectionTitle } from './typography'\n\nconst SANKEY_CATEGORY_COLORS: Record<string, string> = {\n solar: 'var(--orange-a9)',\n battery: 'var(--green-a9)',\n grid: 'var(--blue-9)',\n gridFeedIn: 'var(--blue-7)',\n home: 'var(--spark-9)',\n floor: 'var(--neutral-a9)',\n device: 'var(--neutral-a8)',\n}\n\nexport interface SankeyChartNode {\n id: string\n label: string\n /** Optional category — used to colour the node and any outgoing links. */\n category?: string\n /** Override the colour resolved from `category`. */\n color?: string\n}\n\nexport interface SankeyChartLink {\n source: string\n target: string\n value: number\n /** Override the colour of the link. Defaults to the source node colour. */\n color?: string\n}\n\nexport interface SankeyChartProps {\n nodes: SankeyChartNode[]\n links: SankeyChartLink[]\n /** Resolves a category to a colour. */\n categoryColors?: Record<string, string>\n /** Fallback colour for nodes/links without a category. */\n defaultColor?: string\n /** Suffix appended to each animated value label (e.g. \" W\"). */\n valueSuffix?: string\n /** NumberFlow `format` for value labels. */\n valueFormat?: Format\n height?: number\n nodeWidth?: number\n nodePadding?: number\n /** Reserved horizontal space on the right for labels. */\n labelPadding?: number\n /**\n * Node ids kept at their natural (computed) column instead of being pushed to\n * the last column. By default `sankeyJustify` sends every node with no\n * outgoing link to the right edge; use this to hold a terminal node (e.g. grid\n * feed-in) in a middle column alongside its siblings.\n */\n keepAtDepthIds?: readonly string[]\n title?: string\n className?: string\n}\n\ntype SankeyDatumNode = SankeyChartNode & {\n /** Computed by d3-sankey; present after layout. */\n}\n\ntype SankeyDatumLink = SankeyChartLink\n\nconst DEFAULT_HEIGHT = 260\nconst DEFAULT_NODE_WIDTH = 12\nconst DEFAULT_NODE_PADDING = 10\nconst DEFAULT_LABEL_PADDING = 96\nconst LEFT_INSET = 4\nconst TOP_INSET = 8\nconst LABEL_GAP = 8\nconst LABEL_STACK_HEIGHT = 26\nconst DEFAULT_COLOR = 'var(--neutral-a9)'\n\nfunction resolveNodeColor(\n node: SankeyChartNode,\n categoryColors: Record<string, string> | undefined,\n defaultColor: string,\n) {\n if (node.color) return node.color\n if (node.category && categoryColors?.[node.category]) {\n return categoryColors[node.category] as string\n }\n return defaultColor\n}\n\nfunction SankeyChart({\n nodes,\n links,\n categoryColors = SANKEY_CATEGORY_COLORS,\n defaultColor = DEFAULT_COLOR,\n valueSuffix,\n valueFormat,\n height = DEFAULT_HEIGHT,\n nodeWidth = DEFAULT_NODE_WIDTH,\n nodePadding = DEFAULT_NODE_PADDING,\n labelPadding = DEFAULT_LABEL_PADDING,\n keepAtDepthIds,\n title,\n className,\n}: SankeyChartProps): React.ReactElement {\n const containerRef = useRef<HTMLDivElement>(null)\n const [width, setWidth] = useState(0)\n\n useEffect(() => {\n const element = containerRef.current\n if (!element) return\n\n const resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0]\n if (!entry) return\n setWidth(entry.contentRect.width)\n })\n\n resizeObserver.observe(element)\n return () => resizeObserver.disconnect()\n }, [])\n\n const nodeIds = useMemo(() => new Set(nodes.map((node) => node.id)), [nodes])\n\n // Drop zero-value links and any whose endpoints aren't in the current node\n // set (a stale link referencing a removed node makes d3-sankey throw\n // `missing: <id>`).\n const visibleLinks = useMemo(\n () =>\n links.filter(\n (link) =>\n link.value > 0 &&\n nodeIds.has(link.source) &&\n nodeIds.has(link.target),\n ),\n [links, nodeIds],\n )\n\n const visibleNodes = useMemo(() => {\n const referencedIds = new Set<string>()\n for (const link of visibleLinks) {\n referencedIds.add(link.source)\n referencedIds.add(link.target)\n }\n return nodes.filter((node) => referencedIds.has(node.id))\n }, [nodes, visibleLinks])\n\n const colorById = useMemo(() => {\n const map = new Map<string, string>()\n for (const node of nodes) {\n map.set(node.id, resolveNodeColor(node, categoryColors, defaultColor))\n }\n return map\n }, [categoryColors, defaultColor, nodes])\n\n const orderById = useMemo(() => {\n const map = new Map<string, number>()\n visibleNodes.forEach((node, index) => map.set(node.id, index))\n return map\n }, [visibleNodes])\n\n const nodeSort = useMemo(\n () =>\n (\n a: SankeyNode<SankeyDatumNode, SankeyDatumLink>,\n b: SankeyNode<SankeyDatumNode, SankeyDatumLink>,\n ) =>\n (orderById.get(a.id) ?? 0) - (orderById.get(b.id) ?? 0),\n [orderById],\n )\n\n const graph = useMemo(\n () => ({\n nodes: visibleNodes.map((node) => ({ ...node })),\n links: visibleLinks.map((link) => ({ ...link })),\n }),\n [visibleNodes, visibleLinks],\n )\n\n const nodeAlign = useMemo(() => {\n const keep = new Set(keepAtDepthIds)\n return (node: SankeyNode<SankeyDatumNode, SankeyDatumLink>, n: number) =>\n keep.has(node.id) ? (node.depth ?? 0) : sankeyJustify(node, n)\n }, [keepAtDepthIds])\n\n const innerWidth = Math.max(0, width - labelPadding - LEFT_INSET)\n const innerHeight = Math.max(0, height - TOP_INSET * 2)\n const halfLabelStack = LABEL_STACK_HEIGHT / 2\n const labelWidth = Math.max(40, labelPadding - LABEL_GAP - 4)\n\n return (\n <div className={cn('flex w-full flex-col gap-2 py-2', className)}>\n {title && <SectionTitle>{title}</SectionTitle>}\n\n <div ref={containerRef} className=\"relative w-full\" style={{ height }}>\n {width > 0 && innerWidth > 0 && (\n <Sankey<SankeyDatumNode, SankeyDatumLink>\n root={graph}\n size={[innerWidth, innerHeight]}\n nodeWidth={nodeWidth}\n nodePadding={nodePadding}\n nodeAlign={nodeAlign}\n nodeId={(node) => node.id}\n nodeSort={nodeSort}\n >\n {({ graph: laidOut, createPath }) => (\n <>\n <svg\n aria-hidden=\"true\"\n className=\"block\"\n height={height}\n width={width}\n >\n <Group left={LEFT_INSET} top={TOP_INSET}>\n <Group className=\"visx-sankey-links\">\n {laidOut.links.map((link, index) => {\n const sourceId =\n typeof link.source === 'object'\n ? (\n link.source as SankeyNode<\n SankeyDatumNode,\n SankeyDatumLink\n >\n ).id\n : String(link.source)\n const stroke =\n link.color ?? colorById.get(sourceId) ?? defaultColor\n\n return (\n <path\n key={`sankey-link-${index}`}\n d={createPath(link) ?? ''}\n fill=\"none\"\n stroke={stroke}\n strokeOpacity={0.32}\n strokeWidth={Math.max(1, link.width ?? 0)}\n />\n )\n })}\n </Group>\n\n <Group className=\"visx-sankey-nodes\">\n {laidOut.nodes.map((node, index) => {\n if (\n node.x0 === undefined ||\n node.x1 === undefined ||\n node.y0 === undefined ||\n node.y1 === undefined\n ) {\n return null\n }\n\n const w = node.x1 - node.x0\n const h = node.y1 - node.y0\n const fill = colorById.get(node.id) ?? defaultColor\n\n return (\n <rect\n key={`sankey-node-${index}`}\n x={node.x0}\n y={node.y0}\n width={w}\n height={Math.max(1, h)}\n fill={fill}\n rx={2}\n />\n )\n })}\n </Group>\n </Group>\n </svg>\n\n <div className=\"pointer-events-none absolute inset-0 [-webkit-text-size-adjust:100%] [text-size-adjust:100%]\">\n {laidOut.nodes.map((node, index) => {\n if (\n node.x0 === undefined ||\n node.x1 === undefined ||\n node.y0 === undefined ||\n node.y1 === undefined\n ) {\n return null\n }\n\n const h = node.y1 - node.y0\n const nodeCenterY = TOP_INSET + node.y0 + h / 2\n const labelCenterY = Math.min(\n Math.max(nodeCenterY, halfLabelStack),\n height - halfLabelStack,\n )\n\n return (\n <div\n key={`sankey-label-${node.id ?? index}`}\n className=\"absolute flex -translate-y-1/2 flex-col items-start gap-1\"\n style={{\n left: LEFT_INSET + node.x1 + LABEL_GAP,\n top: labelCenterY,\n width: labelWidth,\n }}\n >\n <span className=\"text-[11px] leading-none font-semibold whitespace-nowrap text-gray\">\n {node.label}\n </span>\n <NumberFlow\n value={node.value ?? 0}\n {...(valueSuffix !== undefined && {\n suffix: valueSuffix,\n })}\n {...(valueFormat !== undefined && {\n format: valueFormat,\n })}\n className=\"block min-h-[10px] text-[10px] leading-none text-gray-light tabular-nums\"\n />\n </div>\n )\n })}\n </div>\n </>\n )}\n </Sankey>\n )}\n </div>\n </div>\n )\n}\n\nexport type { SankeyLink, SankeyNode }\nexport { SankeyChart, SANKEY_CATEGORY_COLORS }\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_chunk = require("../chunk-DY84GLCh.cjs");
4
- let _epilot_spark_icons = require("@epilot/spark-icons");
5
4
  let react = require("react");
6
5
  react = require_chunk.__toESM(react);
6
+ let _epilot_spark_icons = require("@epilot/spark-icons");
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let cnfast = require("cnfast");
9
9
  //#region src/components/schedule-list.tsx
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../utils/cn.js";
3
- import { Add, Delete } from "@epilot/spark-icons";
4
3
  import * as React$1 from "react";
4
+ import { Add, Delete } from "@epilot/spark-icons";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/components/schedule-list.tsx
7
7
  const ScheduleList = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
@@ -4,9 +4,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const require_chunk = require("../chunk-DY84GLCh.cjs");
5
5
  const require_components_field = require("./field.cjs");
6
6
  const require_lib_input_box_variants = require("../lib/input-box-variants.cjs");
7
- let _epilot_spark_icons = require("@epilot/spark-icons");
8
7
  let react = require("react");
9
8
  react = require_chunk.__toESM(react);
9
+ let _epilot_spark_icons = require("@epilot/spark-icons");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
11
  let _base_ui_react_select = require("@base-ui/react/select");
12
12
  let cnfast = require("cnfast");
@@ -3,8 +3,8 @@
3
3
  import { cn } from "../utils/cn.js";
4
4
  import { useFieldControl } from "./field.js";
5
5
  import { boxChrome } from "../lib/input-box-variants.js";
6
- import { Check, Close, KeyboardArrowDown } from "@epilot/spark-icons";
7
6
  import "react";
7
+ import { Check, Close, KeyboardArrowDown } from "@epilot/spark-icons";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import { Select as Select$1 } from "@base-ui/react/select";
10
10
  //#region src/components/select.tsx
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_chunk = require("../chunk-DY84GLCh.cjs");
4
4
  const require_components_button = require("./button.cjs");
5
- let _epilot_spark_icons = require("@epilot/spark-icons");
6
5
  let react = require("react");
7
6
  react = require_chunk.__toESM(react);
7
+ let _epilot_spark_icons = require("@epilot/spark-icons");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
9
  let _base_ui_react_dialog = require("@base-ui/react/dialog");
10
10
  let cnfast = require("cnfast");
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { cn } from "../utils/cn.js";
3
3
  import { buttonVariants } from "./button.js";
4
- import { Close } from "@epilot/spark-icons";
5
4
  import "react";
5
+ import { Close } from "@epilot/spark-icons";
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  import { Dialog } from "@base-ui/react/dialog";
8
8
  //#region src/components/sheet.tsx
@@ -0,0 +1,108 @@
1
+ "use client";
2
+ Object.defineProperties(exports, {
3
+ __esModule: { value: true },
4
+ [Symbol.toStringTag]: { value: "Module" }
5
+ });
6
+ require("../utils/chars.cjs");
7
+ const require_utils_format = require("../utils/format.cjs");
8
+ const require_components_line_chart = require("./line-chart.cjs");
9
+ let react = require("react");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+ let cnfast = require("cnfast");
12
+ //#region src/components/solar-forecast-chart.tsx
13
+ const DEFAULT_HEIGHT = 220;
14
+ const BASE_MARGIN = {
15
+ top: 8,
16
+ right: 8,
17
+ bottom: 28,
18
+ left: 34
19
+ };
20
+ /** Extra headroom for the weather icon row pinned above the plot. */
21
+ const WEATHER_ROW_HEIGHT = 24;
22
+ function defaultFormatValue(value) {
23
+ return `${require_utils_format.oneDecimalFormat.format(value)} kW`;
24
+ }
25
+ function defaultFormatTime(date) {
26
+ return date.getHours().toString().padStart(2, "0");
27
+ }
28
+ /**
29
+ * Responsive line plot of measured production (solid) against a forecast
30
+ * (dashed) over a day. Optionally overlays hourly weather icons and
31
+ * sunrise/sunset markers, all pinned to the same time axis. Sizes itself to its
32
+ * container; compose a title and any summary around it.
33
+ */
34
+ function SolarForecastChart({ produced, forecast, producedLabel = "Produced", forecastLabel = "Forecast", formatValue = defaultFormatValue, yAxisUnit = "kW", formatTime = defaultFormatTime, formatTooltipTime, weather, sunrise, sunset, formatMarkerTime, curve = "monotone", height = DEFAULT_HEIGHT, className, zoomable, zoomResetLabel, zoomDomain, onZoomDomainChange }) {
35
+ const containerRef = (0, react.useRef)(null);
36
+ const [width, setWidth] = (0, react.useState)(0);
37
+ (0, react.useEffect)(() => {
38
+ const el = containerRef.current;
39
+ if (!el) return;
40
+ const ro = new ResizeObserver((entries) => {
41
+ const entry = entries[0];
42
+ if (entry) setWidth(entry.contentRect.width);
43
+ });
44
+ ro.observe(el);
45
+ return () => ro.disconnect();
46
+ }, []);
47
+ const hasWeather = weather != null && weather.length > 0;
48
+ const margin = {
49
+ ...BASE_MARGIN,
50
+ top: hasWeather ? BASE_MARGIN.top + WEATHER_ROW_HEIGHT : BASE_MARGIN.top
51
+ };
52
+ const formatMarker = formatMarkerTime ?? formatTooltipTime ?? formatTime;
53
+ const topContent = hasWeather ? weather.map((point, i) => {
54
+ const WeatherGlyph = point.icon;
55
+ return {
56
+ key: `weather-${point.date.valueOf()}-${i}`,
57
+ date: point.date,
58
+ content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WeatherGlyph, { className: "text-gray-light [--spark-icon-size:18px]" })
59
+ };
60
+ }) : void 0;
61
+ const referenceLines = [sunrise && {
62
+ key: "sunrise",
63
+ date: sunrise,
64
+ label: formatMarker(sunrise)
65
+ }, sunset && {
66
+ key: "sunset",
67
+ date: sunset,
68
+ label: formatMarker(sunset)
69
+ }].filter((x) => Boolean(x));
70
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
71
+ ref: containerRef,
72
+ className: (0, cnfast.cn)("w-full", className),
73
+ children: width > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_line_chart.LineChart, {
74
+ width,
75
+ height,
76
+ margin,
77
+ curve,
78
+ series: [{
79
+ id: "produced",
80
+ data: produced,
81
+ label: producedLabel
82
+ }, {
83
+ id: "forecast",
84
+ data: forecast,
85
+ label: forecastLabel,
86
+ dashed: true
87
+ }],
88
+ showGrid: true,
89
+ xTickCount: 6,
90
+ yTickCount: 4,
91
+ yAxisUnit,
92
+ formatDate: formatTime,
93
+ formatTooltipDate: formatTooltipTime ?? formatTime,
94
+ formatValue,
95
+ referenceLines: referenceLines.length > 0 ? referenceLines : void 0,
96
+ topContent,
97
+ zoomable,
98
+ zoomResetLabel,
99
+ zoomDomain,
100
+ onZoomDomainChange
101
+ })
102
+ });
103
+ }
104
+ //#endregion
105
+ exports.SolarForecastChart = SolarForecastChart;
106
+ exports.default = SolarForecastChart;
107
+
108
+ //# sourceMappingURL=solar-forecast-chart.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solar-forecast-chart.cjs","names":["oneDecimalFormat","LineChart"],"sources":["../../src/components/solar-forecast-chart.tsx"],"sourcesContent":["import type { SparkIcon } from '@epilot/spark-icons'\nimport { type JSX, type ReactNode, useEffect, useRef, useState } from 'react'\nimport { NARROW_NBSP } from '../utils/chars'\nimport { cn } from '../utils/cn'\nimport { oneDecimalFormat } from '../utils/format'\nimport {\n LineChart,\n type LineChartCurve,\n type LineChartDatum,\n} from './line-chart'\n\nconst DEFAULT_HEIGHT = 220\n// `left` leaves room for the y-axis tick labels + unit (matches the 34px gutter\n// used by BarChart / PowerHistoryChart).\nconst BASE_MARGIN = { top: 8, right: 8, bottom: 28, left: 34 }\n/** Extra headroom for the weather icon row pinned above the plot. */\nconst WEATHER_ROW_HEIGHT = 24\n\nfunction defaultFormatValue(value: number): string {\n return `${oneDecimalFormat.format(value)}${NARROW_NBSP}kW`\n}\n\nfunction defaultFormatTime(date: Date): string {\n return date.getHours().toString().padStart(2, '0')\n}\n\nexport interface SolarForecastWeatherPoint {\n date: Date\n icon: SparkIcon\n}\n\nexport interface SolarForecastChartProps {\n /** Measured production, drawn as the solid line. */\n produced: LineChartDatum[]\n /** Predicted production, drawn as the dashed line. */\n forecast: LineChartDatum[]\n /** Tooltip label for the produced series. */\n producedLabel?: ReactNode\n /** Tooltip label for the forecast series. */\n forecastLabel?: ReactNode\n /** Tooltip value formatter. Defaults to one-decimal kW (power). */\n formatValue?: (value: number) => string\n /** Unit shown once atop the y-axis; ticks render as plain numbers. Defaults to \"kW\". */\n yAxisUnit?: string\n /** X-axis tick formatter. Defaults to the zero-padded hour. */\n formatTime?: (date: Date) => string\n /** Tooltip time formatter. Defaults to `formatTime`. */\n formatTooltipTime?: (date: Date) => string\n /**\n * Hourly weather icons drawn along the top of the plot, aligned to the time\n * axis. Their dates must fall within the plotted day.\n */\n weather?: SolarForecastWeatherPoint[]\n /** Sunrise time, drawn as a labelled vertical marker on the plot. */\n sunrise?: Date\n /** Sunset time, drawn as a labelled vertical marker on the plot. */\n sunset?: Date\n /** Label formatter for the sunrise/sunset markers. Defaults to `formatTime`. */\n formatMarkerTime?: (date: Date) => string\n curve?: LineChartCurve\n height?: number\n className?: string\n zoomable?: boolean\n zoomResetLabel?: string\n zoomDomain?: [Date, Date] | null\n onZoomDomainChange?: (domain: [Date, Date] | null) => void\n}\n\n/**\n * Responsive line plot of measured production (solid) against a forecast\n * (dashed) over a day. Optionally overlays hourly weather icons and\n * sunrise/sunset markers, all pinned to the same time axis. Sizes itself to its\n * container; compose a title and any summary around it.\n */\nexport function SolarForecastChart({\n produced,\n forecast,\n producedLabel = 'Produced',\n forecastLabel = 'Forecast',\n formatValue = defaultFormatValue,\n yAxisUnit = 'kW',\n formatTime = defaultFormatTime,\n formatTooltipTime,\n weather,\n sunrise,\n sunset,\n formatMarkerTime,\n curve = 'monotone',\n height = DEFAULT_HEIGHT,\n className,\n zoomable,\n zoomResetLabel,\n zoomDomain,\n onZoomDomainChange,\n}: SolarForecastChartProps): JSX.Element {\n const containerRef = useRef<HTMLDivElement>(null)\n const [width, setWidth] = useState(0)\n\n useEffect(() => {\n const el = containerRef.current\n if (!el) return\n const ro = new ResizeObserver((entries) => {\n const entry = entries[0]\n if (entry) setWidth(entry.contentRect.width)\n })\n ro.observe(el)\n return () => ro.disconnect()\n }, [])\n\n const hasWeather = weather != null && weather.length > 0\n const margin = {\n ...BASE_MARGIN,\n top: hasWeather ? BASE_MARGIN.top + WEATHER_ROW_HEIGHT : BASE_MARGIN.top,\n }\n const formatMarker = formatMarkerTime ?? formatTooltipTime ?? formatTime\n\n const topContent = hasWeather\n ? weather.map((point, i) => {\n const WeatherGlyph = point.icon\n return {\n key: `weather-${point.date.valueOf()}-${i}`,\n date: point.date,\n content: (\n <WeatherGlyph className=\"text-gray-light [--spark-icon-size:18px]\" />\n ),\n }\n })\n : undefined\n\n const referenceLines = [\n sunrise && { key: 'sunrise', date: sunrise, label: formatMarker(sunrise) },\n sunset && { key: 'sunset', date: sunset, label: formatMarker(sunset) },\n ].filter((x): x is { key: string; date: Date; label: string } => Boolean(x))\n\n return (\n <div ref={containerRef} className={cn('w-full', className)}>\n {width > 0 && (\n <LineChart\n width={width}\n height={height}\n margin={margin}\n curve={curve}\n series={[\n { id: 'produced', data: produced, label: producedLabel },\n {\n id: 'forecast',\n data: forecast,\n label: forecastLabel,\n dashed: true,\n },\n ]}\n showGrid\n xTickCount={6}\n yTickCount={4}\n yAxisUnit={yAxisUnit}\n formatDate={formatTime}\n formatTooltipDate={formatTooltipTime ?? formatTime}\n formatValue={formatValue}\n referenceLines={\n referenceLines.length > 0 ? referenceLines : undefined\n }\n topContent={topContent}\n zoomable={zoomable}\n zoomResetLabel={zoomResetLabel}\n zoomDomain={zoomDomain}\n onZoomDomainChange={onZoomDomainChange}\n />\n )}\n </div>\n )\n}\n\nexport default SolarForecastChart\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,iBAAiB;AAGvB,MAAM,cAAc;CAAE,KAAK;CAAG,OAAO;CAAG,QAAQ;CAAI,MAAM;AAAG;;AAE7D,MAAM,qBAAqB;AAE3B,SAAS,mBAAmB,OAAuB;CACjD,OAAO,GAAGA,qBAAAA,iBAAiB,OAAO,KAAK;AACzC;AAEA,SAAS,kBAAkB,MAAoB;CAC7C,OAAO,KAAK,SAAS,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG;AACnD;;;;;;;AAkDA,SAAgB,mBAAmB,EACjC,UACA,UACA,gBAAgB,YAChB,gBAAgB,YAChB,cAAc,oBACd,YAAY,MACZ,aAAa,mBACb,mBACA,SACA,SACA,QACA,kBACA,QAAQ,YACR,SAAS,gBACT,WACA,UACA,gBACA,YACA,sBACuC;CACvC,MAAM,gBAAA,GAAA,MAAA,QAAsC,IAAI;CAChD,MAAM,CAAC,OAAO,aAAA,GAAA,MAAA,UAAqB,CAAC;CAEpC,CAAA,GAAA,MAAA,iBAAgB;EACd,MAAM,KAAK,aAAa;EACxB,IAAI,CAAC,IAAI;EACT,MAAM,KAAK,IAAI,gBAAgB,YAAY;GACzC,MAAM,QAAQ,QAAQ;GACtB,IAAI,OAAO,SAAS,MAAM,YAAY,KAAK;EAC7C,CAAC;EACD,GAAG,QAAQ,EAAE;EACb,aAAa,GAAG,WAAW;CAC7B,GAAG,CAAC,CAAC;CAEL,MAAM,aAAa,WAAW,QAAQ,QAAQ,SAAS;CACvD,MAAM,SAAS;EACb,GAAG;EACH,KAAK,aAAa,YAAY,MAAM,qBAAqB,YAAY;CACvE;CACA,MAAM,eAAe,oBAAoB,qBAAqB;CAE9D,MAAM,aAAa,aACf,QAAQ,KAAK,OAAO,MAAM;EACxB,MAAM,eAAe,MAAM;EAC3B,OAAO;GACL,KAAK,WAAW,MAAM,KAAK,QAAQ,EAAE,GAAG;GACxC,MAAM,MAAM;GACZ,SACE,iBAAA,GAAA,kBAAA,KAAC,cAAD,EAAc,WAAU,2CAA4C,CAAA;EAExE;CACF,CAAC,IACD,KAAA;CAEJ,MAAM,iBAAiB,CACrB,WAAW;EAAE,KAAK;EAAW,MAAM;EAAS,OAAO,aAAa,OAAO;CAAE,GACzE,UAAU;EAAE,KAAK;EAAU,MAAM;EAAQ,OAAO,aAAa,MAAM;CAAE,CACvE,EAAE,QAAQ,MAAuD,QAAQ,CAAC,CAAC;CAE3E,OACE,iBAAA,GAAA,kBAAA,KAAC,OAAD;EAAK,KAAK;EAAc,YAAA,GAAA,OAAA,IAAc,UAAU,SAAS;YACtD,QAAQ,KACP,iBAAA,GAAA,kBAAA,KAACC,8BAAAA,WAAD;GACS;GACC;GACA;GACD;GACP,QAAQ,CACN;IAAE,IAAI;IAAY,MAAM;IAAU,OAAO;GAAc,GACvD;IACE,IAAI;IACJ,MAAM;IACN,OAAO;IACP,QAAQ;GACV,CACF;GACA,UAAA;GACA,YAAY;GACZ,YAAY;GACD;GACX,YAAY;GACZ,mBAAmB,qBAAqB;GAC3B;GACb,gBACE,eAAe,SAAS,IAAI,iBAAiB,KAAA;GAEnC;GACF;GACM;GACJ;GACQ;EACrB,CAAA;CAEA,CAAA;AAET"}
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import { LineChartCurve, LineChartDatum } from "./line-chart.cjs";
4
+ import { JSX, ReactNode } from "react";
5
+ import { SparkIcon } from "@epilot/spark-icons";
6
+
7
+ //#region src/components/solar-forecast-chart.d.ts
8
+ interface SolarForecastWeatherPoint {
9
+ date: Date;
10
+ icon: SparkIcon;
11
+ }
12
+ interface SolarForecastChartProps {
13
+ /** Measured production, drawn as the solid line. */
14
+ produced: LineChartDatum[];
15
+ /** Predicted production, drawn as the dashed line. */
16
+ forecast: LineChartDatum[];
17
+ /** Tooltip label for the produced series. */
18
+ producedLabel?: ReactNode;
19
+ /** Tooltip label for the forecast series. */
20
+ forecastLabel?: ReactNode;
21
+ /** Tooltip value formatter. Defaults to one-decimal kW (power). */
22
+ formatValue?: (value: number) => string;
23
+ /** Unit shown once atop the y-axis; ticks render as plain numbers. Defaults to "kW". */
24
+ yAxisUnit?: string;
25
+ /** X-axis tick formatter. Defaults to the zero-padded hour. */
26
+ formatTime?: (date: Date) => string;
27
+ /** Tooltip time formatter. Defaults to `formatTime`. */
28
+ formatTooltipTime?: (date: Date) => string;
29
+ /**
30
+ * Hourly weather icons drawn along the top of the plot, aligned to the time
31
+ * axis. Their dates must fall within the plotted day.
32
+ */
33
+ weather?: SolarForecastWeatherPoint[];
34
+ /** Sunrise time, drawn as a labelled vertical marker on the plot. */
35
+ sunrise?: Date;
36
+ /** Sunset time, drawn as a labelled vertical marker on the plot. */
37
+ sunset?: Date;
38
+ /** Label formatter for the sunrise/sunset markers. Defaults to `formatTime`. */
39
+ formatMarkerTime?: (date: Date) => string;
40
+ curve?: LineChartCurve;
41
+ height?: number;
42
+ className?: string;
43
+ zoomable?: boolean;
44
+ zoomResetLabel?: string;
45
+ zoomDomain?: [Date, Date] | null;
46
+ onZoomDomainChange?: (domain: [Date, Date] | null) => void;
47
+ }
48
+ /**
49
+ * Responsive line plot of measured production (solid) against a forecast
50
+ * (dashed) over a day. Optionally overlays hourly weather icons and
51
+ * sunrise/sunset markers, all pinned to the same time axis. Sizes itself to its
52
+ * container; compose a title and any summary around it.
53
+ */
54
+ declare function SolarForecastChart({
55
+ produced,
56
+ forecast,
57
+ producedLabel,
58
+ forecastLabel,
59
+ formatValue,
60
+ yAxisUnit,
61
+ formatTime,
62
+ formatTooltipTime,
63
+ weather,
64
+ sunrise,
65
+ sunset,
66
+ formatMarkerTime,
67
+ curve,
68
+ height,
69
+ className,
70
+ zoomable,
71
+ zoomResetLabel,
72
+ zoomDomain,
73
+ onZoomDomainChange
74
+ }: SolarForecastChartProps): JSX.Element;
75
+ //#endregion
76
+ export { SolarForecastChart, SolarForecastChart as default, SolarForecastChartProps, SolarForecastWeatherPoint };
77
+ //# sourceMappingURL=solar-forecast-chart.d.cts.map
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import { LineChartCurve, LineChartDatum } from "./line-chart.js";
4
+ import { JSX, ReactNode } from "react";
5
+ import { SparkIcon } from "@epilot/spark-icons";
6
+
7
+ //#region src/components/solar-forecast-chart.d.ts
8
+ interface SolarForecastWeatherPoint {
9
+ date: Date;
10
+ icon: SparkIcon;
11
+ }
12
+ interface SolarForecastChartProps {
13
+ /** Measured production, drawn as the solid line. */
14
+ produced: LineChartDatum[];
15
+ /** Predicted production, drawn as the dashed line. */
16
+ forecast: LineChartDatum[];
17
+ /** Tooltip label for the produced series. */
18
+ producedLabel?: ReactNode;
19
+ /** Tooltip label for the forecast series. */
20
+ forecastLabel?: ReactNode;
21
+ /** Tooltip value formatter. Defaults to one-decimal kW (power). */
22
+ formatValue?: (value: number) => string;
23
+ /** Unit shown once atop the y-axis; ticks render as plain numbers. Defaults to "kW". */
24
+ yAxisUnit?: string;
25
+ /** X-axis tick formatter. Defaults to the zero-padded hour. */
26
+ formatTime?: (date: Date) => string;
27
+ /** Tooltip time formatter. Defaults to `formatTime`. */
28
+ formatTooltipTime?: (date: Date) => string;
29
+ /**
30
+ * Hourly weather icons drawn along the top of the plot, aligned to the time
31
+ * axis. Their dates must fall within the plotted day.
32
+ */
33
+ weather?: SolarForecastWeatherPoint[];
34
+ /** Sunrise time, drawn as a labelled vertical marker on the plot. */
35
+ sunrise?: Date;
36
+ /** Sunset time, drawn as a labelled vertical marker on the plot. */
37
+ sunset?: Date;
38
+ /** Label formatter for the sunrise/sunset markers. Defaults to `formatTime`. */
39
+ formatMarkerTime?: (date: Date) => string;
40
+ curve?: LineChartCurve;
41
+ height?: number;
42
+ className?: string;
43
+ zoomable?: boolean;
44
+ zoomResetLabel?: string;
45
+ zoomDomain?: [Date, Date] | null;
46
+ onZoomDomainChange?: (domain: [Date, Date] | null) => void;
47
+ }
48
+ /**
49
+ * Responsive line plot of measured production (solid) against a forecast
50
+ * (dashed) over a day. Optionally overlays hourly weather icons and
51
+ * sunrise/sunset markers, all pinned to the same time axis. Sizes itself to its
52
+ * container; compose a title and any summary around it.
53
+ */
54
+ declare function SolarForecastChart({
55
+ produced,
56
+ forecast,
57
+ producedLabel,
58
+ forecastLabel,
59
+ formatValue,
60
+ yAxisUnit,
61
+ formatTime,
62
+ formatTooltipTime,
63
+ weather,
64
+ sunrise,
65
+ sunset,
66
+ formatMarkerTime,
67
+ curve,
68
+ height,
69
+ className,
70
+ zoomable,
71
+ zoomResetLabel,
72
+ zoomDomain,
73
+ onZoomDomainChange
74
+ }: SolarForecastChartProps): JSX.Element;
75
+ //#endregion
76
+ export { SolarForecastChart, SolarForecastChart as default, SolarForecastChartProps, SolarForecastWeatherPoint };
77
+ //# sourceMappingURL=solar-forecast-chart.d.ts.map