@mstone6969/graph 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.
Files changed (238) hide show
  1. package/README.md +111 -14
  2. package/dist/ExcelWriter-3ZSYUH4T.cjs +154 -0
  3. package/dist/GraphType.js +24 -38
  4. package/dist/PdfWriter-6HJFYMCO.cjs +205 -0
  5. package/dist/catalog-YWX6FHRM.cjs +8 -0
  6. package/dist/catalog.js +5 -1
  7. package/dist/charts/bars/index.js +2 -12
  8. package/dist/charts/comparison/EffectForestPlot.js +1 -1
  9. package/dist/charts/comparison/index.js +1 -8
  10. package/dist/charts/demography/Nomogram.js +3 -3
  11. package/dist/charts/density/HexbinChart.js +6 -4
  12. package/dist/charts/diagrams/ArchitectureCanvas.js +68 -0
  13. package/dist/charts/diagrams/ArchitectureDiagram.js +331 -0
  14. package/dist/charts/diagrams/ArchitectureLayout.js +106 -0
  15. package/dist/charts/diagrams/ArchitectureLinkLabels.js +78 -0
  16. package/dist/charts/diagrams/ArchitectureModel.js +108 -0
  17. package/dist/charts/diagrams/ArchitectureRouter.js +348 -0
  18. package/dist/charts/diagrams/ArchitectureSamples.js +312 -0
  19. package/dist/charts/diagrams/ArchitectureShapes.js +130 -0
  20. package/dist/charts/diagrams/ArchitectureText.js +25 -0
  21. package/dist/charts/diagrams/DiagramText.js +38 -0
  22. package/dist/charts/diagrams/EntityRelationshipDiagram.js +465 -0
  23. package/dist/charts/diagrams/ErCrowsFoot.js +33 -0
  24. package/dist/charts/diagrams/ErSamples.js +205 -0
  25. package/dist/charts/diagrams/ErSchema.js +122 -0
  26. package/dist/charts/diagrams/LogicCircuit.js +154 -0
  27. package/dist/charts/diagrams/LogicCircuitLayout.js +600 -0
  28. package/dist/charts/diagrams/LogicCircuitSamples.js +66 -0
  29. package/dist/charts/diagrams/LogicGateDiagram.js +216 -0
  30. package/dist/charts/diagrams/LogicGateSymbol.js +113 -0
  31. package/dist/charts/diagrams/NetworkTopologyDiagram.js +247 -0
  32. package/dist/charts/diagrams/SequenceDiagram.js +351 -0
  33. package/dist/charts/diagrams/SequenceLayout.js +422 -0
  34. package/dist/charts/diagrams/SequenceModel.js +221 -0
  35. package/dist/charts/diagrams/TopologyLayout.js +143 -0
  36. package/dist/charts/diagrams/architectureEnums.js +50 -0
  37. package/dist/charts/diagrams/index.js +26 -0
  38. package/dist/charts/diagrams/logicEnums.js +42 -0
  39. package/dist/charts/diagrams/sequenceEnums.js +40 -0
  40. package/dist/charts/distribution/HistogramChart.js +25 -3
  41. package/dist/charts/distribution/index.js +1 -3
  42. package/dist/charts/earthscience/HovmollerDiagram.js +477 -0
  43. package/dist/charts/earthscience/HovmollerEnums.js +42 -0
  44. package/dist/charts/earthscience/HovmollerField.js +180 -0
  45. package/dist/charts/earthscience/HovmollerSample.js +40 -0
  46. package/dist/charts/earthscience/HovmollerSpeed.js +71 -0
  47. package/dist/charts/earthscience/index.js +10 -2
  48. package/dist/charts/engineering/ConstellationDiagram.js +26 -6
  49. package/dist/charts/engineering/EyeDiagram.js +2 -1
  50. package/dist/charts/engineering/PoleZeroPlot.js +32 -8
  51. package/dist/charts/financial/RelativeStrengthRotation.js +434 -0
  52. package/dist/charts/financial/RelativeStrengthRotationMath.js +71 -0
  53. package/dist/charts/financial/RelativeStrengthRotationSamples.js +27 -0
  54. package/dist/charts/financial/index.js +5 -3
  55. package/dist/charts/flow/index.js +1 -5
  56. package/dist/charts/frameworks/AnsoffMatrix.js +523 -0
  57. package/dist/charts/frameworks/FrameworkText.js +98 -0
  58. package/dist/charts/frameworks/NpsGauge.js +279 -0
  59. package/dist/charts/frameworks/NpsSurvey.js +58 -0
  60. package/dist/charts/frameworks/PerceptualMap.js +419 -0
  61. package/dist/charts/frameworks/PerceptualProjection.js +142 -0
  62. package/dist/charts/frameworks/PositioningSamples.js +191 -0
  63. package/dist/charts/frameworks/RaciMatrix.js +458 -0
  64. package/dist/charts/frameworks/RaciPlan.js +81 -0
  65. package/dist/charts/frameworks/RaciSamples.js +76 -0
  66. package/dist/charts/frameworks/SwotMatrix.js +358 -0
  67. package/dist/charts/frameworks/SwotSamples.js +85 -0
  68. package/dist/charts/frameworks/VanWestendorpChart.js +187 -0
  69. package/dist/charts/frameworks/VanWestendorpSurvey.js +197 -0
  70. package/dist/charts/frameworks/index.js +26 -0
  71. package/dist/charts/frameworks/raciEnums.js +30 -0
  72. package/dist/charts/frameworks/swotEnums.js +34 -0
  73. package/dist/charts/genomics/CircosPlot.js +1 -1
  74. package/dist/charts/geoflows/GeoFlowChart.js +2 -2
  75. package/dist/charts/geoflows/GeoFlowSample.js +2 -1
  76. package/dist/charts/geomaps/FloorPlanMap.js +324 -0
  77. package/dist/charts/geomaps/FloorPlanModel.js +179 -0
  78. package/dist/charts/geomaps/FloorPlanSamples.js +416 -0
  79. package/dist/charts/geomaps/GeoLand.js +2 -2
  80. package/dist/charts/geomaps/GeoSamples.js +2 -1
  81. package/dist/charts/geomaps/GraticuleMap.js +2 -2
  82. package/dist/charts/geomaps/RoomLabelFit.js +200 -0
  83. package/dist/charts/geomaps/TissotMap.js +2 -2
  84. package/dist/charts/geomaps/index.js +8 -4
  85. package/dist/charts/hierarchy/SunburstChart.js +17 -10
  86. package/dist/charts/hierarchy/index.js +2 -11
  87. package/dist/charts/indicators/BulletChart.js +8 -2
  88. package/dist/charts/indicators/SpeedometerChart.js +1 -1
  89. package/dist/charts/indicators/ThermometerChart.js +1 -1
  90. package/dist/charts/lifescience/LoessFit.js +113 -0
  91. package/dist/charts/lifescience/MaPlot.js +337 -0
  92. package/dist/charts/lifescience/MaPlotSamples.js +35 -0
  93. package/dist/charts/lifescience/OncoGlyph.js +137 -0
  94. package/dist/charts/lifescience/OncoPrint.js +373 -0
  95. package/dist/charts/lifescience/OncoPrintMatrix.js +107 -0
  96. package/dist/charts/lifescience/OncoPrintSamples.js +142 -0
  97. package/dist/charts/lifescience/PedigreeChart.js +306 -0
  98. package/dist/charts/lifescience/PedigreeLayout.js +261 -0
  99. package/dist/charts/lifescience/PedigreeRows.js +70 -0
  100. package/dist/charts/lifescience/PedigreeSamples.js +161 -0
  101. package/dist/charts/lifescience/PedigreeSymbol.js +47 -0
  102. package/dist/charts/lifescience/RamachandranPlot.js +364 -0
  103. package/dist/charts/lifescience/RamachandranRegions.js +2712 -0
  104. package/dist/charts/lifescience/RamachandranSamples.js +235 -0
  105. package/dist/charts/lifescience/index.js +25 -0
  106. package/dist/charts/lifescience/pedigreeEnums.js +21 -0
  107. package/dist/charts/lifescience/ramachandranEnums.js +34 -0
  108. package/dist/charts/linesareas/DualAxisChart.js +1 -1
  109. package/dist/charts/linesareas/LineEnums.js +1 -15
  110. package/dist/charts/linesareas/MultiLineChart.js +1 -1
  111. package/dist/charts/linesareas/TrendChart.js +15 -5
  112. package/dist/charts/linesareas/index.js +2 -6
  113. package/dist/charts/massive/MassiveChart.js +3 -2
  114. package/dist/charts/mathematical/FourierSeriesChart.js +7 -1
  115. package/dist/charts/mathematical/index.js +2 -6
  116. package/dist/charts/matrix/GridPlot.js +27 -2
  117. package/dist/charts/matrix/TilegramChart.js +2 -2
  118. package/dist/charts/mlcharts/AttentionGrid.js +79 -0
  119. package/dist/charts/mlcharts/AttentionHeatmap.js +329 -0
  120. package/dist/charts/mlcharts/AttentionModel.js +132 -0
  121. package/dist/charts/mlcharts/AttentionSamples.js +82 -0
  122. package/dist/charts/mlcharts/CellNumber.js +8 -0
  123. package/dist/charts/mlcharts/CurveDiagnosticSamples.js +95 -0
  124. package/dist/charts/mlcharts/CurvePickMark.js +31 -0
  125. package/dist/charts/mlcharts/DecisionBoundaryChart.js +316 -0
  126. package/dist/charts/mlcharts/DecisionSamples.js +60 -0
  127. package/dist/charts/mlcharts/DecisionSurface.js +130 -0
  128. package/dist/charts/mlcharts/DistinctTicks.js +13 -0
  129. package/dist/charts/mlcharts/ElbowMath.js +153 -0
  130. package/dist/charts/mlcharts/ElbowPlot.js +273 -0
  131. package/dist/charts/mlcharts/ExplainabilitySamples.js +70 -0
  132. package/dist/charts/mlcharts/FeatureImportanceChart.js +150 -0
  133. package/dist/charts/mlcharts/ImportanceRanking.js +98 -0
  134. package/dist/charts/mlcharts/LearningCurveChart.js +276 -0
  135. package/dist/charts/mlcharts/LearningCurveMath.js +92 -0
  136. package/dist/charts/mlcharts/PartialDependence.js +92 -0
  137. package/dist/charts/mlcharts/PartialDependencePlot.js +225 -0
  138. package/dist/charts/mlcharts/WideLabel.js +28 -0
  139. package/dist/charts/mlcharts/curveDiagnosticEnums.js +37 -0
  140. package/dist/charts/mlcharts/index.js +32 -0
  141. package/dist/charts/proportion/index.js +5 -9
  142. package/dist/charts/scatter/CategoryScatterBase.js +1 -4
  143. package/dist/charts/scatter/ConnectedScatter.js +7 -4
  144. package/dist/charts/scatter/MarginalScatter.js +13 -8
  145. package/dist/charts/scatter/index.js +4 -17
  146. package/dist/charts/scientific/SpectrogramChart.js +9 -3
  147. package/dist/charts/scientific/index.js +2 -2
  148. package/dist/charts/signals/index.js +3 -13
  149. package/dist/charts/statistics/LorenzCurve.js +7 -4
  150. package/dist/charts/threebars/Candlestick3DChart.js +18 -2
  151. package/dist/charts/threeglobe/GlobeChart.js +2 -2
  152. package/dist/charts/threeglobe/GlobeSamples.js +2 -1
  153. package/dist/charts/threeglobe/RingGlobe.js +1 -1
  154. package/dist/charts/threenetwork/ConeTreeChart.js +2 -1
  155. package/dist/charts/threenetwork/Gantt3DChart.js +3 -2
  156. package/dist/charts/threesurfaces/Ridgeline3DChart.js +22 -4
  157. package/dist/charts/threevolume/LorenzAttractor.js +19 -0
  158. package/dist/charts/threevolume/VolumeRaycastChart.js +4 -3
  159. package/dist/charts/threevolume/volumeEnums.js +1 -0
  160. package/dist/charts/timeline/index.js +2 -10
  161. package/dist/chunk-753UBLV2.cjs +33 -0
  162. package/dist/chunk-VX54A7KA.cjs +17 -0
  163. package/dist/{chunk-W5GAAAZY.cjs → chunk-WAODDPYX.cjs} +24874 -17620
  164. package/dist/chunk-Z3JPIJSC.cjs +338 -0
  165. package/dist/core/CartesianChart.js +87 -11
  166. package/dist/core/Chart.js +63 -15
  167. package/dist/core/Chart2D.js +54 -17
  168. package/dist/core/Chart3D.js +56 -20
  169. package/dist/core/ChartExportError.js +2 -0
  170. package/dist/core/ChartExporter.js +50 -8
  171. package/dist/core/ColumnCheck.js +53 -0
  172. package/dist/core/Context2D.js +29 -15
  173. package/dist/core/Context3D.js +7 -2
  174. package/dist/core/ExcelWriter.js +152 -0
  175. package/dist/core/ExportText.js +28 -0
  176. package/dist/core/MarkOrigin.js +88 -0
  177. package/dist/core/MarkStyle.js +294 -0
  178. package/dist/core/Palette.js +27 -7
  179. package/dist/core/PdfPage.js +13 -0
  180. package/dist/core/PdfWriter.js +245 -0
  181. package/dist/core/Scales.js +52 -2
  182. package/dist/core/SceneOverlay.js +4 -4
  183. package/dist/core/TextFit.js +37 -0
  184. package/dist/core/Theme.js +28 -1
  185. package/dist/core/enums.js +33 -1
  186. package/dist/core/pathMorph.js +15 -0
  187. package/dist/data/SampleData.js +1 -17
  188. package/dist/data/WorldSample.js +23 -0
  189. package/dist/index.cjs +810 -691
  190. package/dist/index.d.cts +8390 -2925
  191. package/dist/index.d.ts +8392 -2927
  192. package/dist/index.js +106 -56
  193. package/dist/react/ChartView.js +18 -8
  194. package/dist/react/GraphHandle.js +2 -0
  195. package/dist/react/components.js +74 -116
  196. package/package.json +13 -3
  197. package/dist/catalog-4UP2YFNR.cjs +0 -7
  198. package/dist/charts/bars/BulletBar.js +0 -184
  199. package/dist/charts/bars/ErrorBarColumn.js +0 -147
  200. package/dist/charts/bars/HistogramBar.js +0 -217
  201. package/dist/charts/bars/MarimekkoBar.js +0 -191
  202. package/dist/charts/bars/ParetoBar.js +0 -143
  203. package/dist/charts/comparison/PanelChart.js +0 -70
  204. package/dist/charts/comparison/SmallMultipleBars.js +0 -134
  205. package/dist/charts/comparison/SpanChart.js +0 -118
  206. package/dist/charts/comparison/TrellisChart.js +0 -158
  207. package/dist/charts/distribution/RidgeSlabs3DChart.js +0 -169
  208. package/dist/charts/financial/Candlestick3D.js +0 -157
  209. package/dist/charts/flow/BumpFlow.js +0 -212
  210. package/dist/charts/flow/StreamFlow.js +0 -174
  211. package/dist/charts/geomaps/ThiessenMap.js +0 -106
  212. package/dist/charts/hierarchy/ConeTree3D.js +0 -135
  213. package/dist/charts/hierarchy/Hierarchy3DChart.js +0 -27
  214. package/dist/charts/hierarchy/SpherePack3D.js +0 -81
  215. package/dist/charts/hierarchy/SunburstTower3D.js +0 -93
  216. package/dist/charts/hierarchy/TreemapSkyline3D.js +0 -98
  217. package/dist/charts/linesareas/ConnectedScatterTime.js +0 -206
  218. package/dist/charts/linesareas/SmallMultipleLines.js +0 -153
  219. package/dist/charts/mathematical/ParametricSurface3D.js +0 -234
  220. package/dist/charts/mathematical/StrangeAttractor3D.js +0 -251
  221. package/dist/charts/proportion/ConeFunnel.js +0 -126
  222. package/dist/charts/proportion/ExtrudedPieChart.js +0 -123
  223. package/dist/charts/scatter/BeeswarmScatter.js +0 -151
  224. package/dist/charts/scatter/BinnedScatterBase.js +0 -51
  225. package/dist/charts/scatter/ContourScatter.js +0 -116
  226. package/dist/charts/scatter/GridBinScatter.js +0 -98
  227. package/dist/charts/scatter/HexbinScatter.js +0 -74
  228. package/dist/charts/scatter/StripScatter.js +0 -107
  229. package/dist/charts/scatter/SunflowerScatter.js +0 -109
  230. package/dist/charts/signals/ConstellationChart.js +0 -153
  231. package/dist/charts/signals/CorrelogramChart.js +0 -171
  232. package/dist/charts/signals/EyeDiagramChart.js +0 -155
  233. package/dist/charts/signals/PoleZeroChart.js +0 -164
  234. package/dist/charts/signals/SpectrogramChart.js +0 -79
  235. package/dist/charts/timeline/CalendarSkylineChart.js +0 -128
  236. package/dist/charts/timeline/ScheduleBlocksChart.js +0 -119
  237. package/dist/charts/timeline/SpiralTimeline.js +0 -154
  238. package/dist/charts/timeline/StreamTimeline.js +0 -141
