@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,152 @@
|
|
|
1
|
+
import { ChartExporter } from "./ChartExporter.js";
|
|
2
|
+
import { ChartExportError, ExportErrorCode } from "./ChartExportError.js";
|
|
3
|
+
import { ExportText } from "./ExportText.js";
|
|
4
|
+
import { strToU8, zipSync } from "fflate";
|
|
5
|
+
// src/core/ExcelWriter.ts
|
|
6
|
+
var TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
7
|
+
var HEAD = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
|
|
8
|
+
var MAIN = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
9
|
+
var PACKAGE = "http://schemas.openxmlformats.org/package/2006";
|
|
10
|
+
var LINKS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
11
|
+
var CONTENT = "application/vnd.openxmlformats-officedocument";
|
|
12
|
+
var MAX_COLUMNS = 16384;
|
|
13
|
+
var MAX_ROWS = 1048576;
|
|
14
|
+
var MAX_TEXT = 32767;
|
|
15
|
+
var MAX_SERIAL = 2958465;
|
|
16
|
+
var DAY = 864e5;
|
|
17
|
+
/** Excel counts days from this date, and believes 1900 was a leap year, so its first 60 days read one too high. */
|
|
18
|
+
var EPOCH = Date.UTC(1899, 11, 30);
|
|
19
|
+
var LEAP_BUG = 61;
|
|
20
|
+
var EMU_PER_PIXEL = 9525;
|
|
21
|
+
var [MIN_WIDTH, MAX_WIDTH] = [8, 60];
|
|
22
|
+
/** Column widths come from this many rows, so a million-row sheet is not measured cell by cell. */
|
|
23
|
+
var MEASURED = 1e3;
|
|
24
|
+
/** Indexes into `cellXfs`. */
|
|
25
|
+
var Style = {
|
|
26
|
+
Date: 1,
|
|
27
|
+
DateTime: 2,
|
|
28
|
+
Header: 3,
|
|
29
|
+
Guarded: 4
|
|
30
|
+
};
|
|
31
|
+
/** A stored string is never run as a formula. The guarded style also keeps it text when someone edits the cell. */
|
|
32
|
+
var FORMULA = /^[=+\-@\t\r\n]/;
|
|
33
|
+
var STYLES = `${HEAD}<styleSheet xmlns="${MAIN}"><numFmts count="2"><numFmt numFmtId="164" formatCode="yyyy\\-mm\\-dd"/><numFmt numFmtId="165" formatCode="yyyy\\-mm\\-dd\\ hh:mm:ss"/></numFmts><fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="5"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="164" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/><xf numFmtId="165" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/><xf numFmtId="49" fontId="1" fillId="0" borderId="0" xfId="0" quotePrefix="1" applyNumberFormat="1" applyFont="1"/><xf numFmtId="49" fontId="0" fillId="0" borderId="0" xfId="0" quotePrefix="1" applyNumberFormat="1"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
|
|
34
|
+
/** Writes an .xlsx file by hand: a workbook is a zip of XML parts. Text goes through the shared string table, and no cell ever holds a formula. */
|
|
35
|
+
var ExcelWriter = class ExcelWriter {
|
|
36
|
+
#strings = /* @__PURE__ */ new Map();
|
|
37
|
+
#references = 0;
|
|
38
|
+
static async write(chart, { image, scale = 2 } = {}) {
|
|
39
|
+
const table = ExportText.rows(chart, "Excel export");
|
|
40
|
+
return ExcelWriter.file(table, image ? await chart.toPNG({ scale }) : null, scale);
|
|
41
|
+
}
|
|
42
|
+
/** `image` is a PNG drawn at `scale`, placed at the chart's own size on a second sheet. */
|
|
43
|
+
static async file(table, image, scale) {
|
|
44
|
+
if (table.rows.length >= MAX_ROWS || table.columns.length > MAX_COLUMNS) throw new ChartExportError(ExportErrorCode.Unsupported, `A worksheet holds ${MAX_ROWS} rows and ${MAX_COLUMNS} columns, and this table has ${table.rows.length} rows and ${table.columns.length} columns. Use CSV.`);
|
|
45
|
+
return new ExcelWriter().#workbook(table, image && new Uint8Array(await image.arrayBuffer()), scale);
|
|
46
|
+
}
|
|
47
|
+
#workbook(table, image, scale) {
|
|
48
|
+
const sheets = [["Data", this.#sheet(table)], ...image ? [["Chart", ExcelWriter.#pictureSheet()]] : []];
|
|
49
|
+
const parts = {
|
|
50
|
+
"[Content_Types].xml": ExcelWriter.#xml(ExcelWriter.#types(sheets.length)),
|
|
51
|
+
"_rels/.rels": ExcelWriter.#xml(ExcelWriter.#links([`${LINKS}/officeDocument`, "xl/workbook.xml"])),
|
|
52
|
+
"xl/workbook.xml": ExcelWriter.#xml(ExcelWriter.#book(sheets.map(([name]) => name))),
|
|
53
|
+
"xl/_rels/workbook.xml.rels": ExcelWriter.#xml(ExcelWriter.#links(...sheets.map((_, i) => [`${LINKS}/worksheet`, `worksheets/sheet${i + 1}.xml`]), [`${LINKS}/styles`, "styles.xml"], [`${LINKS}/sharedStrings`, "sharedStrings.xml"])),
|
|
54
|
+
"xl/styles.xml": ExcelWriter.#xml(STYLES),
|
|
55
|
+
"xl/sharedStrings.xml": ExcelWriter.#xml(this.#stringTable())
|
|
56
|
+
};
|
|
57
|
+
sheets.forEach(([, xml], i) => {
|
|
58
|
+
parts[`xl/worksheets/sheet${i + 1}.xml`] = ExcelWriter.#xml(xml);
|
|
59
|
+
});
|
|
60
|
+
if (image) {
|
|
61
|
+
parts["xl/worksheets/_rels/sheet2.xml.rels"] = ExcelWriter.#xml(ExcelWriter.#links([`${LINKS}/drawing`, "../drawings/drawing1.xml"]));
|
|
62
|
+
parts["xl/drawings/drawing1.xml"] = ExcelWriter.#xml(ExcelWriter.#drawing(image, scale));
|
|
63
|
+
parts["xl/drawings/_rels/drawing1.xml.rels"] = ExcelWriter.#xml(ExcelWriter.#links([`${LINKS}/image`, "../media/image1.png"]));
|
|
64
|
+
parts["xl/media/image1.png"] = [image, { level: 0 }];
|
|
65
|
+
}
|
|
66
|
+
return new Blob([zipSync(parts)], { type: TYPE });
|
|
67
|
+
}
|
|
68
|
+
static #xml(text) {
|
|
69
|
+
return [strToU8(text), { level: 6 }];
|
|
70
|
+
}
|
|
71
|
+
static #types(sheets) {
|
|
72
|
+
const override = (part, type) => `<Override PartName="/${part}" ContentType="${type}+xml"/>`;
|
|
73
|
+
const sheetParts = Array.from({ length: sheets }, (_, i) => override(`xl/worksheets/sheet${i + 1}.xml`, `${CONTENT}.spreadsheetml.worksheet`));
|
|
74
|
+
return `${HEAD}<Types xmlns="${PACKAGE}/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="png" ContentType="image/png"/>` + override("xl/workbook.xml", `${CONTENT}.spreadsheetml.sheet.main`) + sheetParts.join("") + override("xl/styles.xml", `${CONTENT}.spreadsheetml.styles`) + override("xl/sharedStrings.xml", `${CONTENT}.spreadsheetml.sharedStrings`) + (sheets > 1 ? override("xl/drawings/drawing1.xml", `${CONTENT}.drawing`) : "") + "</Types>";
|
|
75
|
+
}
|
|
76
|
+
static #links(...targets) {
|
|
77
|
+
return `${HEAD}<Relationships xmlns="${PACKAGE}/relationships">${targets.map(([type, target], i) => `<Relationship Id="rId${i + 1}" Type="${type}" Target="${target}"/>`).join("")}</Relationships>`;
|
|
78
|
+
}
|
|
79
|
+
static #book(names) {
|
|
80
|
+
return `${HEAD}<workbook xmlns="${MAIN}" xmlns:r="${LINKS}"><bookViews><workbookView/></bookViews><sheets>${names.map((name, i) => `<sheet name="${name}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join("")}</sheets></workbook>`;
|
|
81
|
+
}
|
|
82
|
+
static #pictureSheet() {
|
|
83
|
+
return `${HEAD}<worksheet xmlns="${MAIN}" xmlns:r="${LINKS}"><sheetData/><drawing r:id="rId1"/></worksheet>`;
|
|
84
|
+
}
|
|
85
|
+
/** The element order is fixed by the schema, and Excel calls a file corrupt when it differs. */
|
|
86
|
+
#sheet({ columns, rows }) {
|
|
87
|
+
const widths = columns.map((column) => column.length + 1);
|
|
88
|
+
const lines = [`<row r="1">${columns.map((column, c) => this.#textCell(ExcelWriter.#ref(c, 1), column, Style.Header)).join("")}</row>`];
|
|
89
|
+
rows.forEach((row, r) => {
|
|
90
|
+
const number = r + 2;
|
|
91
|
+
const cells = row.slice(0, MAX_COLUMNS).map((cell, c) => {
|
|
92
|
+
if (r < MEASURED) widths[c] = Math.max(widths[c] ?? 0, ExportText.of(cell).length);
|
|
93
|
+
return this.#cell(ExcelWriter.#ref(c, number), cell);
|
|
94
|
+
});
|
|
95
|
+
lines.push(`<row r="${number}">${cells.join("")}</row>`);
|
|
96
|
+
});
|
|
97
|
+
const pane = "<pane ySplit=\"1\" topLeftCell=\"A2\" activePane=\"bottomLeft\" state=\"frozen\"/><selection pane=\"bottomLeft\" activeCell=\"A2\" sqref=\"A2\"/>";
|
|
98
|
+
const cols = widths.map((width, c) => `<col min="${c + 1}" max="${c + 1}" width="${Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, width + 2))}" customWidth="1"/>`).join("");
|
|
99
|
+
return `${HEAD}<worksheet xmlns="${MAIN}"><dimension ref="A1:${ExcelWriter.#ref(Math.max(widths.length, 1) - 1, rows.length + 1)}"/><sheetViews><sheetView tabSelected="1" workbookViewId="0">${pane}</sheetView></sheetViews><sheetFormatPr defaultRowHeight="15"/>${cols && `<cols>${cols}</cols>`}<sheetData>${lines.join("")}</sheetData></worksheet>`;
|
|
100
|
+
}
|
|
101
|
+
#cell(ref, cell) {
|
|
102
|
+
if (typeof cell === "number") return Number.isFinite(cell) ? `<c r="${ref}"><v>${cell}</v></c>` : Number.isNaN(cell) ? "" : this.#textCell(ref, String(cell));
|
|
103
|
+
if (typeof cell === "boolean") return `<c r="${ref}" t="b"><v>${+cell}</v></c>`;
|
|
104
|
+
if (cell instanceof Date && !Number.isNaN(+cell)) return ExcelWriter.#dateCell(ref, cell) ?? this.#textCell(ref, ExportText.stamp(cell));
|
|
105
|
+
const text = ChartExporter.text(cell);
|
|
106
|
+
return text ? this.#textCell(ref, text) : "";
|
|
107
|
+
}
|
|
108
|
+
/** A date outside the range Excel can show has no serial, so it stays text. */
|
|
109
|
+
static #dateCell(ref, date) {
|
|
110
|
+
const days = (ExportText.wall(date) - EPOCH) / DAY;
|
|
111
|
+
const serial = days < LEAP_BUG ? days - 1 : days;
|
|
112
|
+
if (serial < 1 || serial > MAX_SERIAL) return null;
|
|
113
|
+
return `<c r="${ref}" s="${Number.isInteger(serial) ? Style.Date : Style.DateTime}"><v>${serial}</v></c>`;
|
|
114
|
+
}
|
|
115
|
+
#textCell(ref, text, style) {
|
|
116
|
+
const shown = style ?? (FORMULA.test(text) ? Style.Guarded : void 0);
|
|
117
|
+
return `<c r="${ref}"${shown ? ` s="${shown}"` : ""} t="s"><v>${this.#string(text)}</v></c>`;
|
|
118
|
+
}
|
|
119
|
+
#string(text) {
|
|
120
|
+
this.#references++;
|
|
121
|
+
const known = this.#strings.get(text);
|
|
122
|
+
if (known !== void 0) return known;
|
|
123
|
+
const index = this.#strings.size;
|
|
124
|
+
this.#strings.set(text, index);
|
|
125
|
+
return index;
|
|
126
|
+
}
|
|
127
|
+
#stringTable() {
|
|
128
|
+
const items = [...this.#strings.keys()].map((text) => `<si><t xml:space="preserve">${ExcelWriter.#escape(text)}</t></si>`);
|
|
129
|
+
return `${HEAD}<sst xmlns="${MAIN}" count="${this.#references}" uniqueCount="${this.#strings.size}">${items.join("")}</sst>`;
|
|
130
|
+
}
|
|
131
|
+
/** Spreadsheets read `_x0041_` in a string as "A", so a literal one has its underscore escaped the same way. */
|
|
132
|
+
static #escape(text) {
|
|
133
|
+
return ChartExporter.writable(text.slice(0, MAX_TEXT)).replace(/_(?=x[0-9a-fA-F]{4}_)/g, "_x005F_").replace(/[&<>\r]/g, (char) => ({
|
|
134
|
+
"&": "&",
|
|
135
|
+
"<": "<",
|
|
136
|
+
">": ">",
|
|
137
|
+
"\r": " "
|
|
138
|
+
})[char]);
|
|
139
|
+
}
|
|
140
|
+
static #ref(column, row) {
|
|
141
|
+
let letters = "";
|
|
142
|
+
for (let left = column + 1; left > 0; left = Math.floor((left - 1) / 26)) letters = String.fromCharCode(65 + (left - 1) % 26) + letters;
|
|
143
|
+
return `${letters}${row}`;
|
|
144
|
+
}
|
|
145
|
+
/** The size comes from the PNG's own header: width and height follow its 16-byte preamble. */
|
|
146
|
+
static #drawing(image, scale) {
|
|
147
|
+
const header = new DataView(image.buffer, image.byteOffset);
|
|
148
|
+
const cx = Math.round(header.getUint32(16) / scale * EMU_PER_PIXEL), cy = Math.round(header.getUint32(20) / scale * EMU_PER_PIXEL);
|
|
149
|
+
return `${HEAD}<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${LINKS}"><xdr:oneCellAnchor><xdr:from><xdr:col>0</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from><xdr:ext cx="${cx}" cy="${cy}"/><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="2" name="Chart"/><xdr:cNvPicPr><a:picLocks noChangeAspect="1"/></xdr:cNvPicPr></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"/><a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic><xdr:clientData/></xdr:oneCellAnchor></xdr:wsDr>`;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
export { ExcelWriter };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ChartExporter } from "./ChartExporter.js";
|
|
2
|
+
import { ChartExportError, ExportErrorCode } from "./ChartExportError.js";
|
|
3
|
+
// src/core/ExportText.ts
|
|
4
|
+
/** Cells as people read them. Dates use local time, as chart axes do. */
|
|
5
|
+
var ExportText = class ExportText {
|
|
6
|
+
/** `table()` with any rows still queued, or an error naming what needed them. A chart holding no data has no rows, whatever its override reads. */
|
|
7
|
+
static rows(chart, needed) {
|
|
8
|
+
chart.flush();
|
|
9
|
+
if (chart.data != null) {
|
|
10
|
+
const table = chart.table();
|
|
11
|
+
if (table.rows.length) return table;
|
|
12
|
+
}
|
|
13
|
+
throw new ChartExportError(ExportErrorCode.NoData, `${needed} needs rows, and this chart's data has no flat table view.`);
|
|
14
|
+
}
|
|
15
|
+
/** The local date and time as milliseconds on a UTC clock. Zone offsets are not whole minutes in old dates, so this reads the fields. */
|
|
16
|
+
static wall(date) {
|
|
17
|
+
const wall = /* @__PURE__ */ new Date(0);
|
|
18
|
+
wall.setUTCFullYear(date.getFullYear(), date.getMonth(), date.getDate());
|
|
19
|
+
return wall.setUTCHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
|
20
|
+
}
|
|
21
|
+
static stamp(date) {
|
|
22
|
+
return new Date(ExportText.wall(date)).toISOString().slice(0, 19).replace("T", " ").replace(/ 00:00:00$/, "");
|
|
23
|
+
}
|
|
24
|
+
static of(cell) {
|
|
25
|
+
return cell instanceof Date && !Number.isNaN(+cell) ? ExportText.stamp(cell) : ChartExporter.text(cell);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export { ExportText };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/core/MarkOrigin.ts
|
|
2
|
+
/** Where derived marks keep the record they were drawn from, most direct first. */
|
|
3
|
+
var POINTERS = [
|
|
4
|
+
"row",
|
|
5
|
+
"record",
|
|
6
|
+
"datum",
|
|
7
|
+
"data",
|
|
8
|
+
"raw",
|
|
9
|
+
"source",
|
|
10
|
+
"node",
|
|
11
|
+
"link",
|
|
12
|
+
"feature",
|
|
13
|
+
"region",
|
|
14
|
+
"sample"
|
|
15
|
+
];
|
|
16
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
/** Text form of a cell. Charts often turn dates into timestamps. */
|
|
18
|
+
var cell = (value) => String(value instanceof Date ? value.getTime() : value);
|
|
19
|
+
/** A graph layout works on copies, so its nodes and links are known by id and by their two ends. */
|
|
20
|
+
var nameOf = (record) => {
|
|
21
|
+
const end = (value) => isRecord(value) ? value.id : value;
|
|
22
|
+
return "source" in record && "target" in record ? `${end(record.source)}>${end(record.target)}` : record.id;
|
|
23
|
+
};
|
|
24
|
+
var held = /* @__PURE__ */ new WeakMap();
|
|
25
|
+
/** Finds the record a mark stands for, so `onSelect` hands back what the user passed in rather than layout state. */
|
|
26
|
+
var MarkOrigin = class {
|
|
27
|
+
/** The row, node or feature behind `mark`, or `mark` when it has no single source. */
|
|
28
|
+
static of(mark, { data, ...options }) {
|
|
29
|
+
if (typeof mark !== "object" || mark === null || typeof data !== "object" || data === null) return mark;
|
|
30
|
+
const known = this.#held(data), derived = mark;
|
|
31
|
+
return this.#known(derived, known) ?? this.#pointedAt(derived, known, 2) ?? (Array.isArray(data) ? this.#matched(derived, data, options) : void 0) ?? mark;
|
|
32
|
+
}
|
|
33
|
+
static #held(data) {
|
|
34
|
+
let known = held.get(data);
|
|
35
|
+
if (!known) {
|
|
36
|
+
known = {
|
|
37
|
+
records: /* @__PURE__ */ new Set(),
|
|
38
|
+
named: /* @__PURE__ */ new Map()
|
|
39
|
+
};
|
|
40
|
+
held.set(data, known);
|
|
41
|
+
this.#collect(data, known.records);
|
|
42
|
+
if (!Array.isArray(data)) for (const record of known.records) known.named.set(nameOf(record), record);
|
|
43
|
+
known.named.delete(void 0);
|
|
44
|
+
}
|
|
45
|
+
return known;
|
|
46
|
+
}
|
|
47
|
+
static #known(candidate, { records, named }) {
|
|
48
|
+
return records.has(candidate) ? candidate : named.get(nameOf(candidate));
|
|
49
|
+
}
|
|
50
|
+
/** Rows, tree nodes, graph nodes and links, and map features. Arrays of numbers or coordinates hold no records. */
|
|
51
|
+
static #collect(value, into) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
if (isRecord(value.find((item) => item !== null && item !== void 0))) for (const item of value) this.#collect(item, into);
|
|
54
|
+
} else if (isRecord(value) && !into.has(value)) {
|
|
55
|
+
into.add(value);
|
|
56
|
+
for (const child of Object.values(value)) if (Array.isArray(child)) this.#collect(child, into);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
static #pointedAt(mark, known, depth) {
|
|
60
|
+
const links = POINTERS.filter((key) => key !== "source" || !("target" in mark)).map((key) => mark[key]).filter(isRecord);
|
|
61
|
+
for (const link of links) {
|
|
62
|
+
const found = this.#known(link, known);
|
|
63
|
+
if (found) return found;
|
|
64
|
+
}
|
|
65
|
+
if (depth === 1) return void 0;
|
|
66
|
+
for (const link of links) {
|
|
67
|
+
const deeper = this.#pointedAt(link, known, depth - 1);
|
|
68
|
+
if (deeper) return deeper;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* For marks built by pivoting rows: the one row whose named columns all appear on the mark. Summed
|
|
73
|
+
* rows fail on the value, so a second pass ignores numbers. More than one match means a real aggregate.
|
|
74
|
+
*/
|
|
75
|
+
static #matched(mark, rows, options) {
|
|
76
|
+
const table = rows.filter(isRecord);
|
|
77
|
+
const first = table[0];
|
|
78
|
+
if (!first) return void 0;
|
|
79
|
+
const columns = Object.values(options).flat().filter((name) => typeof name === "string" && name in first);
|
|
80
|
+
const shown = new Set(Object.values(mark).flatMap((value) => isRecord(value) ? Object.values(value) : [value]).map(cell));
|
|
81
|
+
const unique = (names) => {
|
|
82
|
+
const hits = names.length ? table.filter((row) => names.every((name) => shown.has(cell(row[name])))) : [];
|
|
83
|
+
return hits.length === 1 ? hits[0] : void 0;
|
|
84
|
+
};
|
|
85
|
+
return unique(columns) ?? unique(columns.filter((name) => typeof first[name] !== "number"));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
export { MarkOrigin };
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { FRAMING } from "./Context2D.js";
|
|
2
|
+
import { EdgeRendering, StrokeDash } from "./enums.js";
|
|
3
|
+
import { color } from "d3";
|
|
4
|
+
// src/core/MarkStyle.ts
|
|
5
|
+
var SHAPES = "path,line,polyline,polygon,rect,circle,ellipse";
|
|
6
|
+
var UNDRAWN = "defs,clipPath,mask,pattern,marker,symbol,.tick,.domain";
|
|
7
|
+
/** Mark-less groups that the shared layer and chart modules name, past what `FRAME` and `FRAMING` list. */
|
|
8
|
+
var FRAMED = /(^|-)(axis|grid|clip|brush(es)?|zoom|annotations?|crosshair|hair)(-|$)/;
|
|
9
|
+
/** Path groups that fill their layout, however small each shape is: cells, tiles, and the keys read against them. */
|
|
10
|
+
var TILED = /^(cells(-\d+)?|tiles|nonzeros|before|after|size-key)$/;
|
|
11
|
+
var MEASURED = [
|
|
12
|
+
"strokeWidth",
|
|
13
|
+
"strokeDash",
|
|
14
|
+
"strokeCap",
|
|
15
|
+
"strokeJoin",
|
|
16
|
+
"opacity",
|
|
17
|
+
"markSize",
|
|
18
|
+
"edges",
|
|
19
|
+
"fontSize"
|
|
20
|
+
];
|
|
21
|
+
/** Past this radius a circle is layout, such as a packed node or a dial, not a marker. */
|
|
22
|
+
var MARKER_RADIUS = 24;
|
|
23
|
+
/** The longest `d` a d3 symbol writes. */
|
|
24
|
+
var SYMBOL_PATH = 300;
|
|
25
|
+
var SYMBOL_REACH = 30;
|
|
26
|
+
var SCALE = /scale\(\s*([-\d.e]+)\s*\)/;
|
|
27
|
+
var NUMBER = /-?\d*\.?\d+(?:e-?\d+)?/gi;
|
|
28
|
+
var DASHES = {
|
|
29
|
+
[StrokeDash.Dashed]: [6, 4],
|
|
30
|
+
[StrokeDash.Dotted]: [2, 3]
|
|
31
|
+
};
|
|
32
|
+
var EDGES = {
|
|
33
|
+
[EdgeRendering.Crisp]: "crispEdges",
|
|
34
|
+
[EdgeRendering.Precise]: "geometricPrecision"
|
|
35
|
+
};
|
|
36
|
+
var round = (value) => String(+value.toFixed(4));
|
|
37
|
+
var positive = (value) => value !== void 0 && value > 0 && Number.isFinite(value) ? value : void 0;
|
|
38
|
+
var write = (el, name, value) => {
|
|
39
|
+
if (value === null) el.removeAttribute(name);
|
|
40
|
+
else el.setAttribute(name, value);
|
|
41
|
+
};
|
|
42
|
+
var inFlight = (el) => !!el.__transition;
|
|
43
|
+
var groupNames = (group) => [...group.classList].filter((name) => name.startsWith("m-")).map((name) => name.slice(2));
|
|
44
|
+
/** `base` times `by`, reading `fallback` when the element has no value of its own. A value that is no number stays. */
|
|
45
|
+
var times = (by, fallback) => (base) => {
|
|
46
|
+
const value = parseFloat(base ?? fallback ?? "");
|
|
47
|
+
return Number.isFinite(value) ? round(value * by) : base;
|
|
48
|
+
};
|
|
49
|
+
/** True for a path drawn around the origin and small enough to be a glyph, which is how d3 symbols come. */
|
|
50
|
+
var glyph = (d) => {
|
|
51
|
+
if (!d || d.length > SYMBOL_PATH) return false;
|
|
52
|
+
const numbers = (d.match(NUMBER) ?? []).map(Number);
|
|
53
|
+
return numbers.length > 0 && Math.max(...numbers) <= SYMBOL_REACH && Math.min(...numbers) >= -30 && numbers.some((value) => value < 0) && numbers.some((value) => value > 0);
|
|
54
|
+
};
|
|
55
|
+
/** Adds to a glyph's own `scale()`, which entry animations use, or starts one after its `translate()`. */
|
|
56
|
+
var scaled = (by) => (base) => {
|
|
57
|
+
if (base === null) return `scale(${by})`;
|
|
58
|
+
return SCALE.test(base) ? base.replace(SCALE, (_, own) => `scale(${round(parseFloat(own) * by)})`) : `${base} scale(${by})`;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The `chartStyle` options that attributes carry. Attributes go back to what the chart drew before each
|
|
62
|
+
* paint, so a chart always draws from clean values, and take the style again after it. A transition
|
|
63
|
+
* owns the attributes it moves: `ends` gives it styled targets, and they count as held until it lands.
|
|
64
|
+
*/
|
|
65
|
+
var MarkStyle = class {
|
|
66
|
+
#held = /* @__PURE__ */ new Map();
|
|
67
|
+
/** What running transitions are heading for, since a mark is classed by how it will end up. */
|
|
68
|
+
#planned = /* @__PURE__ */ new Map();
|
|
69
|
+
#framed = /* @__PURE__ */ new WeakMap();
|
|
70
|
+
/** Whether a group's shapes of one tag can size, which holds for all of them or none. */
|
|
71
|
+
#sizing = /* @__PURE__ */ new Map();
|
|
72
|
+
#style = {};
|
|
73
|
+
#theme = null;
|
|
74
|
+
#key = "";
|
|
75
|
+
#bare = /* @__PURE__ */ new Set();
|
|
76
|
+
/** True when `style` has anything this pass does. Colour and the 3D options do not need it. */
|
|
77
|
+
static wanted(style) {
|
|
78
|
+
return !!style && MEASURED.some((key) => style[key] !== void 0);
|
|
79
|
+
}
|
|
80
|
+
/** Sets what the next paint draws and puts back what the last one styled. A new style also takes back what is moving. */
|
|
81
|
+
begin(style, theme) {
|
|
82
|
+
const key = JSON.stringify([
|
|
83
|
+
style,
|
|
84
|
+
theme.surface,
|
|
85
|
+
theme.grid,
|
|
86
|
+
theme.axis
|
|
87
|
+
]);
|
|
88
|
+
this.end(key !== this.#key);
|
|
89
|
+
this.#sizing.clear();
|
|
90
|
+
this.#key = key;
|
|
91
|
+
this.#style = style;
|
|
92
|
+
this.#theme = theme;
|
|
93
|
+
this.#bare = new Set([
|
|
94
|
+
theme.surface,
|
|
95
|
+
theme.grid,
|
|
96
|
+
theme.axis
|
|
97
|
+
].map(hex));
|
|
98
|
+
}
|
|
99
|
+
/** Puts back every styled attribute, leaving those a transition is still moving unless `all`. */
|
|
100
|
+
end(all = true) {
|
|
101
|
+
for (const [el, held] of this.#held) {
|
|
102
|
+
for (const [name, { value, moving }] of held) {
|
|
103
|
+
if (moving && !all && inFlight(el)) continue;
|
|
104
|
+
write(el, name, value);
|
|
105
|
+
held.delete(name);
|
|
106
|
+
}
|
|
107
|
+
if (!held.size) this.#held.delete(el);
|
|
108
|
+
}
|
|
109
|
+
for (const el of this.#planned.keys()) if (all || !inFlight(el)) this.#planned.delete(el);
|
|
110
|
+
}
|
|
111
|
+
/** Styles the finished drawing. Marks only under `g-plot`; text anywhere in the chart. */
|
|
112
|
+
apply(svg) {
|
|
113
|
+
const plot = svg.querySelector("g.g-plot");
|
|
114
|
+
if (plot) {
|
|
115
|
+
for (const el of plot.querySelectorAll(SHAPES)) if (this.#isMark(el)) this.#restyle(el);
|
|
116
|
+
}
|
|
117
|
+
if (positive(this.#style.fontSize)) for (const el of svg.querySelectorAll("text")) this.#restyle(el);
|
|
118
|
+
}
|
|
119
|
+
/** The values a transition should land on, for the attributes this style changes. Call before the transition exists. */
|
|
120
|
+
ends(selection, attrs) {
|
|
121
|
+
const names = Object.keys(attrs), landing = /* @__PURE__ */ new Map(), targets = /* @__PURE__ */ new Map(), claimed = /* @__PURE__ */ new Set();
|
|
122
|
+
const land = (el, datum, index) => {
|
|
123
|
+
const raw = {};
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
const value = attrs[name];
|
|
126
|
+
const end = typeof value === "function" ? value.call(el, datum, index) : value;
|
|
127
|
+
raw[name] = end === null ? null : String(end);
|
|
128
|
+
}
|
|
129
|
+
landing.set(el, raw);
|
|
130
|
+
this.#planned.set(el, {
|
|
131
|
+
...this.#planned.get(el),
|
|
132
|
+
...raw
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
selection.each(function(datum, index) {
|
|
136
|
+
land(this, datum, index);
|
|
137
|
+
});
|
|
138
|
+
this.#sizing.clear();
|
|
139
|
+
for (const [el, raw] of landing) {
|
|
140
|
+
const styled = this.#claim(el, raw);
|
|
141
|
+
for (const name in styled) claimed.add(name);
|
|
142
|
+
targets.set(el, {
|
|
143
|
+
...raw,
|
|
144
|
+
...styled
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const ends = {};
|
|
148
|
+
for (const name of claimed) ends[name] = function() {
|
|
149
|
+
return targets.get(this)[name];
|
|
150
|
+
};
|
|
151
|
+
return ends;
|
|
152
|
+
}
|
|
153
|
+
/** `names` were just written without a transition, so whatever was moving them no longer speaks for them. */
|
|
154
|
+
placed(selection, names) {
|
|
155
|
+
selection.each((_, index, nodes) => {
|
|
156
|
+
const el = nodes[index], held = this.#held.get(el), planned = this.#planned.get(el);
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
if (held?.get(name)?.moving) held.delete(name);
|
|
159
|
+
if (planned) delete planned[name];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
#claim(el, raw) {
|
|
164
|
+
const target = {};
|
|
165
|
+
if (el.tagName !== "text" && !(el.matches(SHAPES) && this.#isMark(el))) return target;
|
|
166
|
+
const rules = this.#rules(el);
|
|
167
|
+
let held = this.#held.get(el);
|
|
168
|
+
for (const name of Object.keys(raw)) {
|
|
169
|
+
const rule = rules[name];
|
|
170
|
+
if (!rule) continue;
|
|
171
|
+
held ??= /* @__PURE__ */ new Map();
|
|
172
|
+
this.#held.set(el, held);
|
|
173
|
+
if (!held.get(name)?.moving) write(el, name, rule(el.getAttribute(name)));
|
|
174
|
+
held.set(name, {
|
|
175
|
+
value: raw[name],
|
|
176
|
+
moving: true
|
|
177
|
+
});
|
|
178
|
+
target[name] = rule(raw[name]);
|
|
179
|
+
}
|
|
180
|
+
return target;
|
|
181
|
+
}
|
|
182
|
+
#restyle(el) {
|
|
183
|
+
for (const [name, rule] of Object.entries(this.#rules(el))) {
|
|
184
|
+
const held = this.#held.get(el);
|
|
185
|
+
if (held?.has(name)) continue;
|
|
186
|
+
const base = el.getAttribute(name), next = rule(base);
|
|
187
|
+
if (next === base) continue;
|
|
188
|
+
if (!held) this.#held.set(el, /* @__PURE__ */ new Map([[name, {
|
|
189
|
+
value: base,
|
|
190
|
+
moving: false
|
|
191
|
+
}]]));
|
|
192
|
+
else held.set(name, {
|
|
193
|
+
value: base,
|
|
194
|
+
moving: false
|
|
195
|
+
});
|
|
196
|
+
write(el, name, next);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
#rules(el) {
|
|
200
|
+
if (el.tagName === "text") {
|
|
201
|
+
const by = positive(this.#style.fontSize);
|
|
202
|
+
return by ? { "font-size": times(by, this.#inherited(el, "font-size") ?? "16") } : {};
|
|
203
|
+
}
|
|
204
|
+
const { strokeDash, strokeCap, strokeJoin, edges } = this.#style;
|
|
205
|
+
const strokeWidth = positive(this.#style.strokeWidth), markSize = positive(this.#style.markSize);
|
|
206
|
+
const opacity = this.#style.opacity === void 0 || Number.isNaN(this.#style.opacity) ? void 0 : Math.max(0, Math.min(1, this.#style.opacity));
|
|
207
|
+
const paint = this.#inherited(el, "stroke") ?? "none";
|
|
208
|
+
const stroke = this.#ink(paint), fill = el.tagName === "line" ? 0 : this.#ink(this.#inherited(el, "fill") ?? "#000");
|
|
209
|
+
const rules = {};
|
|
210
|
+
if (stroke === 0 && fill === 0) return rules;
|
|
211
|
+
if (stroke === 2) {
|
|
212
|
+
if (strokeWidth) rules["stroke-width"] = times(strokeWidth, this.#inherited(el, "stroke-width") ?? "1");
|
|
213
|
+
const dash = strokeDash && DASHES[strokeDash];
|
|
214
|
+
if (dash && !this.#painted(this.#inherited(el, "stroke-dasharray"))) {
|
|
215
|
+
const pattern = dash.map((step) => round(step * (strokeWidth ?? 1))).join(" ");
|
|
216
|
+
rules["stroke-dasharray"] = () => pattern;
|
|
217
|
+
}
|
|
218
|
+
if (strokeCap) rules["stroke-linecap"] = () => strokeCap;
|
|
219
|
+
if (strokeJoin) rules["stroke-linejoin"] = () => strokeJoin;
|
|
220
|
+
if (opacity !== void 0) rules["stroke-opacity"] = times(opacity, this.#inherited(el, "stroke-opacity") ?? "1");
|
|
221
|
+
}
|
|
222
|
+
if (fill === 2 && opacity !== void 0) rules["fill-opacity"] = times(opacity, this.#inherited(el, "fill-opacity") ?? "1");
|
|
223
|
+
const guide = fill === 0 && hex(paint) === hex(this.#theme.grid);
|
|
224
|
+
if (markSize && !guide) this.#size(el, markSize, stroke, rules);
|
|
225
|
+
const rendering = edges && EDGES[edges];
|
|
226
|
+
if (rendering && (stroke === 2 || fill === 2)) rules["shape-rendering"] = () => rendering;
|
|
227
|
+
return rules;
|
|
228
|
+
}
|
|
229
|
+
#size(el, by, stroke, rules) {
|
|
230
|
+
if (el.tagName === "circle") {
|
|
231
|
+
if (this.#sizes(el)) rules.r = times(by, null);
|
|
232
|
+
} else if (el.tagName === "path") {
|
|
233
|
+
if (!(this.#planned.get(el) && "d" in this.#planned.get(el))) this.#whenLanded(el);
|
|
234
|
+
if (!this.#sizes(el)) return;
|
|
235
|
+
const transform = this.#read(el, "transform");
|
|
236
|
+
if (transform !== null && !transform.startsWith("translate(")) return;
|
|
237
|
+
rules.transform = scaled(by);
|
|
238
|
+
if (stroke !== 0) rules["vector-effect"] = () => "non-scaling-stroke";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** One shape past the limit makes its whole group layout: sizing only the small ones would stop size reading as value. */
|
|
242
|
+
#sizes(el) {
|
|
243
|
+
const group = el.parentElement;
|
|
244
|
+
if (!group) return false;
|
|
245
|
+
let tags = this.#sizing.get(group);
|
|
246
|
+
if (!tags) this.#sizing.set(group, tags = /* @__PURE__ */ new Map());
|
|
247
|
+
let sizes = tags.get(el.tagName);
|
|
248
|
+
if (sizes === void 0) {
|
|
249
|
+
const peers = [...group.children].filter((peer) => peer.tagName === el.tagName);
|
|
250
|
+
sizes = el.tagName === "circle" ? peers.every((peer) => !(parseFloat(this.#read(peer, "r") ?? "") > MARKER_RADIUS)) : !groupNames(group).some((name) => TILED.test(name)) && peers.every((peer) => glyph(this.#read(peer, "d")));
|
|
251
|
+
tags.set(el.tagName, sizes);
|
|
252
|
+
}
|
|
253
|
+
return sizes;
|
|
254
|
+
}
|
|
255
|
+
/** A chart's own transition keeps its end to itself, so the shape it lands on is only known then. */
|
|
256
|
+
#whenLanded(el) {
|
|
257
|
+
for (const schedule of Object.values(el.__transition ?? {})) schedule.on = schedule.on.copy().on("end.mark-style", () => {
|
|
258
|
+
this.#sizing.delete(el.parentElement);
|
|
259
|
+
this.#restyle(el);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
#read(el, name) {
|
|
263
|
+
const planned = this.#planned.get(el);
|
|
264
|
+
if (planned && name in planned) return planned[name];
|
|
265
|
+
return this.#held.get(el)?.get(name)?.value ?? el.getAttribute(name);
|
|
266
|
+
}
|
|
267
|
+
#inherited(el, name) {
|
|
268
|
+
return this.#read(el, name) ?? el.parentElement?.closest(`[${name}]`)?.getAttribute(name) ?? null;
|
|
269
|
+
}
|
|
270
|
+
#painted(dash) {
|
|
271
|
+
return !!dash && dash !== "none" && dash !== "0";
|
|
272
|
+
}
|
|
273
|
+
#ink(paint) {
|
|
274
|
+
if (paint === "none" || paint === "transparent") return 0;
|
|
275
|
+
return this.#bare.has(hex(paint)) ? 1 : 2;
|
|
276
|
+
}
|
|
277
|
+
#isMark(el) {
|
|
278
|
+
return !el.closest(UNDRAWN) && !this.#inFrame(el.parentElement);
|
|
279
|
+
}
|
|
280
|
+
#inFrame(group) {
|
|
281
|
+
if (!group || group.classList.contains("g-plot")) return false;
|
|
282
|
+
let framed = this.#framed.get(group);
|
|
283
|
+
if (framed === void 0) {
|
|
284
|
+
const names = groupNames(group);
|
|
285
|
+
framed = group.matches(".m-axis-x,.m-axis-y,.m-axis-title,.m-grid,.m-zoom-hit,.m-view-clip,.m-domain-clip,.m-brush,.m-zoom-controls") || names.some((name) => FRAMING.test(name) || FRAMED.test(name)) || this.#inFrame(group.parentElement);
|
|
286
|
+
this.#framed.set(group, framed);
|
|
287
|
+
}
|
|
288
|
+
return framed;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function hex(paint) {
|
|
292
|
+
return color(paint)?.formatHex() ?? paint;
|
|
293
|
+
}
|
|
294
|
+
export { MarkStyle };
|
package/dist/core/Palette.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import { color, interpolateRgb, interpolateRgbBasis } from "d3";
|
|
2
2
|
// src/core/Palette.ts
|
|
3
|
-
/** Assigns colours by job. Categorical colour follows the entity in first-seen order
|
|
3
|
+
/** Assigns colours by job. Categorical colour follows the entity in first-seen order and never cycles: series past the validated list share `inkMuted`, and the legend folds them into "Other". */
|
|
4
4
|
var Palette = class {
|
|
5
5
|
tokens;
|
|
6
6
|
seats;
|
|
7
|
+
pins;
|
|
8
|
+
opacity;
|
|
7
9
|
keys = /* @__PURE__ */ new Map();
|
|
8
10
|
ramp;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
ranked = 0;
|
|
12
|
+
/**
|
|
13
|
+
* `seats` holds each key's place across palettes, for a stream whose series come and go. `pins`
|
|
14
|
+
* gives series their own colour: a pinned series takes no place in the order, so the others keep theirs.
|
|
15
|
+
* `opacity` is `chartStyle.opacity`: `inkOn` picks ink for the colour a mark ends up on the surface.
|
|
16
|
+
*/
|
|
17
|
+
constructor(tokens, seats, pins = {}, opacity = 1) {
|
|
11
18
|
this.tokens = tokens;
|
|
12
19
|
this.seats = seats;
|
|
20
|
+
this.pins = pins;
|
|
21
|
+
this.opacity = opacity;
|
|
13
22
|
this.ramp = interpolateRgbBasis([...tokens.sequential]);
|
|
14
23
|
}
|
|
15
24
|
color = (key) => {
|
|
16
25
|
let hit = this.keys.get(key);
|
|
17
26
|
if (!hit) {
|
|
18
|
-
hit = this.tokens.categorical[this.seat(key)] ?? this.tokens.inkMuted;
|
|
27
|
+
hit = Object.hasOwn(this.pins, key) ? this.pins[key] : this.tokens.categorical[this.seat(key)] ?? this.tokens.inkMuted;
|
|
19
28
|
this.keys.set(key, hit);
|
|
20
29
|
}
|
|
21
30
|
return hit;
|
|
22
31
|
};
|
|
23
32
|
seat(key) {
|
|
24
33
|
const { seats, tokens: { categorical: { length } } } = this;
|
|
25
|
-
if (!seats) return this.
|
|
34
|
+
if (!seats) return this.ranked++;
|
|
26
35
|
if (!seats.has(key) && seats.size < length) seats.set(key, seats.size);
|
|
27
36
|
return seats.get(key) ?? length;
|
|
28
37
|
}
|
|
@@ -37,13 +46,24 @@ var Palette = class {
|
|
|
37
46
|
inkOn = (fill) => {
|
|
38
47
|
const rgb = color(fill)?.rgb();
|
|
39
48
|
if (!rgb) return "#ffffff";
|
|
40
|
-
|
|
49
|
+
const share = Number.isNaN(this.opacity) ? 1 : Math.max(0, Math.min(1, this.opacity)), surface = color(this.tokens.surface)?.rgb() ?? rgb;
|
|
50
|
+
const seen = (channel) => rgb[channel] * share + surface[channel] * (1 - share);
|
|
51
|
+
return .299 * seen("r") + .587 * seen("g") + .114 * seen("b") > 150 ? "#0b0b0b" : "#ffffff";
|
|
41
52
|
};
|
|
53
|
+
/** Series that look the same get one entry, naming them in `title`. A lone muted series is still told apart, so it keeps its name. */
|
|
42
54
|
items() {
|
|
43
|
-
|
|
55
|
+
const items = [...this.keys].map(([key, color]) => ({
|
|
44
56
|
key,
|
|
45
57
|
color
|
|
46
58
|
}));
|
|
59
|
+
const folded = (item) => item.color === this.tokens.inkMuted && !Object.hasOwn(this.pins, item.key);
|
|
60
|
+
const muted = items.filter(folded);
|
|
61
|
+
if (muted.length < 2) return items;
|
|
62
|
+
return [...items.filter((item) => !folded(item)), {
|
|
63
|
+
key: `Other (${muted.length})`,
|
|
64
|
+
color: this.tokens.inkMuted,
|
|
65
|
+
title: muted.map((item) => item.key).join(", ")
|
|
66
|
+
}];
|
|
47
67
|
}
|
|
48
68
|
};
|
|
49
69
|
export { Palette };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/core/PdfPage.ts
|
|
2
|
+
var PdfPageSize = /* @__PURE__ */ function(PdfPageSize) {
|
|
3
|
+
PdfPageSize["A4"] = "a4";
|
|
4
|
+
PdfPageSize["Letter"] = "letter";
|
|
5
|
+
return PdfPageSize;
|
|
6
|
+
}({});
|
|
7
|
+
var PdfOrientation = /* @__PURE__ */ function(PdfOrientation) {
|
|
8
|
+
PdfOrientation["Auto"] = "auto";
|
|
9
|
+
PdfOrientation["Portrait"] = "portrait";
|
|
10
|
+
PdfOrientation["Landscape"] = "landscape";
|
|
11
|
+
return PdfOrientation;
|
|
12
|
+
}({});
|
|
13
|
+
export { PdfOrientation, PdfPageSize };
|