@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,245 @@
1
+ import { ChartExporter } from "./ChartExporter.js";
2
+ import { ChartExportError, ExportErrorCode } from "./ChartExportError.js";
3
+ import { ExportText } from "./ExportText.js";
4
+ import { PdfOrientation, PdfPageSize } from "./PdfPage.js";
5
+ import { PDFDocument, PageSizes, StandardFonts, rgb } from "pdf-lib";
6
+ // src/core/PdfWriter.ts
7
+ var SIZES = {
8
+ [PdfPageSize.A4]: PageSizes.A4,
9
+ [PdfPageSize.Letter]: PageSizes.Letter
10
+ };
11
+ var MARGIN = 36;
12
+ var TITLE = 16;
13
+ var TEXT = 8;
14
+ var FOOTER = 7;
15
+ var ROW = 14;
16
+ var PADDING = 4;
17
+ var [MIN_COLUMN, MAX_COLUMN] = [44, 180];
18
+ /** Column widths come from this many rows, so a long table costs the same to measure. */
19
+ var MEASURED = 500;
20
+ /** Around 500 pages, where a viewer struggles and a spreadsheet does better. */
21
+ var MAX_ROWS = 25e3;
22
+ /** Canvas pixels per point, for text the built-in fonts have no glyph for. */
23
+ var SHARPNESS = 4;
24
+ var [ASCENT, DESCENT] = [1.05, .35];
25
+ var FALLBACK_FONT = "Helvetica, Arial, \"Noto Sans\", sans-serif";
26
+ var INK = rgb(.1, .1, .1);
27
+ var QUIET = rgb(.4, .4, .4);
28
+ var RULE = rgb(.8, .8, .8);
29
+ var BAND = rgb(.94, .94, .94);
30
+ /** Places a chart's PNG on a page and sets `table()` out as real text, one set of pages per block of columns that fits. */
31
+ var PdfWriter = class PdfWriter {
32
+ #doc;
33
+ #regular;
34
+ #bold;
35
+ #glyphs;
36
+ #pictures = /* @__PURE__ */ new Map();
37
+ #ruler = null;
38
+ #width;
39
+ #height;
40
+ constructor(doc, regular, bold, size) {
41
+ this.#doc = doc;
42
+ this.#regular = regular;
43
+ this.#bold = bold;
44
+ this.#glyphs = new Set(regular.getCharacterSet());
45
+ [this.#width, this.#height] = size;
46
+ }
47
+ static async write(chart, options = {}) {
48
+ const table = options.table ? ExportText.rows(chart, "A PDF data table") : null;
49
+ return PdfWriter.file(await chart.toPNG({ scale: options.scale ?? 3 }), table, options);
50
+ }
51
+ static async file(png, table, options) {
52
+ const { pageSize = PdfPageSize.A4, orientation = PdfOrientation.Auto, title } = options;
53
+ const size = SIZES[pageSize];
54
+ if (!size) throw new RangeError(`Unknown PDF page size ${pageSize}.`);
55
+ if (!Object.values(PdfOrientation).includes(orientation)) throw new RangeError(`Unknown PDF orientation ${orientation}.`);
56
+ if (table && table.rows.length > MAX_ROWS) throw new ChartExportError(ExportErrorCode.Unsupported, `A PDF table holds ${MAX_ROWS} rows and this one has ${table.rows.length}. Use Excel or CSV for the data.`);
57
+ const doc = await PDFDocument.create();
58
+ doc.setCreator("@mstone6969/graph");
59
+ doc.setProducer("pdf-lib");
60
+ if (title) doc.setTitle(title);
61
+ const chart = await doc.embedPng(await png.arrayBuffer());
62
+ const [narrow, wide] = size;
63
+ const landscape = orientation === PdfOrientation.Landscape || orientation === PdfOrientation.Auto && chart.width >= chart.height;
64
+ const writer = new PdfWriter(doc, await doc.embedFont(StandardFonts.Helvetica), await doc.embedFont(StandardFonts.HelveticaBold), landscape ? [wide, narrow] : [narrow, wide]);
65
+ await writer.#chartPage(chart, title);
66
+ if (table) await writer.#tablePages(table);
67
+ await writer.#numberPages();
68
+ return new Blob([await doc.save()], { type: "application/pdf" });
69
+ }
70
+ #page() {
71
+ return this.#doc.addPage([this.#width, this.#height]);
72
+ }
73
+ get #room() {
74
+ return this.#width - 72;
75
+ }
76
+ async #chartPage(chart, title) {
77
+ const page = this.#page();
78
+ let top = this.#height - MARGIN;
79
+ if (title) {
80
+ top -= TITLE;
81
+ await this.#text(page, title, MARGIN, top, this.#room, {
82
+ size: TITLE,
83
+ bold: true
84
+ });
85
+ top -= TITLE;
86
+ }
87
+ const fit = Math.min(this.#room / chart.width, (top - MARGIN) / chart.height);
88
+ const [width, height] = [chart.width * fit, chart.height * fit];
89
+ page.drawImage(chart, {
90
+ x: (this.#width - width) / 2,
91
+ y: top - height,
92
+ width,
93
+ height
94
+ });
95
+ }
96
+ async #tablePages({ columns, rows }) {
97
+ const texts = rows.map((row) => columns.map((_, c) => ExportText.of(row[c])));
98
+ const natural = columns.map((column, c) => {
99
+ const widest = texts.slice(0, MEASURED).reduce((most, row) => Math.max(most, this.#textWidth(PdfWriter.#clean(row[c]), TEXT)), this.#textWidth(PdfWriter.#clean(column), TEXT, true));
100
+ return Math.min(MAX_COLUMN, Math.max(MIN_COLUMN, widest + 8));
101
+ });
102
+ for (const block of this.#blocks(natural)) {
103
+ const used = block.reduce((sum, c) => sum + natural[c], 0);
104
+ const widths = block.map((c) => natural[c] * this.#room / used);
105
+ let page = null, y = 0;
106
+ for (const [r, row] of texts.entries()) {
107
+ if (!page || y < 50) {
108
+ page = this.#page();
109
+ y = this.#height - MARGIN - ROW;
110
+ page.drawRectangle({
111
+ x: MARGIN,
112
+ y: y - PADDING,
113
+ width: this.#room,
114
+ height: ROW,
115
+ color: BAND
116
+ });
117
+ await this.#row(page, block.map((c) => columns[c]), block.map((c) => typeof rows[0][c] === "number"), widths, y, true);
118
+ y -= ROW;
119
+ }
120
+ await this.#row(page, block.map((c) => row[c]), block.map((c) => typeof rows[r][c] === "number"), widths, y, false);
121
+ page.drawLine({
122
+ start: {
123
+ x: MARGIN,
124
+ y: y - PADDING
125
+ },
126
+ end: {
127
+ x: MARGIN + this.#room,
128
+ y: y - PADDING
129
+ },
130
+ thickness: .25,
131
+ color: RULE
132
+ });
133
+ y -= ROW;
134
+ }
135
+ }
136
+ }
137
+ /** Greedy runs of columns that fit a page. Later runs repeat the first column, so their rows can still be told apart. */
138
+ #blocks(widths) {
139
+ const blocks = [];
140
+ let used = Infinity;
141
+ widths.forEach((width, c) => {
142
+ if (used + width > this.#room) {
143
+ blocks.push(c ? [0, c] : [c]);
144
+ used = (c ? widths[0] : 0) + width;
145
+ } else {
146
+ blocks.at(-1).push(c);
147
+ used += width;
148
+ }
149
+ });
150
+ return blocks;
151
+ }
152
+ async #row(page, cells, right, widths, y, bold) {
153
+ let x = MARGIN;
154
+ for (const [c, cell] of cells.entries()) {
155
+ const room = widths[c] - 8;
156
+ await this.#text(page, cell, right[c] ? x + PADDING + room : x + PADDING, y, room, {
157
+ size: TEXT,
158
+ bold,
159
+ right: right[c]
160
+ });
161
+ x += widths[c];
162
+ }
163
+ }
164
+ async #numberPages() {
165
+ const pages = this.#doc.getPages();
166
+ if (pages.length < 2) return;
167
+ for (const [i, page] of pages.entries()) await this.#text(page, `Page ${i + 1} of ${pages.length}`, this.#width - MARGIN, MARGIN / 2, this.#room, {
168
+ size: FOOTER,
169
+ color: QUIET,
170
+ right: true
171
+ });
172
+ }
173
+ static #clean(text) {
174
+ return text.replace(/\s+/g, " ").trim();
175
+ }
176
+ #encodable(text) {
177
+ for (const char of text) if (!this.#glyphs.has(char.codePointAt(0))) return false;
178
+ return true;
179
+ }
180
+ #font(bold) {
181
+ return bold ? this.#bold : this.#regular;
182
+ }
183
+ #cssFont(px, bold) {
184
+ return `${bold ? "bold " : ""}${px}px ${FALLBACK_FONT}`;
185
+ }
186
+ #textWidth(text, size, bold) {
187
+ if (this.#encodable(text)) return this.#font(bold).widthOfTextAtSize(text, size);
188
+ this.#ruler ??= ChartExporter.paint(ChartExporter.canvas(1, 1));
189
+ this.#ruler.font = this.#cssFont(size * SHARPNESS, bold);
190
+ return this.#ruler.measureText(text).width / SHARPNESS;
191
+ }
192
+ /** Cuts by code point, so an ellipsis never lands inside a surrogate pair. */
193
+ #fit(text, room, size, bold) {
194
+ if (this.#textWidth(text, size, bold) <= room) return text;
195
+ const chars = [...text];
196
+ let [low, high] = [0, chars.length];
197
+ while (low < high) {
198
+ const middle = Math.ceil((low + high) / 2);
199
+ if (this.#textWidth(`${chars.slice(0, middle).join("")}…`, size, bold) <= room) low = middle;
200
+ else high = middle - 1;
201
+ }
202
+ return `${chars.slice(0, low).join("").trimEnd()}…`;
203
+ }
204
+ /** The 14 built-in fonts cover Western European text only. Anything else is drawn as a small picture, so it shows but cannot be selected. */
205
+ async #text(page, text, x, y, room, style) {
206
+ const { size, bold, color = INK, right } = style;
207
+ const clean = PdfWriter.#clean(text);
208
+ if (!clean) return;
209
+ const shown = this.#fit(clean, room, size, bold);
210
+ const width = this.#textWidth(shown, size, bold);
211
+ const left = right ? x - width : x;
212
+ if (this.#encodable(shown)) {
213
+ page.drawText(shown, {
214
+ x: left,
215
+ y,
216
+ size,
217
+ font: this.#font(bold),
218
+ color
219
+ });
220
+ return;
221
+ }
222
+ const picture = await this.#picture(shown, width, size, bold);
223
+ page.drawImage(picture, {
224
+ x: left,
225
+ y: y - size * DESCENT,
226
+ width: picture.width / SHARPNESS,
227
+ height: picture.height / SHARPNESS
228
+ });
229
+ }
230
+ async #picture(text, width, size, bold) {
231
+ const key = `${size}|${bold}|${text}`;
232
+ const known = this.#pictures.get(key);
233
+ if (known) return known;
234
+ const canvas = ChartExporter.canvas(Math.ceil(width * SHARPNESS), Math.ceil(size * (ASCENT + DESCENT) * SHARPNESS));
235
+ const paint = ChartExporter.paint(canvas);
236
+ paint.font = this.#cssFont(size * SHARPNESS, bold);
237
+ paint.fillStyle = "#1a1a1a";
238
+ paint.textBaseline = "alphabetic";
239
+ paint.fillText(text, 0, size * ASCENT * SHARPNESS);
240
+ const picture = await this.#doc.embedPng(await (await ChartExporter.png(canvas)).arrayBuffer());
241
+ this.#pictures.set(key, picture);
242
+ return picture;
243
+ }
244
+ };
245
+ export { PdfWriter };
@@ -1,5 +1,6 @@
1
+ import { AxisWindow } from "./AxisWindow.js";
1
2
  import { CurveKind, ScaleKind, SymbolKind } from "./enums.js";