@@ -0,0 +1,338 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/core/ChartExportError.ts
2
+ var ExportErrorCode = /* @__PURE__ */ ((ExportErrorCode2) => {
3
+ ExportErrorCode2["Unsupported"] = "unsupported";
4
+ ExportErrorCode2["NoData"] = "no-data";
5
+ ExportErrorCode2["Tainted"] = "tainted";
6
+ ExportErrorCode2["Encoding"] = "encoding";
7
+ ExportErrorCode2["Unavailable"] = "unavailable";
8
+ ExportErrorCode2["Unmounted"] = "unmounted";
9
+ return ExportErrorCode2;
10
+ })(ExportErrorCode || {});
11
+ var ChartExportError = class extends Error {
12
+ constructor(code, message, options) {
13
+ super(message, options);
14
+ this.code = code;
15
+ this.name = "ChartExportError";
16
+ }
17
+
18
+ };
19
+
20
+ // src/core/ChartExporter.ts
21
+ var ExportFormat = /* @__PURE__ */ ((ExportFormat2) => {
22
+ ExportFormat2["SVG"] = "svg";
23
+ ExportFormat2["PNG"] = "png";
24
+ ExportFormat2["CSV"] = "csv";
25
+ ExportFormat2["PDF"] = "pdf";
26
+ ExportFormat2["Excel"] = "xlsx";
27
+ return ExportFormat2;
28
+ })(ExportFormat || {});
29
+ var SVG_NS = "http://www.w3.org/2000/svg";
30
+ var MIME = { ["svg" /* SVG */]: "image/svg+xml;charset=utf-8", ["png" /* PNG */]: "image/png", ["csv" /* CSV */]: "text/csv;charset=utf-8" };
31
+ var STYLED = [
32
+ "fill",
33
+ "fill-opacity",
34
+ "fill-rule",
35
+ "stroke",
36
+ "stroke-opacity",
37
+ "stroke-width",
38
+ "stroke-linecap",
39
+ "stroke-linejoin",
40
+ "paint-order",
41
+ "opacity",
42
+ "visibility",
43
+ "font-family",
44
+ "font-size",
45
+ "font-style",
46
+ "font-weight",
47
+ "letter-spacing",
48
+ "text-anchor",
49
+ "dominant-baseline"
50
+ ];
51
+ var LIVE_ONLY = [
52
+ "cursor",
53
+ "pointer-events",
54
+ "touch-action",
55
+ "transition",
56
+ "stroke-dasharray",
57
+ "stroke-dashoffset",
58
+ "opacity",
59
+ "scale",
60
+ "transform-origin",
61
+ "transform-box",
62
+ "--geo-unzoom"
63
+ ];
64
+ var LIVE_ROOT = ["display", "max-width", "background", "position", "z-index"];
65
+ var CHROME = '[role="button"], .m-zoom-hit, .g-keyboard';
66
+ var TIMEOUT = 2e4;
67
+ var HELD_STROKE = "non-scaling-stroke";
68
+ var UNWRITABLE = /[^\t\n\r\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu;
69
+ var FORMULA = /^[=+\-@\t\r]/;
70
+ var SHEET_ROWS = 1048576;
71
+ var quoteless = (name) => name.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
72
+ var ChartExporter = class _ChartExporter {
73
+ /** Comma-separated values per RFC 4180. Text that a spreadsheet would run as a formula is prefixed with `'`. */
74
+ static csv({ columns, rows }) {
75
+ if (!rows.length) throw new ChartExportError("no-data" /* NoData */, "CSV export needs rows, and this chart's data has no flat table view.");
76
+ return [columns, ...rows].map((row) => row.map((cell) => _ChartExporter.#cell(cell)).join(",")).join("\r\n");
77
+ }
78
+ /** The formats built from rows. A worksheet has a fixed height, and one row of it is the header. */
79
+ static tabular(rows) {
80
+ return rows ? ["csv" /* CSV */, ...rows < SHEET_ROWS ? ["xlsx" /* Excel */] : []] : [];
81
+ }
82
+ /** Awaits a writer's dynamic import, so a chunk that fails to load rejects like any other export. */
83
+ static async load(format, load) {
84
+ try {
85
+ return await load();
86
+ } catch (cause) {
87
+ throw new ChartExportError("unavailable" /* Unavailable */, `The ${format} writer could not be loaded: ${cause.message}`, { cause });
88
+ }
89
+ }
90
+ static #cell(cell) {
91
+ const text = _ChartExporter.text(cell);
92
+ const guarded = typeof cell === "string" && FORMULA.test(text) && !Number.isFinite(+text) ? `'${text}` : text;
93
+ return /[",\r\n]/.test(guarded) ? `"${guarded.replaceAll('"', '""')}"` : guarded;
94
+ }
95
+ /** A cell as plain text. */
96
+ static text(cell) {
97
+ if (cell === null || cell === void 0) return "";
98
+ if (cell instanceof Date) return Number.isNaN(+cell) ? "" : cell.toISOString();
99
+ if (typeof cell === "number") return Number.isNaN(cell) ? "" : String(cell);
100
+ return typeof cell === "object" ? _ChartExporter.#json(cell) : String(cell);
101
+ }
102
+ /** Layouts leave cycles in the objects they are handed, which JSON cannot write. */
103
+ static #json(cell) {
104
+ try {
105
+ return JSON.stringify(cell);
106
+ } catch (e) {
107
+ return String(cell);
108
+ }
109
+ }
110
+ /**
111
+ * A standalone copy of a live chart's SVG: computed styles written onto the marks, zoom state baked
112
+ * into plain transforms, the theme surface as a real rectangle, and on-screen controls removed.
113
+ */
114
+ static document(svg, theme) {
115
+ _ChartExporter.#finish(svg);
116
+ const copy = svg.cloneNode(true);
117
+ _ChartExporter.#inline(svg, copy);
118
+ copy.querySelectorAll(CHROME).forEach((el) => el.remove());
119
+ copy.querySelectorAll("[tabindex]").forEach((el) => el.removeAttribute("tabindex"));
120
+ for (const el of [copy, ...copy.querySelectorAll("[style]")]) {
121
+ for (const name of el === copy ? [...LIVE_ONLY, ...LIVE_ROOT] : LIVE_ONLY) el.style.removeProperty(name);
122
+ if (!el.style.length) el.removeAttribute("style");
123
+ }
124
+ copy.setAttribute("font-family", theme.fontFamily);
125
+ const surface = copy.ownerDocument.createElementNS(SVG_NS, "rect");
126
+ for (const [name, value] of Object.entries({ x: 0, y: 0, width: svg.getAttribute("width"), height: svg.getAttribute("height"), fill: theme.surface })) surface.setAttribute(name, `${value}`);
127
+ copy.insertBefore(surface, _nullishCoalesce(_optionalChain([copy, 'access', _ => _.querySelector, 'call', _2 => _2(":scope > title"), 'optionalAccess', _3 => _3.nextSibling]), () => ( copy.firstChild)));
128
+ return copy;
129
+ }
130
+ /** Web Animations show through computed styles, so fades and line reveals jump to their end first. */
131
+ static #finish(svg) {
132
+ for (const motion of _nullishCoalesce(_optionalChain([svg, 'access', _4 => _4.getAnimations, 'optionalCall', _5 => _5({ subtree: true })]), () => ( []))) {
133
+ if (_optionalChain([motion, 'access', _6 => _6.effect, 'optionalAccess', _7 => _7.getComputedTiming, 'call', _8 => _8(), 'access', _9 => _9.iterations]) !== Infinity) motion.finish();
134
+ }
135
+ }
136
+ /** Writes only what differs from the parent, so output stays close to the size of the live SVG. */
137
+ static #inline(live, copy) {
138
+ const copies = [copy, ...copy.querySelectorAll("*")];
139
+ const values = /* @__PURE__ */ new Map();
140
+ [live, ...live.querySelectorAll("*")].forEach((el, index) => {
141
+ const computed = getComputedStyle(el), own = {}, parent = values.get(el.parentElement), target = copies[index];
142
+ for (const name of STYLED) {
143
+ if (name === "opacity" && el.style.opacity) continue;
144
+ const value = own[name] = computed.getPropertyValue(name);
145
+ const inherited = name === "opacity" ? "1" : _optionalChain([parent, 'optionalAccess', _10 => _10[name]]);
146
+ if (value && value !== inherited && !value.includes("url(")) target.setAttribute(name, value);
147
+ }
148
+ values.set(el, own);
149
+ _ChartExporter.#bakeScale(el, target, computed);
150
+ });
151
+ }
152
+ /** Map symbols hold their size with the CSS `scale` property, which SVG tools outside the browser ignore. */
153
+ static #bakeScale(el, target, computed) {
154
+ const scale = parseFloat(computed.scale);
155
+ if (!el.style.scale || !Number.isFinite(scale)) return;
156
+ const box = computed.transformBox === "fill-box" ? _optionalChain([el, 'access', _11 => _11.getBBox, 'optionalCall', _12 => _12()]) : null;
157
+ const [x = 0, y = 0] = box ? [box.x + box.width / 2, box.y + box.height / 2] : computed.transformOrigin.split(" ").map(parseFloat);
158
+ const placed = target.getAttribute("transform");
159
+ target.setAttribute("transform", `translate(${x} ${y}) scale(${scale})${placed ? ` ${placed}` : ""} translate(${-x} ${-y})`);
160
+ }
161
+ /**
162
+ * Replaces links inside an exported SVG with their bytes: map tiles and any `@font-face` the page
163
+ * declares for the theme's font. Tiles must be readable across origins, or this rejects. A font
164
+ * that cannot be fetched stays linked, because the text still draws in the fallback fonts.
165
+ */
166
+ static async embed(svg, theme) {
167
+ await Promise.all([...svg.querySelectorAll("image")].map(async (image) => {
168
+ const url = image.getAttribute("href");
169
+ if (url && !url.startsWith("data:")) image.setAttribute("href", await _ChartExporter.#tile(url));
170
+ }));
171
+ const fonts = await _ChartExporter.#fonts(theme.fontFamily);
172
+ if (!fonts) return;
173
+ const style = svg.ownerDocument.createElementNS(SVG_NS, "style");
174
+ style.textContent = fonts;
175
+ svg.insertBefore(style, _nullishCoalesce(_optionalChain([svg, 'access', _13 => _13.querySelector, 'call', _14 => _14(":scope > title"), 'optionalAccess', _15 => _15.nextSibling]), () => ( svg.firstChild)));
176
+ }
177
+ static async #tile(url) {
178
+ try {
179
+ const data = await _ChartExporter.#fetch(url);
180
+ if (data.startsWith("data:image/")) return data;
181
+ throw new Error("the response is not an image");
182
+ } catch (cause) {
183
+ throw new ChartExportError(
184
+ "tainted" /* Tainted */,
185
+ `Cannot embed map tile ${url}: ${cause.message}. Pages may only read tiles from servers that send Access-Control-Allow-Origin. Use a tile source with CORS enabled, or drop \`tiles\` for this export. toSVG() still works, and keeps tiles as links.`,
186
+ { cause }
187
+ );
188
+ }
189
+ }
190
+ static async #fetch(url) {
191
+ const response = await fetch(url, { mode: "cors", credentials: "omit", signal: AbortSignal.timeout(TIMEOUT) });
192
+ if (!response.ok) throw new Error(`the server answered ${response.status}`);
193
+ const blob = await response.blob();
194
+ return new Promise((resolve, reject) => {
195
+ const reader = new FileReader();
196
+ reader.onload = () => resolve(reader.result);
197
+ reader.onerror = () => reject(_nullishCoalesce(reader.error, () => ( new Error("the response could not be read"))));
198
+ reader.readAsDataURL(blob);
199
+ });
200
+ }
201
+ static async #fonts(family) {
202
+ const wanted = new Set(family.split(",").map(quoteless));
203
+ const rules = [];
204
+ for (const sheet of document.styleSheets) {
205
+ let list;
206
+ try {
207
+ list = sheet.cssRules;
208
+ } catch (e2) {
209
+ continue;
210
+ }
211
+ for (const rule of _ChartExporter.#rules(list)) {
212
+ if (rule.constructor.name === "CSSFontFaceRule" && wanted.has(quoteless(rule.style.getPropertyValue("font-family")))) {
213
+ rules.push(_ChartExporter.#fontFace(rule.cssText, _nullishCoalesce(sheet.href, () => ( document.baseURI))));
214
+ }
215
+ }
216
+ }
217
+ return (await Promise.all(rules)).join("\n");
218
+ }
219
+ /** Every rule, including those nested in `@media` or `@layer`. */
220
+ static *#rules(list) {
221
+ for (const rule of list) {
222
+ yield rule;
223
+ if ("cssRules" in rule) yield* _ChartExporter.#rules(rule.cssRules);
224
+ }
225
+ }
226
+ static async #fontFace(css, base) {
227
+ let out = css;
228
+ for (const [link, url] of css.matchAll(/url\(["']?([^"')]+)["']?\)/g)) {
229
+ if (url.startsWith("data:")) continue;
230
+ out = out.replace(link, await _ChartExporter.#fetch(new URL(url, base).href).then((data) => `url("${data}")`, () => link));
231
+ }
232
+ return out;
233
+ }
234
+ static serialize(svg) {
235
+ return _ChartExporter.writable(new XMLSerializer().serializeToString(svg));
236
+ }
237
+ /** `text` without the characters XML cannot hold. */
238
+ static writable(text) {
239
+ return text.replace(UNWRITABLE, "");
240
+ }
241
+ /** Draws SVG text onto a canvas filled with `surface`. The SVG must already be embedded: an SVG image cannot load links. */
242
+ static async raster(svg, { width, height }, scale, surface) {
243
+ const url = URL.createObjectURL(_ChartExporter.blob(_ChartExporter.#atScale(svg, { width, height }, scale), "svg" /* SVG */));
244
+ try {
245
+ const image = await _ChartExporter.#load(url);
246
+ const canvas = _ChartExporter.canvas(width * scale, height * scale);
247
+ const paint = _ChartExporter.paint(canvas);
248
+ paint.fillStyle = surface;
249
+ paint.fillRect(0, 0, canvas.width, canvas.height);
250
+ paint.drawImage(image, 0, 0, canvas.width, canvas.height);
251
+ return canvas;
252
+ } finally {
253
+ URL.revokeObjectURL(url);
254
+ }
255
+ }
256
+ /**
257
+ * A held stroke is sized in screen pixels, and engines disagree on what those are in an image drawn larger:
258
+ * Chromium keeps it thin, WebKit scales it. In an image already at full size they agree, so that is what they get.
259
+ */
260
+ static #atScale(svg, { width, height }, scale) {
261
+ if (scale === 1 || !svg.includes(HELD_STROKE)) return svg;
262
+ const root = new DOMParser().parseFromString(svg, "image/svg+xml").documentElement;
263
+ for (const [name, value] of Object.entries({ viewBox: `0 0 ${width} ${height}`, width: width * scale, height: height * scale })) root.setAttribute(name, `${value}`);
264
+ for (const el of root.querySelectorAll(`[vector-effect="${HELD_STROKE}"]`)) {
265
+ el.style.setProperty("stroke-width", `${parseFloat(_nullishCoalesce(_optionalChain([el, 'access', _16 => _16.closest, 'call', _17 => _17("[stroke-width]"), 'optionalAccess', _18 => _18.getAttribute, 'call', _19 => _19("stroke-width")]), () => ( "1"))) * scale}px`);
266
+ const dash = el.getAttribute("stroke-dasharray");
267
+ if (dash && dash !== "none") el.setAttribute("stroke-dasharray", dash.split(/[\s,]+/).map((length) => parseFloat(length) * scale).join(" "));
268
+ }
269
+ return _ChartExporter.serialize(root);
270
+ }
271
+ static #load(url) {
272
+ return new Promise((resolve, reject) => {
273
+ const image = new Image();
274
+ image.onload = () => resolve(image);
275
+ image.onerror = () => reject(new ChartExportError("encoding" /* Encoding */, "The browser could not decode the exported SVG."));
276
+ image.src = url;
277
+ });
278
+ }
279
+ static canvas(width, height) {
280
+ const canvas = document.createElement("canvas");
281
+ canvas.width = Math.round(width);
282
+ canvas.height = Math.round(height);
283
+ return canvas;
284
+ }
285
+ static paint(canvas) {
286
+ const paint = canvas.getContext("2d");
287
+ if (!paint) throw new ChartExportError("encoding" /* Encoding */, `The browser could not create a ${canvas.width}\xD7${canvas.height} canvas. Try a smaller scale.`);
288
+ return paint;
289
+ }
290
+ /** Encodes a canvas, turning a tainted or oversized canvas into a clear error. */
291
+ static png(canvas) {
292
+ return new Promise((resolve, reject) => {
293
+ const tooLarge = () => new ChartExportError("encoding" /* Encoding */, `The browser could not encode a ${canvas.width}\xD7${canvas.height} PNG. Try a smaller scale.`);
294
+ try {
295
+ canvas.toBlob((blob) => blob ? resolve(blob) : reject(tooLarge()), MIME["png" /* PNG */]);
296
+ } catch (cause) {
297
+ reject(cause.name === "SecurityError" ? new ChartExportError("tainted" /* Tainted */, "The chart drew a cross-origin image, so the browser will not let the page read it as a PNG.", { cause }) : cause);
298
+ }
299
+ });
300
+ }
301
+ static scale(scale = 2) {
302
+ if (Number.isFinite(scale) && scale > 0) return scale;
303
+ throw new RangeError(`PNG scale must be a positive number, got ${scale}.`);
304
+ }
305
+ /** A file for `content`. CSV carries a byte order mark, which Excel needs to read UTF-8. */
306
+ static blob(content, format) {
307
+ return new Blob([format === "csv" /* CSV */ ? "\uFEFF" : "", content], { type: MIME[format] });
308
+ }
309
+ /** Whether any mark is still moving, in which case the DOM holds in-between geometry. */
310
+ static animating(svg) {
311
+ return !!svg.querySelector(".live-marks[transform]") || !!_optionalChain([svg, 'access', _20 => _20.getAnimations, 'optionalCall', _21 => _21({ subtree: true }), 'access', _22 => _22.length]) || [...svg.querySelectorAll("*")].some((el) => "__transition" in el && el.__transition);
312
+ }
313
+ /** An off-screen host, so an unmounted chart still measures text and resolves styles. */
314
+ static scratch({ width, height }) {
315
+ const host = document.createElement("div");
316
+ host.style.cssText = `position:absolute;left:-99999px;top:0;width:${width}px;height:${height}px`;
317
+ host.setAttribute("aria-hidden", "true");
318
+ return document.body.appendChild(host);
319
+ }
320
+ /** Hands a file to the browser's download flow. Use it directly for a Blob you already hold. */
321
+ static save(blob, filename) {
322
+ const url = URL.createObjectURL(blob);
323
+ const link = document.createElement("a");
324
+ link.href = url;
325
+ link.download = filename;
326
+ link.style.display = "none";
327
+ document.body.appendChild(link).click();
328
+ link.remove();
329
+ setTimeout(() => URL.revokeObjectURL(url), 3e4);
330
+ }
331
+ };
332
+
333
+
334
+
335
+
336
+
337
+
338
+ exports.ExportErrorCode = ExportErrorCode; exports.ChartExportError = ChartExportError; exports.ExportFormat = ExportFormat; exports.ChartExporter = ChartExporter;
@@ -1,16 +1,39 @@
1
1
  import { AnnotationLayer } from "./AnnotationLayer.js";
