@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.
- package/README.md +111 -14
- package/dist/ExcelWriter-3ZSYUH4T.cjs +154 -0
- package/dist/GraphType.js +24 -38
- package/dist/PdfWriter-6HJFYMCO.cjs +205 -0
- package/dist/catalog-YWX6FHRM.cjs +8 -0
- package/dist/catalog.js +5 -1
- package/dist/charts/bars/index.js +2 -12
- package/dist/charts/comparison/EffectForestPlot.js +1 -1
- package/dist/charts/comparison/index.js +1 -8
- package/dist/charts/demography/Nomogram.js +3 -3
- package/dist/charts/density/HexbinChart.js +6 -4
- package/dist/charts/diagrams/ArchitectureCanvas.js +68 -0
- package/dist/charts/diagrams/ArchitectureDiagram.js +331 -0
- package/dist/charts/diagrams/ArchitectureLayout.js +106 -0
- package/dist/charts/diagrams/ArchitectureLinkLabels.js +78 -0
- package/dist/charts/diagrams/ArchitectureModel.js +108 -0
- package/dist/charts/diagrams/ArchitectureRouter.js +348 -0
- package/dist/charts/diagrams/ArchitectureSamples.js +312 -0
- package/dist/charts/diagrams/ArchitectureShapes.js +130 -0
- package/dist/charts/diagrams/ArchitectureText.js +25 -0
- package/dist/charts/diagrams/DiagramText.js +38 -0
- package/dist/charts/diagrams/EntityRelationshipDiagram.js +465 -0
- package/dist/charts/diagrams/ErCrowsFoot.js +33 -0
- package/dist/charts/diagrams/ErSamples.js +205 -0
- package/dist/charts/diagrams/ErSchema.js +122 -0
- package/dist/charts/diagrams/LogicCircuit.js +154 -0
- package/dist/charts/diagrams/LogicCircuitLayout.js +600 -0
- package/dist/charts/diagrams/LogicCircuitSamples.js +66 -0
- package/dist/charts/diagrams/LogicGateDiagram.js +216 -0
- package/dist/charts/diagrams/LogicGateSymbol.js +113 -0
- package/dist/charts/diagrams/NetworkTopologyDiagram.js +247 -0
- package/dist/charts/diagrams/SequenceDiagram.js +351 -0
- package/dist/charts/diagrams/SequenceLayout.js +422 -0
- package/dist/charts/diagrams/SequenceModel.js +221 -0
- package/dist/charts/diagrams/TopologyLayout.js +143 -0
- package/dist/charts/diagrams/architectureEnums.js +50 -0
- package/dist/charts/diagrams/index.js +26 -0
- package/dist/charts/diagrams/logicEnums.js +42 -0
- package/dist/charts/diagrams/sequenceEnums.js +40 -0
- package/dist/charts/distribution/HistogramChart.js +25 -3
- package/dist/charts/distribution/index.js +1 -3
- package/dist/charts/earthscience/HovmollerDiagram.js +477 -0
- package/dist/charts/earthscience/HovmollerEnums.js +42 -0
- package/dist/charts/earthscience/HovmollerField.js +180 -0
- package/dist/charts/earthscience/HovmollerSample.js +40 -0
- package/dist/charts/earthscience/HovmollerSpeed.js +71 -0
- package/dist/charts/earthscience/index.js +10 -2
- package/dist/charts/engineering/ConstellationDiagram.js +26 -6
- package/dist/charts/engineering/EyeDiagram.js +2 -1
- package/dist/charts/engineering/PoleZeroPlot.js +32 -8
- package/dist/charts/financial/RelativeStrengthRotation.js +434 -0
- package/dist/charts/financial/RelativeStrengthRotationMath.js +71 -0
- package/dist/charts/financial/RelativeStrengthRotationSamples.js +27 -0
- package/dist/charts/financial/index.js +5 -3
- package/dist/charts/flow/index.js +1 -5
- package/dist/charts/frameworks/AnsoffMatrix.js +523 -0
- package/dist/charts/frameworks/FrameworkText.js +98 -0
- package/dist/charts/frameworks/NpsGauge.js +279 -0
- package/dist/charts/frameworks/NpsSurvey.js +58 -0
- package/dist/charts/frameworks/PerceptualMap.js +419 -0
- package/dist/charts/frameworks/PerceptualProjection.js +142 -0
- package/dist/charts/frameworks/PositioningSamples.js +191 -0
- package/dist/charts/frameworks/RaciMatrix.js +458 -0
- package/dist/charts/frameworks/RaciPlan.js +81 -0
- package/dist/charts/frameworks/RaciSamples.js +76 -0
- package/dist/charts/frameworks/SwotMatrix.js +358 -0
- package/dist/charts/frameworks/SwotSamples.js +85 -0
- package/dist/charts/frameworks/VanWestendorpChart.js +187 -0
- package/dist/charts/frameworks/VanWestendorpSurvey.js +197 -0
- package/dist/charts/frameworks/index.js +26 -0
- package/dist/charts/frameworks/raciEnums.js +30 -0
- package/dist/charts/frameworks/swotEnums.js +34 -0
- package/dist/charts/genomics/CircosPlot.js +1 -1
- package/dist/charts/geoflows/GeoFlowChart.js +2 -2
- package/dist/charts/geoflows/GeoFlowSample.js +2 -1
- package/dist/charts/geomaps/FloorPlanMap.js +324 -0
- package/dist/charts/geomaps/FloorPlanModel.js +179 -0
- package/dist/charts/geomaps/FloorPlanSamples.js +416 -0
- package/dist/charts/geomaps/GeoLand.js +2 -2
- package/dist/charts/geomaps/GeoSamples.js +2 -1
- package/dist/charts/geomaps/GraticuleMap.js +2 -2
- package/dist/charts/geomaps/RoomLabelFit.js +200 -0
- package/dist/charts/geomaps/TissotMap.js +2 -2
- package/dist/charts/geomaps/index.js +8 -4
- package/dist/charts/hierarchy/SunburstChart.js +17 -10
- package/dist/charts/hierarchy/index.js +2 -11
- package/dist/charts/indicators/BulletChart.js +8 -2
- package/dist/charts/indicators/SpeedometerChart.js +1 -1
- package/dist/charts/indicators/ThermometerChart.js +1 -1
- package/dist/charts/lifescience/LoessFit.js +113 -0
- package/dist/charts/lifescience/MaPlot.js +337 -0
- package/dist/charts/lifescience/MaPlotSamples.js +35 -0
- package/dist/charts/lifescience/OncoGlyph.js +137 -0
- package/dist/charts/lifescience/OncoPrint.js +373 -0
- package/dist/charts/lifescience/OncoPrintMatrix.js +107 -0
- package/dist/charts/lifescience/OncoPrintSamples.js +142 -0
- package/dist/charts/lifescience/PedigreeChart.js +306 -0
- package/dist/charts/lifescience/PedigreeLayout.js +261 -0
- package/dist/charts/lifescience/PedigreeRows.js +70 -0
- package/dist/charts/lifescience/PedigreeSamples.js +161 -0
- package/dist/charts/lifescience/PedigreeSymbol.js +47 -0
- package/dist/charts/lifescience/RamachandranPlot.js +364 -0
- package/dist/charts/lifescience/RamachandranRegions.js +2712 -0
- package/dist/charts/lifescience/RamachandranSamples.js +235 -0
- package/dist/charts/lifescience/index.js +25 -0
- package/dist/charts/lifescience/pedigreeEnums.js +21 -0
- package/dist/charts/lifescience/ramachandranEnums.js +34 -0
- package/dist/charts/linesareas/DualAxisChart.js +1 -1
- package/dist/charts/linesareas/LineEnums.js +1 -15
- package/dist/charts/linesareas/MultiLineChart.js +1 -1
- package/dist/charts/linesareas/TrendChart.js +15 -5
- package/dist/charts/linesareas/index.js +2 -6
- package/dist/charts/massive/MassiveChart.js +3 -2
- package/dist/charts/mathematical/FourierSeriesChart.js +7 -1
- package/dist/charts/mathematical/index.js +2 -6
- package/dist/charts/matrix/GridPlot.js +27 -2
- package/dist/charts/matrix/TilegramChart.js +2 -2
- package/dist/charts/mlcharts/AttentionGrid.js +79 -0
- package/dist/charts/mlcharts/AttentionHeatmap.js +329 -0
- package/dist/charts/mlcharts/AttentionModel.js +132 -0
- package/dist/charts/mlcharts/AttentionSamples.js +82 -0
- package/dist/charts/mlcharts/CellNumber.js +8 -0
- package/dist/charts/mlcharts/CurveDiagnosticSamples.js +95 -0
- package/dist/charts/mlcharts/CurvePickMark.js +31 -0
- package/dist/charts/mlcharts/DecisionBoundaryChart.js +316 -0
- package/dist/charts/mlcharts/DecisionSamples.js +60 -0
- package/dist/charts/mlcharts/DecisionSurface.js +130 -0
- package/dist/charts/mlcharts/DistinctTicks.js +13 -0
- package/dist/charts/mlcharts/ElbowMath.js +153 -0
- package/dist/charts/mlcharts/ElbowPlot.js +273 -0
- package/dist/charts/mlcharts/ExplainabilitySamples.js +70 -0
- package/dist/charts/mlcharts/FeatureImportanceChart.js +150 -0
- package/dist/charts/mlcharts/ImportanceRanking.js +98 -0
- package/dist/charts/mlcharts/LearningCurveChart.js +276 -0
- package/dist/charts/mlcharts/LearningCurveMath.js +92 -0
- package/dist/charts/mlcharts/PartialDependence.js +92 -0
- package/dist/charts/mlcharts/PartialDependencePlot.js +225 -0
- package/dist/charts/mlcharts/WideLabel.js +28 -0
- package/dist/charts/mlcharts/curveDiagnosticEnums.js +37 -0
- package/dist/charts/mlcharts/index.js +32 -0
- package/dist/charts/proportion/index.js +5 -9
- package/dist/charts/scatter/CategoryScatterBase.js +1 -4
- package/dist/charts/scatter/ConnectedScatter.js +7 -4
- package/dist/charts/scatter/MarginalScatter.js +13 -8
- package/dist/charts/scatter/index.js +4 -17
- package/dist/charts/scientific/SpectrogramChart.js +9 -3
- package/dist/charts/scientific/index.js +2 -2
- package/dist/charts/signals/index.js +3 -13
- package/dist/charts/statistics/LorenzCurve.js +7 -4
- package/dist/charts/threebars/Candlestick3DChart.js +18 -2
- package/dist/charts/threeglobe/GlobeChart.js +2 -2
- package/dist/charts/threeglobe/GlobeSamples.js +2 -1
- package/dist/charts/threeglobe/RingGlobe.js +1 -1
- package/dist/charts/threenetwork/ConeTreeChart.js +2 -1
- package/dist/charts/threenetwork/Gantt3DChart.js +3 -2
- package/dist/charts/threesurfaces/Ridgeline3DChart.js +22 -4
- package/dist/charts/threevolume/LorenzAttractor.js +19 -0
- package/dist/charts/threevolume/VolumeRaycastChart.js +4 -3
- package/dist/charts/threevolume/volumeEnums.js +1 -0
- package/dist/charts/timeline/index.js +2 -10
- package/dist/chunk-753UBLV2.cjs +33 -0
- package/dist/chunk-VX54A7KA.cjs +17 -0
- package/dist/{chunk-W5GAAAZY.cjs → chunk-WAODDPYX.cjs} +24874 -17620
- package/dist/chunk-Z3JPIJSC.cjs +338 -0
- package/dist/core/CartesianChart.js +87 -11
- package/dist/core/Chart.js +63 -15
- package/dist/core/Chart2D.js +54 -17
- package/dist/core/Chart3D.js +56 -20
- package/dist/core/ChartExportError.js +2 -0
- package/dist/core/ChartExporter.js +50 -8
- package/dist/core/ColumnCheck.js +53 -0
- package/dist/core/Context2D.js +29 -15
- package/dist/core/Context3D.js +7 -2
- package/dist/core/ExcelWriter.js +152 -0
- package/dist/core/ExportText.js +28 -0
- package/dist/core/MarkOrigin.js +88 -0
- package/dist/core/MarkStyle.js +294 -0
- package/dist/core/Palette.js +27 -7
- package/dist/core/PdfPage.js +13 -0
- package/dist/core/PdfWriter.js +245 -0
- package/dist/core/Scales.js +52 -2
- package/dist/core/SceneOverlay.js +4 -4
- package/dist/core/TextFit.js +37 -0
- package/dist/core/Theme.js +28 -1
- package/dist/core/enums.js +33 -1
- package/dist/core/pathMorph.js +15 -0
- package/dist/data/SampleData.js +1 -17
- package/dist/data/WorldSample.js +23 -0
- package/dist/index.cjs +810 -691
- package/dist/index.d.cts +8390 -2925
- package/dist/index.d.ts +8392 -2927
- package/dist/index.js +106 -56
- package/dist/react/ChartView.js +18 -8
- package/dist/react/GraphHandle.js +2 -0
- package/dist/react/components.js +74 -116
- package/package.json +13 -3
- package/dist/catalog-4UP2YFNR.cjs +0 -7
- package/dist/charts/bars/BulletBar.js +0 -184
- package/dist/charts/bars/ErrorBarColumn.js +0 -147
- package/dist/charts/bars/HistogramBar.js +0 -217
- package/dist/charts/bars/MarimekkoBar.js +0 -191
- package/dist/charts/bars/ParetoBar.js +0 -143
- package/dist/charts/comparison/PanelChart.js +0 -70
- package/dist/charts/comparison/SmallMultipleBars.js +0 -134
- package/dist/charts/comparison/SpanChart.js +0 -118
- package/dist/charts/comparison/TrellisChart.js +0 -158
- package/dist/charts/distribution/RidgeSlabs3DChart.js +0 -169
- package/dist/charts/financial/Candlestick3D.js +0 -157
- package/dist/charts/flow/BumpFlow.js +0 -212
- package/dist/charts/flow/StreamFlow.js +0 -174
- package/dist/charts/geomaps/ThiessenMap.js +0 -106
- package/dist/charts/hierarchy/ConeTree3D.js +0 -135
- package/dist/charts/hierarchy/Hierarchy3DChart.js +0 -27
- package/dist/charts/hierarchy/SpherePack3D.js +0 -81
- package/dist/charts/hierarchy/SunburstTower3D.js +0 -93
- package/dist/charts/hierarchy/TreemapSkyline3D.js +0 -98
- package/dist/charts/linesareas/ConnectedScatterTime.js +0 -206
- package/dist/charts/linesareas/SmallMultipleLines.js +0 -153
- package/dist/charts/mathematical/ParametricSurface3D.js +0 -234
- package/dist/charts/mathematical/StrangeAttractor3D.js +0 -251
- package/dist/charts/proportion/ConeFunnel.js +0 -126
- package/dist/charts/proportion/ExtrudedPieChart.js +0 -123
- package/dist/charts/scatter/BeeswarmScatter.js +0 -151
- package/dist/charts/scatter/BinnedScatterBase.js +0 -51
- package/dist/charts/scatter/ContourScatter.js +0 -116
- package/dist/charts/scatter/GridBinScatter.js +0 -98
- package/dist/charts/scatter/HexbinScatter.js +0 -74
- package/dist/charts/scatter/StripScatter.js +0 -107
- package/dist/charts/scatter/SunflowerScatter.js +0 -109
- package/dist/charts/signals/ConstellationChart.js +0 -153
- package/dist/charts/signals/CorrelogramChart.js +0 -171
- package/dist/charts/signals/EyeDiagramChart.js +0 -155
- package/dist/charts/signals/PoleZeroChart.js +0 -164
- package/dist/charts/signals/SpectrogramChart.js +0 -79
- package/dist/charts/timeline/CalendarSkylineChart.js +0 -128
- package/dist/charts/timeline/ScheduleBlocksChart.js +0 -119
- package/dist/charts/timeline/SpiralTimeline.js +0 -154
- 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
|
|
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
|
|
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(
|
|
112
|
-
const fx = this.#view.legible(x, this.tickFormat(x), xCount), fy = this.#view.legible(y, this.tickFormat(y),
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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 };
|
package/dist/core/Chart.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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,
|
|
194
|
-
ChartExporter.save(await this.#file(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,
|
|
197
|
-
if (format === ExportFormat.PNG) return this.toPNG(
|
|
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
|
-
|
|
206
|
-
this.
|
|
207
|
-
|
|
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
|
|
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
|
}
|