2
- import { curveBasis, curveBumpX, curveCardinal, curveCatmullRom, curveLinear, curveMonotoneX, curveNatural, curveStep, curveStepAfter, curveStepBefore, scaleLinear, scaleLog, scalePow, scaleSqrt, scaleSymlog, symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye } from "d3";
3
+ import { curveBasis, curveBumpX, curveCardinal, curveCatmullRom, curveLinear, curveMonotoneX, curveNatural, curveStep, curveStepAfter, curveStepBefore, interpolateNumber, scaleLinear, scaleLog, scalePow, scaleSqrt, scaleSymlog, symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye } from "d3";
3
4
  // src/core/Scales.ts
4
5
  var CURVES = {
5
6
  [CurveKind.Linear]: curveLinear,
@@ -23,7 +24,7 @@ var SYMBOLS = {
23
24
  [SymbolKind.Wye]: symbolWye
24
25
  };
25
26
  /** Maps library enums to d3 factories. */
26
- var Scales = class {
27
+ var Scales = class Scales {
27
28
  static curve(kind = CurveKind.Linear) {
28
29
  return CURVES[kind];
29
30
  }
@@ -39,6 +40,50 @@ var Scales = class {
39
40
  default: return scaleLinear();
40
41
  }
41
42
  }
43
+ /**
44
+ * Turns a built continuous scale into `kind` in place, because subclasses close over the scales
45
+ * they build. Says if it did: dates, bands and piecewise domains stay as they are, and so does `Log`
46
+ * over a value at or below 0. `floor`, the smallest value plotted, lets `Log` start a zero-based domain.
47
+ */
48
+ static retype(scale, kind, floor = null) {
49
+ const domain = scale.domain?.();
50
+ if (AxisWindow.kind(scale) !== "continuous" || !scale.interpolate || domain?.length !== 2 || domain.some((value) => value instanceof Date)) return false;
51
+ if (kind === ScaleKind.Log) {
52
+ const ends = domain, low = ends[0] <= ends[1] ? 0 : 1;
53
+ if (ends[1 - low] <= 0 || ends[low] <= 0 && floor === null) return false;
54
+ if (ends[low] <= 0) {
55
+ ends[low] = 10 ** Math.floor(Math.log10(floor));
56
+ scale.domain(ends);
57
+ }
58
+ }
59
+ Scales.#adopt(scale, kind, scale.copy().interpolate(interpolateNumber).range([0, 1]).clamp(false));
60
+ return true;
61
+ }
62
+ /** `reader` undoes the scale's own normalising. What the scale's new shape cannot place lands on the low end, never on NaN. */
63
+ static #adopt(scale, kind, reader) {
64
+ const shape = Scales.continuous(kind).range([0, 1]);
65
+ const synced = () => shape.domain(scale.domain());
66
+ const { copy } = scale;
67
+ scale.interpolate((from, to) => {
68
+ synced();
69
+ return (t) => {
70
+ const at = shape(reader.domain(scale.domain()).invert(t));
71
+ return from + (to - from) * (Number.isFinite(at) ? at : 0);
72
+ };
73
+ });
74
+ scale.shows = (value) => synced()(value);
75
+ scale.invert = (at) => {
76
+ const [from, to] = scale.range();
77
+ return synced().invert((at - from) / (to - from));
78
+ };
79
+ scale.tickFormat = (...args) => synced().tickFormat(...args);
80
+ scale.ticks = (...args) => kind === ScaleKind.Log ? decades(synced().ticks(...args)) : synced().ticks(...args);
81
+ scale.copy = () => {
82
+ const twin = copy.call(scale);
83
+ Scales.#adopt(twin, kind, reader.copy());
84
+ return twin;
85
+ };
86
+ }
42
87
  /** Clean axis ticks: full below 10,000, compact above. */
43
88
  static format(value, reach) {
44
89
  if (reach >= 1e4) return Intl.NumberFormat("en", {
@@ -48,4 +93,9 @@ var Scales = class {
48
93
  return value.toLocaleString("en", { maximumFractionDigits: reach < 10 ? 2 : 0 });
49
94
  }
50
95
  };
96
+ /** A log scale offers a tick per mantissa, far more than an axis can label: keep powers of ten while there are at least two. */
97
+ function decades(ticks) {
98
+ const tens = ticks.filter((tick) => Math.abs(Math.log10(tick) % 1) < 1e-9);
99
+ return tens.length >= 2 ? tens : ticks.filter((_, i) => i % Math.ceil(ticks.length / 6) === 0);
100
+ }
51
101
  export { Scales };
@@ -31,7 +31,7 @@ var SceneOverlay = class SceneOverlay {
31
31
  paint.fillText(element.textContent, (at.x + 1) / 2 * width - center.x * box.width, (1 - at.y) / 2 * height + (.5 - center.y) * parseFloat(element.style.fontSize || "11") * LINE_HEIGHT);
32
32
  });
33
33
  }
34
- static #header(paint, { width }, theme, { title, subtitle, items }) {
34
+ static #header(paint, { width }, theme, { title, subtitle, items, fontScale = 1 }) {
35
35
  let y = INSET.y;
36
36
  const line = (text, font, color, size) => {
37
37
  paint.font = font;
@@ -39,9 +39,9 @@ var SceneOverlay = class SceneOverlay {
39
39
  paint.fillText(text, INSET.x, y + size * LINE_HEIGHT / 2);
40
40
  y += size * LINE_HEIGHT;
41
41
  };
42
- if (title) line(title, `600 15px ${theme.fontFamily}`, theme.ink, 15);
43
- if (subtitle) line(subtitle, `12px ${theme.fontFamily}`, theme.inkSecondary, 12);
44
- paint.font = `12px ${theme.fontFamily}`;
42
+ if (title) line(title, `600 ${15 * fontScale}px ${theme.fontFamily}`, theme.ink, 15 * fontScale);
43
+ if (subtitle) line(subtitle, `${12 * fontScale}px ${theme.fontFamily}`, theme.inkSecondary, 12 * fontScale);
44
+ paint.font = `${12 * fontScale}px ${theme.fontFamily}`;
45
45
  let x = INSET.x;
46
46
  y += items.length ? 13 : 0;
47
47
  for (const { key, label, color } of items) {
@@ -0,0 +1,37 @@
1
+ // src/core/TextFit.ts
2
+ var ELLIPSIS = "…";
3
+ /** Weights from glyph widths measured in the theme's system font, where one unit is an average Latin character. */
4
+ var ELLIPSIS_WEIGHT = 1.5;
5
+ var THIN = /^[fijlrt.,:;'|!()[\] ]$/;
6
+ var BROAD = /^[A-Zmw@%]$/;
7
+ /** One grapheme drawn about a full em wide: CJK, kana, hangul, full-width forms and emoji. */
8
+ var WIDE = /\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Hangul}|[ -〿!-⦆¢-₩]|\p{Emoji_Presentation}|️/u;
9
+ var ZERO = /^[\p{M}\p{Cf}]+$/u;
10
+ var segmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
11
+ var graphemes = (text) => segmenter ? Array.from(segmenter.segment(text), (part) => part.segment) : Array.from(text);
12
+ var cells = (grapheme) => ZERO.test(grapheme) ? 0 : WIDE.test(grapheme) ? 2 : 1;
13
+ /** A closer estimate for deciding on a cut, so all-caps text stops in time and text of thin letters is left whole. */
14
+ var weight = (grapheme) => THIN.test(grapheme) ? .6 : BROAD.test(grapheme) ? 1.3 : cells(grapheme) === 1 ? 1.05 : cells(grapheme);
15
+ /** Estimated text width and truncation, without measuring. `char` is the px width of an average Latin character. */
16
+ var TextFit = class {
17
+ /** Width of `text` in px. Wide scripts count double, combining marks count nothing. */
18
+ static width(text, char) {
19
+ return graphemes(text).reduce((sum, grapheme) => sum + cells(grapheme), 0) * char;
20
+ }
21
+ /** `text` cut to `room` px with an ellipsis. Whole graphemes only, so an emoji or accented letter never splits. */
22
+ static fit(text, room, char) {
23
+ const parts = graphemes(text), limit = room / char;
24
+ if (parts.reduce((sum, part) => sum + weight(part), 0) <= limit) return text;
25
+ let used = ELLIPSIS_WEIGHT, keep = 0;
26
+ while (keep < parts.length && used + weight(parts[keep]) <= limit) used += weight(parts[keep++]);
27
+ return `${parts.slice(0, keep).join("").trimEnd()}${ELLIPSIS}`;
28
+ }
29
+ /** Adds a `<title>` child holding the full text, or removes it when `full` gives nothing. */
30
+ static hint(selection, full) {
31
+ selection.selectAll("title").data((datum) => {
32
+ const text = full(datum);
33
+ return text ? [text] : [];
34
+ }).join("title").text((text) => text);
35
+ }
36
+ };
37
+ export { TextFit };
@@ -1,4 +1,5 @@
1
1
  import { ThemeMode } from "./enums.js";
2
+ import { hsl } from "d3";
2
3
  // src/core/Theme.ts
3
4
  var SEQUENTIAL = [
4
5
  "#cde2fb",
@@ -66,10 +67,36 @@ var Theme = class Theme {
66
67
  ...STATUS,
67
68
  fontFamily: FONT
68
69
  };
69
- static resolve(theme = ThemeMode.Auto) {
70
+ static resolve(theme = ThemeMode.Auto, style) {
71
+ const tokens = Theme.#mode(theme);
72
+ return style ? Theme.#styled(tokens, style) : tokens;
73
+ }
74
+ static #mode(theme) {
70
75
  if (typeof theme === "object") return theme;
71
76
  if (theme === ThemeMode.Auto) return globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ? Theme.dark : Theme.light;
72
77
  return theme === ThemeMode.Dark ? Theme.dark : Theme.light;
73
78
  }
79
+ /** A style colour as a CSS colour: token names such as `good` follow the theme's mode. */
80
+ static #paint(tokens, value) {
81
+ const token = tokens[value];
82
+ return typeof token === "string" ? token : value;
83
+ }
84
+ /** Slots past a short `colors` hold `inkMuted`, the colour `Palette` folds into Other, so nothing reads an empty slot. */
85
+ static #styled(tokens, { colors, sequential, diverging, fontFamily }) {
86
+ const paint = (value) => Theme.#paint(tokens, value);
87
+ const stops = sequential && sequential.length >= 2 ? sequential.map(paint) : null;
88
+ if (!colors?.length && !stops && !diverging && !fontFamily) return tokens;
89
+ const styled = { ...tokens };
90
+ if (colors?.length) styled.categorical = Array.from({ length: Math.max(colors.length, tokens.categorical.length) }, (_, i) => colors[i] === void 0 ? tokens.inkMuted : paint(colors[i]));
91
+ if (stops) styled.sequential = hsl(tokens.surface).l < .5 ? stops.reverse() : stops;
92
+ if (diverging) [styled.divergingLow, styled.divergingMid, styled.divergingHigh] = diverging.map(paint);
93
+ if (fontFamily) styled.fontFamily = fontFamily;
94
+ return styled;
95
+ }
96
+ /** `seriesColors` resolved against `tokens`, for `Palette`. */
97
+ static pins(tokens, style) {
98
+ const pinned = style?.seriesColors;
99
+ return pinned && Object.fromEntries(Object.entries(pinned).map(([key, value]) => [key, Theme.#paint(tokens, value)]));
100
+ }
74
101
  };
75
102
  export { Theme };
@@ -104,4 +104,36 @@ var ColorScale = /* @__PURE__ */ function(ColorScale) {
104
104
  ColorScale["Categorical"] = "categorical";
105
105
  return ColorScale;
106
106
  }({});
107
- export { ChartFamily, ColorScale, CurveKind, DataShape, Dimension, LegendPosition, Orientation, ScaleKind, StackMode, SymbolKind, ThemeMode };
107
+ /** What draws a chart's marks. */
108
+ var RenderType = /* @__PURE__ */ function(RenderType) {
109
+ RenderType["Svg"] = "svg";
110
+ RenderType["Canvas"] = "canvas";
111
+ RenderType["WebGL"] = "webgl";
112
+ return RenderType;
113
+ }({});
114
+ var StrokeDash = /* @__PURE__ */ function(StrokeDash) {
115
+ StrokeDash["Solid"] = "solid";
116
+ StrokeDash["Dashed"] = "dashed";
117
+ StrokeDash["Dotted"] = "dotted";
118
+ return StrokeDash;
119
+ }({});
120
+ var StrokeCap = /* @__PURE__ */ function(StrokeCap) {
121
+ StrokeCap["Butt"] = "butt";
122
+ StrokeCap["Round"] = "round";
123
+ StrokeCap["Square"] = "square";
124
+ return StrokeCap;
125
+ }({});
126
+ var StrokeJoin = /* @__PURE__ */ function(StrokeJoin) {
127
+ StrokeJoin["Miter"] = "miter";
128
+ StrokeJoin["Round"] = "round";
129
+ StrokeJoin["Bevel"] = "bevel";
130
+ return StrokeJoin;
131
+ }({});
132
+ /** `shape-rendering` on marks: `Crisp` snaps edges to pixels, `Precise` favours exact geometry. */
133
+ var EdgeRendering = /* @__PURE__ */ function(EdgeRendering) {
134
+ EdgeRendering["Auto"] = "auto";
135
+ EdgeRendering["Crisp"] = "crisp";
136
+ EdgeRendering["Precise"] = "precise";
137
+ return EdgeRendering;
138
+ }({});
139
+ export { ChartFamily, ColorScale, CurveKind, DataShape, Dimension, EdgeRendering, LegendPosition, Orientation, RenderType, ScaleKind, StackMode, StrokeCap, StrokeDash, StrokeJoin, SymbolKind, ThemeMode };
@@ -0,0 +1,15 @@
1
+ import { interpolateString } from "d3";
2
+ // src/core/pathMorph.ts
3
+ var COMMAND = /([A-Za-df-z])([^A-Za-df-z]*)/g;
4
+ var NUMBER = /[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
5
+ /** Commands and arc flags, with every other number gone. Flags take 0 or 1 only, so two paths tween if and only if these match. */
6
+ var skeleton = (d) => [...d.matchAll(COMMAND)].map(([, command, args]) => {
7
+ const numbers = args.match(NUMBER) ?? [];
8
+ const arc = command === "A" || command === "a";
9
+ return `${command}${numbers.length}${arc ? numbers.filter((_, i) => i % 7 === 3 || i % 7 === 4).join("") : ""}`;
10
+ }).join();
11
+ /** An attribute tween for `d`. A path with arcs snaps unless its start has the same skeleton; all other paths tween as plain strings. */
12
+ function pathMorph(from, to) {
13
+ return /a/i.test(to) && skeleton(from) !== skeleton(to) ? () => to : interpolateString(from, to);
14
+ }
15
+ export { pathMorph };
@@ -1,6 +1,4 @@
1
- import countries_110m_default from "../vendor/world-atlas/countries-110m.json.js";
2
1
  import { randomLcg, randomNormal, randomUniform } from "d3";
3
- import { feature } from "topojson-client";
4
2
  // src/data/SampleData.ts
5
3
  var NAMES = [
6
4
  "North",
@@ -28,6 +26,7 @@ var SERIES = [
28
26
  ];
29
27
  /** Deterministic demo datasets: same seed, same data. All gallery and test data comes from here. */
30
28
  var SampleData = class {
29
+ /** Seeded generators behind every sample. */
31
30
  static rng(seed) {
32
31
  const source = randomLcg(seed * .6180339887 % 1);
33
32
  return {
@@ -193,21 +192,6 @@ var SampleData = class {
193
192
  values
194
193
  };
195
194
  }
196
- /** Lower-48-free world countries as GeoJSON features; `properties.value` is seeded. */
197
- static world(seed = 1) {
198
- const { u } = this.rng(seed);
199
- const geo = feature(countries_110m_default, countries_110m_default.objects.countries);
200
- return {
201
- ...geo,
202
- features: geo.features.map((f) => ({
203
- ...f,
204
- properties: {
205
- ...f.properties,
206
- value: +(u() * 100).toFixed(1)
207
- }
208
- }))
209
- };
210
- }
211
195
  /** `{ name, lon, lat, value }`, world cities. */
212
196
  static cities(seed = 1) {
213
197
  const { u } = this.rng(seed);
@@ -0,0 +1,23 @@
1
+ import countries_110m_default from "../vendor/world-atlas/countries-110m.json.js";
2
+ import { SampleData } from "./SampleData.js";
3
+ import { feature } from "topojson-client";
4
+ // src/data/WorldSample.ts
5
+ /** The world atlas as sample data, apart from `SampleData` so charts without a map do not carry it. */
6
+ var WorldSample = class {
7
+ /** World countries as GeoJSON features; `properties.value` is seeded. */
8
+ static countries(seed = 1) {
9
+ const { u } = SampleData.rng(seed);
10
+ const geo = feature(countries_110m_default, countries_110m_default.objects.countries);
11
+ return {
12
+ ...geo,
13
+ features: geo.features.map((f) => ({
14
+ ...f,
15
+ properties: {
16
+ ...f.properties,
17
+ value: +(u() * 100).toFixed(1)
18
+ }
19
+ }))
20
+ };
21
+ }
22
+ };
23
+ export { WorldSample };