2
+ import { AxisWindow } from "./AxisWindow.js";
2
3
  import { Chart2D } from "./Chart2D.js";
3
4
  import { ChartViewport, ZoomAxis } from "./ChartViewport.js";
5
+ import { FRAME } from "./Context2D.js";
6
+ import { RenderType } from "./enums.js";
4
7
  import { LiveMotion } from "./LiveMotion.js";
5
8
  import { Scales } from "./Scales.js";
9
+ import { TextFit } from "./TextFit.js";
6
10
  import { axisBottom, axisLeft, max } from "d3";
7
11
  // src/core/CartesianChart.ts
8
- var estimate = (label) => label.length * 6.2;
12
+ var OPEN = 1e4;
13
+ var clips = 0;
14
+ var CHAR = 6.2;
15
+ var GUTTER = 96;
16
+ var TICK_HEIGHT = 12;
17
+ /** Sets the ends of a continuous scale in place, because subclasses close over the scales they build. Says if it did. */
18
+ function pin(scale, [low, high]) {
19
+ if (AxisWindow.kind(scale) !== "continuous") return false;
20
+ const domain = [...scale.domain()], last = domain.length - 1;
21
+ const flipped = +domain[0] > +domain[last];
22
+ domain[flipped ? last : 0] = low ?? domain[flipped ? last : 0];
23
+ domain[flipped ? 0 : last] = high ?? domain[flipped ? 0 : last];
24
+ const shows = scale.shows ?? scale.copy().domain(domain);
25
+ if (+domain[0] === +domain[last] || !domain.every((value) => Number.isFinite(shows(value)))) return false;
26
+ scale.domain(domain);
27
+ return true;
28
+ }
9
29
  /** x/y plots: draws recessive grid and axes around `plot`. Subclasses build scales then marks. */
10
30
  var CartesianChart = class CartesianChart extends Chart2D {
11
31
  gutter = 44;
12
32
  fitted = 44;
13
33
  #axes = null;
34
+ #clipId = `chart-domain-${++clips}`;
35
+ #pinnedX = false;
36
+ #pinnedY = false;
14
37
  #plot = {
15
38
  width: 0,
16
39
  height: 0
@@ -35,16 +58,16 @@ var CartesianChart = class CartesianChart extends Chart2D {
35
58
  return super.animated && !this.#view.moving;
36
59
  }
37
60
  xTicks() {
38
- return 7;
61
+ return this.options.xTicks ?? 7;
39
62
  }
40
63
  yTicks() {
41
- return 6;
64
+ return this.options.yTicks ?? 6;
42
65
  }
43
66
  plotMargin() {
44
67
  return {
45
68
  top: 12,
46
69
  right: 20,
47
- bottom: this.options.xLabel ? 44 : 28,
70
+ bottom: (this.options.xLabel ? 44 : 28) + this.#tickGrowth,
48
71
  left: this.gutter + (this.options.yLabel ? 18 : 0)
49
72
  };
50
73
  }
@@ -84,6 +107,8 @@ var CartesianChart = class CartesianChart extends Chart2D {
84
107
  }
85
108
  draw(ctx) {
86
109
  const axes = this.#axes = this.scales(ctx);
110
+ this.#typeAxes(axes);
111
+ this.#pinDomains(axes);
87
112
  this.#plot = ctx.plot;
88
113
  const stage = this.#view.attach(ctx, axes);
89
114
  const moved = this.#motion?.layer?.node();
@@ -94,6 +119,52 @@ var CartesianChart = class CartesianChart extends Chart2D {
94
119
  this.plotMarks(marks, axes);
95
120
  AnnotationLayer.draw(layer ? stage.within(layer) : stage, axes, this.options, this.#view.controlsBox);
96
121
  this.#view.finish(ctx);
122
+ this.#clipToDomain(ctx);
123
+ }
124
+ /** Extra height a larger `fontSize` needs under the plot. */
125
+ get #tickGrowth() {
126
+ return TICK_HEIGHT * (this.textRoom - 1);
127
+ }
128
+ #typeAxes({ x, y }) {
129
+ const { xScaleType, yScaleType } = this.options, follows = this.zoomAxes();
130
+ if (!follows || this.renderer !== RenderType.Svg) return;
131
+ if (xScaleType && follows !== ZoomAxis.Y) Scales.retype(x, xScaleType, this.#floor("x"));
132
+ if (yScaleType && follows !== ZoomAxis.X) Scales.retype(y, yScaleType, this.#floor("y"));
133
+ }
134
+ /** The smallest value of the column an axis plots, while all of them are positive. */
135
+ #floor(axis) {
136
+ const column = this.options[axis], { data } = this.options;
137
+ if (typeof column !== "string" || !Array.isArray(data)) return null;
138
+ let low = Infinity;
139
+ for (const row of data) {
140
+ const value = row?.[column];
141
+ if (value === null || value === void 0 || !Number.isFinite(+value)) continue;
142
+ if (+value <= 0) return null;
143
+ low = Math.min(low, +value);
144
+ }
145
+ return Number.isFinite(low) ? low : null;
146
+ }
147
+ /** Runs before the viewport sees the scales, so zoom starts from these ranges. */
148
+ #pinDomains({ x, y }) {
149
+ const { xDomain, yDomain } = this.options, follows = this.zoomAxes();
150
+ this.#pinnedX = !!xDomain && !!follows && follows !== ZoomAxis.Y && pin(x, xDomain);
151
+ this.#pinnedY = !!yDomain && !!follows && follows !== ZoomAxis.X && pin(y, yDomain);
152
+ }
153
+ /** Clipped along the pinned axis only, so labels beside the plot stay. Zoomed, the viewport's own clip takes over. */
154
+ #clipToDomain(ctx) {
155
+ const pinnedX = this.#pinnedX, pinnedY = this.#pinnedY;
156
+ if (!pinnedX && !pinnedY || this.#view.zoomed) return;
157
+ const { width, height } = ctx.plot;
158
+ const box = {
159
+ x: pinnedX ? 0 : -1e4,
160
+ y: pinnedY ? 0 : -1e4,
161
+ width: pinnedX ? width : 2 * OPEN,
162
+ height: pinnedY ? height : 2 * OPEN
163
+ };
164
+ ctx.marks("domain-clip", "clipPath", [box]).attr("id", this.#clipId).selectAll("rect").data((d) => [d]).join("rect").attr("x", (d) => d.x).attr("y", (d) => d.y).attr("width", (d) => d.width).attr("height", (d) => d.height);
165
+ ctx.root.selectAll(":scope > g").filter(function() {
166
+ return !this.matches(FRAME);
167
+ }).attr("clip-path", `url(#${this.#clipId})`);
97
168
  }
98
169
  tickFormat(scale) {
99
170
  const domain = scale.domain?.() ?? [];
@@ -106,18 +177,20 @@ var CartesianChart = class CartesianChart extends Chart2D {
106
177
  drawAxes(ctx, { x, y }) {
107
178
  const { width, height } = ctx.plot;
108
179
  const { theme } = ctx;
109
- const xCount = Math.min(this.xTicks(), Math.floor(width / 80));
180
+ const { xFormat, yFormat, xTicks, yTicks } = this.options;
181
+ const xCount = Math.min(xTicks ?? this.xTicks(), Math.floor(width / 80)), yCount = yTicks ?? this.yTicks();
110
182
  const bottom = axisBottom(x).ticks(xCount).tickSizeOuter(0);
111
- const left = axisLeft(y).ticks(this.yTicks()).tickSize(0);
112
- const fx = this.#view.legible(x, this.tickFormat(x), xCount), fy = this.#view.legible(y, this.tickFormat(y), this.yTicks());
183
+ const left = axisLeft(y).ticks(yCount).tickSize(0);
184
+ const fx = this.#view.legible(x, xFormat ?? this.tickFormat(x), xCount), fy = this.#view.legible(y, yFormat ?? this.tickFormat(y), yCount);
113
185
  if (fx) bottom.tickFormat(fx);
114
186
  if (fy) left.tickFormat(fy);
115
187
  const yAxis = ctx.group("axis-y").call(left);
116
188
  const labels = yAxis.selectAll("text").nodes().map((node) => node.textContent ?? "");
117
- this.fitted = Math.min(96, Math.ceil(Math.max(28, ...labels.map(estimate))) + 12);
189
+ const char = CHAR * this.fontScale;
190
+ this.fitted = Math.min(GUTTER * this.textRoom, Math.ceil(Math.max(28, ...labels.map((label) => label.length * char))) + 12);
118
191
  const xAxis = ctx.group("axis-x").attr("transform", `translate(0,${height})`).call(bottom);
119
192
  const grid = ctx.group("grid").lower();
120
- const ticks = "ticks" in y ? y.ticks(this.yTicks()) : [];
193
+ const ticks = "ticks" in y ? y.ticks(yCount) : [];
121
194
  grid.selectAll("line").data(this.options.grid === false ? [] : ticks).join("line").attr("x1", 0).attr("x2", width).attr("y1", (tick) => y(tick) ?? 0).attr("y2", (tick) => y(tick) ?? 0).attr("stroke", theme.grid).attr("stroke-width", 1);
122
195
  for (const axis of [xAxis, yAxis]) {
123
196
  axis.select(".domain").attr("stroke", theme.axis);
@@ -127,16 +200,19 @@ var CartesianChart = class CartesianChart extends Chart2D {
127
200
  yAxis.select(".domain").attr("stroke", "none");
128
201
  const titles = [this.options.xLabel && {
129
202
  t: this.options.xLabel,
203
+ shown: TextFit.fit(this.options.xLabel, width, char),
130
204
  x: width / 2,
131
- y: height + 38,
205
+ y: height + 38 + this.#tickGrowth,
132
206
  r: 0
133
207
  }, this.options.yLabel && {
134
208
  t: this.options.yLabel,
209
+ shown: TextFit.fit(this.options.yLabel, height, char),
135
210
  x: -height / 2,
136
211
  y: -this.gutter - 4,
137
212
  r: -90
138
213
  }].filter(Boolean);
139
- ctx.text(ctx.marks("axis-title", "text", titles, (d) => d.t).attr("text-anchor", "middle").attr("transform", (d) => `rotate(${d.r}) translate(${d.x},${d.y})`).text((d) => d.t));
214
+ const axisTitles = ctx.text(ctx.marks("axis-title", "text", titles, (d) => d.t).attr("text-anchor", "middle").attr("transform", (d) => `rotate(${d.r}) translate(${d.x},${d.y})`).text((d) => d.shown));
215
+ TextFit.hint(axisTitles, (d) => d.shown === d.t ? void 0 : d.t);
140
216
  }
141
217
  };
142
218
  export { CartesianChart };
@@ -1,5 +1,6 @@
1
1
  import { ChartExportError, ExportErrorCode } from "./ChartExportError.js";
2
2
  import { ChartExporter, ExportFormat } from "./ChartExporter.js";
3
+ import { ColumnCheck } from "./ColumnCheck.js";
3
4
  import { LegendPosition, ThemeMode } from "./enums.js";
4
5
  import { LiveStream } from "./LiveStream.js";
5
6
  import { Palette } from "./Palette.js";
@@ -46,7 +47,13 @@ var Chart = class {
46
47
  if (getComputedStyle(host).position === "static") host.style.position = "relative";
47
48
  this.tooltipBox = new Tooltip(host);
48
49
  this.render();
49
- if (typeof ResizeObserver !== "undefined" && !(this.options.width && this.options.height)) {
50
+ this.#watch(host);
51
+ return this;
52
+ }
53
+ /** Follows the host's size and the system scheme only while the options leave them open, which an update can change. */
54
+ #watch(host) {
55
+ const fluid = typeof ResizeObserver !== "undefined" && !(this.options.width && this.options.height);
56
+ if (fluid && !this.observer) {
50
57
  let last = "";
51
58
  this.observer = new ResizeObserver(([entry]) => {
52
59
  const key = `${entry.contentRect.width}x${entry.contentRect.height}`;
@@ -57,22 +64,33 @@ var Chart = class {
57
64
  }
58
65
  });
59
66
  this.observer.observe(host);
67
+ } else if (!fluid) {
68
+ this.observer?.disconnect();
69
+ this.observer = null;
60
70
  }
61
- if ((this.options.theme ?? ThemeMode.Auto) === ThemeMode.Auto && typeof matchMedia === "function") {
71
+ const auto = (this.options.theme ?? ThemeMode.Auto) === ThemeMode.Auto && typeof matchMedia === "function";
72
+ if (auto && !this.media) {
62
73
  this.media = matchMedia("(prefers-color-scheme: dark)");
63
74
  this.media.addEventListener("change", this.onScheme);
75
+ } else if (!auto) {
76
+ this.media?.removeEventListener("change", this.onScheme);
77
+ this.media = null;
64
78
  }
65
- return this;
66
79
  }
80
+ /** Merges `patch` into the options. A key set to `undefined` returns to the chart's default. */
67
81
  update(patch) {
68
- this.options = {
82
+ const defaults = this.defaults();
83
+ const next = {
69
84
  ...this.options,
70
85
  ...patch
71
86
  };
87
+ for (const key of Object.keys(patch)) if (patch[key] === void 0) next[key] = defaults[key];
88
+ this.options = next;
72
89
  if ("data" in patch && this.#stream) {
73
90
  this.#stream.reset(patch.data);
74
91
  this.#seats.clear();
75
92
  }
93
+ if (this.host) this.#watch(this.host);
76
94
  return this.render();
77
95
  }
78
96
  /**
@@ -139,7 +157,9 @@ var Chart = class {
139
157
  this.#stream?.stop(this.#scratch);
140
158
  this.#scratch = false;
141
159
  this.observer?.disconnect();
160
+ this.observer = null;
142
161
  this.media?.removeEventListener("change", this.onScheme);
162
+ this.media = null;
143
163
  this.teardown();
144
164
  this.tooltipBox?.destroy();
145
165
  this.tooltipBox = null;
@@ -162,7 +182,11 @@ var Chart = class {
162
182
  }
163
183
  /** What this chart can export right now. Check this before offering a format. */
164
184
  get exportFormats() {
165
- return [ExportFormat.PNG, ...this.#rows.rows.length ? [ExportFormat.CSV] : []];
185
+ return [
186
+ ExportFormat.PNG,
187
+ ExportFormat.PDF,
188
+ ...ChartExporter.tabular(this.#rows.rows.length)
189
+ ];
166
190
  }
167
191
  /** The chart as SVG text. Map tiles stay links here: use `toStandaloneSVG` to embed them. 3D charts have no SVG and throw. */
168
192
  toSVG() {
@@ -182,6 +206,14 @@ var Chart = class {
182
206
  this.flush();
183
207
  return ChartExporter.csv(this.#rows);
184
208
  }
209
+ /** The chart as a picture on a PDF page, optionally followed by `table()`. Its code loads on first use. */
210
+ async toPDF(options) {
211
+ return (await ChartExporter.load("PDF", () => import("./PdfWriter.js"))).PdfWriter.write(this, options);
212
+ }
213
+ /** `table()` as an .xlsx workbook, optionally with the chart on a second sheet. Rejects when the chart has no rows. Its code loads on first use. */
214
+ async toExcel(options) {
215
+ return (await ChartExporter.load("Excel", () => import("./ExcelWriter.js"))).ExcelWriter.write(this, options);
216
+ }
185
217
  /** `table()` for exports. Overrides read their own data shape, so a chart holding none has no rows. */
186
218
  get #rows() {
187
219
  return this.options.data == null ? {
@@ -190,11 +222,13 @@ var Chart = class {
190
222
  } : this.table();
191
223
  }
192
224
  /** Saves the chart through the browser's download flow. */
193
- async download(format = ExportFormat.PNG, { filename, scale } = {}) {
194
- ChartExporter.save(await this.#file(format, scale), filename ?? `${this.meta.id}.${format}`);
225
+ async download(format = ExportFormat.PNG, options = {}) {
226
+ ChartExporter.save(await this.#file(format, options), options.filename ?? `${this.meta.id}.${format}`);
195
227
  }
196
- async #file(format, scale) {
197
- if (format === ExportFormat.PNG) return this.toPNG({ scale });
228
+ async #file(format, options) {
229
+ if (format === ExportFormat.PNG) return this.toPNG(options);
230
+ if (format === ExportFormat.PDF) return this.toPDF(options);
231
+ if (format === ExportFormat.Excel) return this.toExcel(options);
198
232
  return format === ExportFormat.SVG ? ChartExporter.blob(await this.toStandaloneSVG(), format) : ChartExporter.blob(this.toCSV(), format);
199
233
  }
200
234
  /** Settles once anything the chart loads by itself has arrived. Exports wait for it, so they never picture a chart still loading. */
@@ -202,18 +236,32 @@ var Chart = class {
202
236
  await this.meta.prepare?.();
203
237
  }
204
238
  render() {
205
- this.theme = Theme.resolve(this.options.theme);
206
- this.palette = new Palette(this.theme, this.#stream ? this.#seats : void 0);
207
- if (this.host) this.paint();
239
+ const { theme, chartStyle } = this.options;
240
+ this.theme = Theme.resolve(theme, chartStyle);
241
+ this.palette = new Palette(this.theme, this.#stream ? this.#seats : void 0, Theme.pins(this.theme, chartStyle), chartStyle?.opacity);
242
+ if (this.host) {
243
+ if (!this.#liveFrame) ColumnCheck.run(this.meta, this.defaults(), this.options);
244
+ this.paint();
245
+ }
208
246
  return this;
209
247
  }
210
248
  get size() {
211
- const box = this.host?.getBoundingClientRect();
249
+ const box = this.host?.getBoundingClientRect(), { aspectRatio } = this.options;
250
+ const width = this.options.width ?? (box?.width || 640);
212
251
  return {
213
- width: this.options.width ?? (box?.width || 640),
214
- height: this.options.height ?? (box?.height || 400)
252
+ width,
253
+ height: this.options.height ?? (aspectRatio && aspectRatio > 0 ? width / aspectRatio : box?.height || 400)
215
254
  };
216
255
  }
256
+ /** `chartStyle.fontSize`, the multiplier on all chart text. */
257
+ get fontScale() {
258
+ const scale = this.options.chartStyle?.fontSize;
259
+ return scale && scale > 0 && Number.isFinite(scale) ? scale : 1;
260
+ }
261
+ /** How much taller lines of text get: smaller text keeps the room it had. */
262
+ get textRoom() {
263
+ return Math.max(1, this.fontScale);
264
+ }
217
265
  get animated() {
218
266
  return this.motionAllowed && !this.liveSliding;
219
267
  }