@diagrammo/dgmo 0.23.0 → 0.24.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/dist/advanced.cjs +152 -73
- package/dist/advanced.d.cts +35 -19
- package/dist/advanced.d.ts +35 -19
- package/dist/advanced.js +152 -73
- package/dist/auto.cjs +153 -74
- package/dist/auto.js +110 -110
- package/dist/auto.mjs +153 -74
- package/dist/cli.cjs +150 -150
- package/dist/index.cjs +284 -73
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +282 -73
- package/dist/internal.cjs +152 -73
- package/dist/internal.d.cts +35 -19
- package/dist/internal.d.ts +35 -19
- package/dist/internal.js +152 -73
- package/docs/language-reference.md +3 -2
- package/package.json +1 -1
- package/src/boxes-and-lines/renderer.ts +98 -51
- package/src/d3.ts +22 -10
- package/src/index.ts +8 -0
- package/src/map/dimensions.ts +21 -5
- package/src/map/layout.ts +57 -46
- package/src/map/legend-band.ts +99 -0
- package/src/map/renderer.ts +10 -28
- package/src/map/resolver.ts +43 -1
- package/src/map/types.ts +20 -0
- package/src/utils/svg-embed.ts +193 -0
package/dist/internal.js
CHANGED
|
@@ -26649,7 +26649,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
|
|
|
26649
26649
|
const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
|
|
26650
26650
|
const matchColorGroup = (v) => {
|
|
26651
26651
|
const lv = v.trim().toLowerCase();
|
|
26652
|
-
if (lv === "none") return null;
|
|
26652
|
+
if (lv === "" || lv === "none") return null;
|
|
26653
26653
|
const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
|
|
26654
26654
|
if (tg) return tg.name;
|
|
26655
26655
|
if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
|
|
@@ -26990,6 +26990,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
|
|
|
26990
26990
|
const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
|
|
26991
26991
|
nodeG.append("title").text(tooltipText);
|
|
26992
26992
|
}
|
|
26993
|
+
} else if (parsed.showValues && node.value !== void 0) {
|
|
26994
|
+
const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
|
|
26995
|
+
const headerH = ln.height / 2;
|
|
26996
|
+
const sepY = -ln.height / 2 + headerH;
|
|
26997
|
+
const fitted = fitLabelToHeader(node.label, ln.width, 2);
|
|
26998
|
+
const labelLineH = fitted.fontSize * 1.3;
|
|
26999
|
+
const labelTotalH = fitted.lines.length * labelLineH;
|
|
27000
|
+
const headerCenterY = -ln.height / 2 + headerH / 2;
|
|
27001
|
+
for (let li = 0; li < fitted.lines.length; li++) {
|
|
27002
|
+
nodeG.append("text").attr("x", 0).attr(
|
|
27003
|
+
"y",
|
|
27004
|
+
headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
|
|
27005
|
+
).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", fitted.fontSize).attr("font-weight", "600").attr("fill", colors.text).text(fitted.lines[li]);
|
|
27006
|
+
}
|
|
27007
|
+
nodeG.append("line").attr("x1", -ln.width / 2).attr("y1", sepY).attr("x2", ln.width / 2).attr("y2", sepY).attr("stroke", colors.stroke).attr("stroke-opacity", 0.3).attr("stroke-width", 1);
|
|
27008
|
+
nodeG.append("text").attr("class", "bl-node-value").attr("x", 0).attr("y", (sepY + ln.height / 2) / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("fill", colors.text).attr("opacity", 0.85).text(valueLabel);
|
|
26993
27009
|
} else {
|
|
26994
27010
|
const maxLabelLines = Math.max(
|
|
26995
27011
|
2,
|
|
@@ -27002,21 +27018,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
|
|
|
27002
27018
|
nodeG.append("text").attr("x", 0).attr("y", -totalH / 2 + lineH / 2 + li * lineH).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", fitted.fontSize).attr("font-weight", "600").attr("fill", colors.text).text(fitted.lines[li]);
|
|
27003
27019
|
}
|
|
27004
27020
|
}
|
|
27005
|
-
if (parsed.showValues && node.value !== void 0) {
|
|
27021
|
+
if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
|
|
27006
27022
|
const valueText = String(node.value);
|
|
27007
|
-
const
|
|
27008
|
-
|
|
27009
|
-
|
|
27010
|
-
|
|
27011
|
-
|
|
27012
|
-
|
|
27013
|
-
|
|
27014
|
-
|
|
27015
|
-
nodeG.append("rect").attr("x", bx).attr("y", by).attr("width", bw).attr("height", bh).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.85);
|
|
27016
|
-
nodeG.append("text").attr("class", "bl-node-value").attr("x", bx + bw - padX).attr("y", by + padY).attr("text-anchor", "end").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", palette.textMuted).text(valueText);
|
|
27017
|
-
} else {
|
|
27018
|
-
nodeG.append("text").attr("class", "bl-node-value").attr("x", 0).attr("y", ln.height / 2 - VALUE_FONT_SIZE).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", colors.text).attr("opacity", 0.8).text(valueText);
|
|
27019
|
-
}
|
|
27023
|
+
const padX = 6;
|
|
27024
|
+
const padY = 5;
|
|
27025
|
+
const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
|
|
27026
|
+
const bh = VALUE_FONT_SIZE + 4;
|
|
27027
|
+
const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
|
|
27028
|
+
const by = -ln.height / 2 + 4;
|
|
27029
|
+
nodeG.append("rect").attr("x", bx).attr("y", by).attr("width", bw).attr("height", bh).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.85);
|
|
27030
|
+
nodeG.append("text").attr("class", "bl-node-value").attr("x", bx + bw - padX).attr("y", by + padY).attr("text-anchor", "end").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", palette.textMuted).text(valueText);
|
|
27020
27031
|
}
|
|
27021
27032
|
}
|
|
27022
27033
|
const hasDescriptions = parsed.nodes.some(
|
|
@@ -27110,7 +27121,7 @@ var init_renderer6 = __esm({
|
|
|
27110
27121
|
init_wrapped_desc();
|
|
27111
27122
|
init_scaling();
|
|
27112
27123
|
DIAGRAM_PADDING6 = 20;
|
|
27113
|
-
NODE_FONT_SIZE =
|
|
27124
|
+
NODE_FONT_SIZE = 11;
|
|
27114
27125
|
MIN_NODE_FONT_SIZE = 9;
|
|
27115
27126
|
EDGE_LABEL_FONT_SIZE4 = 11;
|
|
27116
27127
|
EDGE_STROKE_WIDTH5 = 1.5;
|
|
@@ -47070,7 +47081,11 @@ function resolveMap(parsed, data) {
|
|
|
47070
47081
|
if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
|
|
47071
47082
|
}
|
|
47072
47083
|
const containerUnion = unionExtent(containerBoxes, points);
|
|
47073
|
-
if (containerUnion)
|
|
47084
|
+
if (containerUnion)
|
|
47085
|
+
extent2 = pad(
|
|
47086
|
+
clampContainerToCluster(containerUnion, points),
|
|
47087
|
+
PAD_FRACTION
|
|
47088
|
+
);
|
|
47074
47089
|
}
|
|
47075
47090
|
if (isPoiOnly) {
|
|
47076
47091
|
const cx = (extent2[0][0] + extent2[1][0]) / 2;
|
|
@@ -47151,6 +47166,22 @@ function mostCommonCountry(regions, poiCountries) {
|
|
|
47151
47166
|
}
|
|
47152
47167
|
return best;
|
|
47153
47168
|
}
|
|
47169
|
+
function clampContainerToCluster(container, points) {
|
|
47170
|
+
const poi = unionExtent([], points);
|
|
47171
|
+
if (!poi) return container;
|
|
47172
|
+
let [[west, south], [east, north]] = container;
|
|
47173
|
+
const [[pWest, pSouth], [pEast, pNorth]] = poi;
|
|
47174
|
+
south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
|
|
47175
|
+
north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
|
|
47176
|
+
if (east <= 180 && pEast <= 180) {
|
|
47177
|
+
west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
|
|
47178
|
+
east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
|
|
47179
|
+
}
|
|
47180
|
+
return [
|
|
47181
|
+
[west, south],
|
|
47182
|
+
[east, north]
|
|
47183
|
+
];
|
|
47184
|
+
}
|
|
47154
47185
|
function pad(e, frac) {
|
|
47155
47186
|
const dLon = (e[1][0] - e[0][0]) * frac || 1;
|
|
47156
47187
|
const dLat = (e[1][1] - e[0][1]) * frac || 1;
|
|
@@ -47163,7 +47194,7 @@ function firstError(diags) {
|
|
|
47163
47194
|
const e = diags.find((d) => d.severity === "error");
|
|
47164
47195
|
return e ? formatDgmoError(e) : null;
|
|
47165
47196
|
}
|
|
47166
|
-
var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
|
|
47197
|
+
var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, CONTAINER_OVERSHOOT_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
|
|
47167
47198
|
var init_resolver2 = __esm({
|
|
47168
47199
|
"src/map/resolver.ts"() {
|
|
47169
47200
|
"use strict";
|
|
@@ -47176,6 +47207,7 @@ var init_resolver2 = __esm({
|
|
|
47176
47207
|
WORLD_LAT_SOUTH = -58;
|
|
47177
47208
|
WORLD_LAT_NORTH = 78;
|
|
47178
47209
|
POI_ZOOM_FLOOR_DEG = 7;
|
|
47210
|
+
CONTAINER_OVERSHOOT_DEG = 8;
|
|
47179
47211
|
US_NATIONAL_LON_SPAN = 48;
|
|
47180
47212
|
REGION_ALIASES = {
|
|
47181
47213
|
// Common everyday names → the Natural-Earth display name actually shipped.
|
|
@@ -47254,6 +47286,55 @@ var init_resolver2 = __esm({
|
|
|
47254
47286
|
}
|
|
47255
47287
|
});
|
|
47256
47288
|
|
|
47289
|
+
// src/map/legend-band.ts
|
|
47290
|
+
function mapLegendGroups(legend) {
|
|
47291
|
+
const ramp = legend.ramp;
|
|
47292
|
+
const scoreGroup = ramp ? {
|
|
47293
|
+
name: ramp.metric?.trim() || "Value",
|
|
47294
|
+
entries: [],
|
|
47295
|
+
gradient: {
|
|
47296
|
+
min: ramp.min,
|
|
47297
|
+
max: ramp.max,
|
|
47298
|
+
hue: ramp.hue,
|
|
47299
|
+
base: ramp.base
|
|
47300
|
+
}
|
|
47301
|
+
} : null;
|
|
47302
|
+
const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
|
|
47303
|
+
return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
|
|
47304
|
+
}
|
|
47305
|
+
function mapLegendConfig(groups, mode) {
|
|
47306
|
+
return {
|
|
47307
|
+
groups,
|
|
47308
|
+
position: { placement: "top-center", titleRelation: "below-title" },
|
|
47309
|
+
mode,
|
|
47310
|
+
showEmptyGroups: false,
|
|
47311
|
+
showInactivePills: true
|
|
47312
|
+
};
|
|
47313
|
+
}
|
|
47314
|
+
function mapLegendTop(hasTitle, hasSubtitle) {
|
|
47315
|
+
return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
|
|
47316
|
+
}
|
|
47317
|
+
function mapLegendBand(legend, opts) {
|
|
47318
|
+
if (!legend) return 0;
|
|
47319
|
+
const groups = mapLegendGroups(legend);
|
|
47320
|
+
if (groups.length === 0) return 0;
|
|
47321
|
+
const config = mapLegendConfig(groups, opts.mode);
|
|
47322
|
+
const state = { activeGroup: legend.activeGroup };
|
|
47323
|
+
const { height } = computeLegendLayout(config, state, opts.width);
|
|
47324
|
+
if (height <= 0) return 0;
|
|
47325
|
+
return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
|
|
47326
|
+
}
|
|
47327
|
+
var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
|
|
47328
|
+
var init_legend_band = __esm({
|
|
47329
|
+
"src/map/legend-band.ts"() {
|
|
47330
|
+
"use strict";
|
|
47331
|
+
init_legend_layout();
|
|
47332
|
+
init_title_constants();
|
|
47333
|
+
LEGEND_TOP_GAP2 = 8;
|
|
47334
|
+
LEGEND_BOTTOM_GAP2 = 10;
|
|
47335
|
+
}
|
|
47336
|
+
});
|
|
47337
|
+
|
|
47257
47338
|
// src/map/colorize.ts
|
|
47258
47339
|
function assignColors(isos, adjacency) {
|
|
47259
47340
|
const sorted = [...isos].sort();
|
|
@@ -47845,12 +47926,43 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
47845
47926
|
return tagFill(r.tags, activeGroup) ?? neutralFill;
|
|
47846
47927
|
};
|
|
47847
47928
|
const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
|
|
47929
|
+
let legend = null;
|
|
47930
|
+
if (!resolved.directives.noLegend) {
|
|
47931
|
+
const legendTagGroups = resolved.tagGroups.map((g) => ({
|
|
47932
|
+
name: g.name,
|
|
47933
|
+
entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
|
|
47934
|
+
}));
|
|
47935
|
+
if (legendTagGroups.length > 0 || hasRamp) {
|
|
47936
|
+
legend = {
|
|
47937
|
+
tagGroups: legendTagGroups,
|
|
47938
|
+
activeGroup,
|
|
47939
|
+
...hasRamp && {
|
|
47940
|
+
ramp: {
|
|
47941
|
+
...resolved.directives.regionMetric !== void 0 && {
|
|
47942
|
+
metric: resolved.directives.regionMetric
|
|
47943
|
+
},
|
|
47944
|
+
min: rampMin,
|
|
47945
|
+
max: rampMax,
|
|
47946
|
+
hue: rampHue,
|
|
47947
|
+
base: rampBase
|
|
47948
|
+
}
|
|
47949
|
+
}
|
|
47950
|
+
};
|
|
47951
|
+
}
|
|
47952
|
+
}
|
|
47848
47953
|
const TITLE_GAP2 = 16;
|
|
47849
47954
|
let topPad = FIT_PAD;
|
|
47850
47955
|
if (resolved.title && resolved.pois.length > 0) {
|
|
47851
47956
|
const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
|
|
47852
47957
|
topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
|
|
47853
47958
|
}
|
|
47959
|
+
const legendBand = mapLegendBand(legend, {
|
|
47960
|
+
width,
|
|
47961
|
+
mode: opts.legendMode ?? "preview",
|
|
47962
|
+
hasTitle: Boolean(resolved.title),
|
|
47963
|
+
hasSubtitle: Boolean(resolved.subtitle)
|
|
47964
|
+
});
|
|
47965
|
+
if (legendBand > topPad) topPad = legendBand;
|
|
47854
47966
|
const fitBox = [
|
|
47855
47967
|
[FIT_PAD, topPad],
|
|
47856
47968
|
[
|
|
@@ -47868,7 +47980,7 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
47868
47980
|
const by0 = cb[0][1];
|
|
47869
47981
|
const cw = cb[1][0] - bx0;
|
|
47870
47982
|
const ch = cb[1][1] - by0;
|
|
47871
|
-
const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
|
|
47983
|
+
const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
|
|
47872
47984
|
const ox = 0;
|
|
47873
47985
|
const oy = topReserve;
|
|
47874
47986
|
const sx = cw > 0 ? width / cw : 1;
|
|
@@ -48932,30 +49044,6 @@ function layoutMap(resolved, data, size, opts) {
|
|
|
48932
49044
|
});
|
|
48933
49045
|
labels.push(...contextLabels);
|
|
48934
49046
|
}
|
|
48935
|
-
let legend = null;
|
|
48936
|
-
if (!resolved.directives.noLegend) {
|
|
48937
|
-
const tagGroups = resolved.tagGroups.map((g) => ({
|
|
48938
|
-
name: g.name,
|
|
48939
|
-
entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
|
|
48940
|
-
}));
|
|
48941
|
-
if (tagGroups.length > 0 || hasRamp) {
|
|
48942
|
-
legend = {
|
|
48943
|
-
tagGroups,
|
|
48944
|
-
activeGroup,
|
|
48945
|
-
...hasRamp && {
|
|
48946
|
-
ramp: {
|
|
48947
|
-
...resolved.directives.regionMetric !== void 0 && {
|
|
48948
|
-
metric: resolved.directives.regionMetric
|
|
48949
|
-
},
|
|
48950
|
-
min: rampMin,
|
|
48951
|
-
max: rampMax,
|
|
48952
|
-
hue: rampHue,
|
|
48953
|
-
base: rampBase
|
|
48954
|
-
}
|
|
48955
|
-
}
|
|
48956
|
-
};
|
|
48957
|
-
}
|
|
48958
|
-
}
|
|
48959
49047
|
return {
|
|
48960
49048
|
width,
|
|
48961
49049
|
height,
|
|
@@ -48991,6 +49079,7 @@ var init_layout15 = __esm({
|
|
|
48991
49079
|
init_label_layout();
|
|
48992
49080
|
init_legend_constants();
|
|
48993
49081
|
init_title_constants();
|
|
49082
|
+
init_legend_band();
|
|
48994
49083
|
init_context_labels();
|
|
48995
49084
|
FIT_PAD = 24;
|
|
48996
49085
|
RAMP_FLOOR2 = 15;
|
|
@@ -49191,6 +49280,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
|
|
|
49191
49280
|
// stretch-distorting. The in-app preview pane passes no exportDims → unset →
|
|
49192
49281
|
// keeps the global stretch-fill.
|
|
49193
49282
|
preferContain: exportDims?.preferContain ?? false,
|
|
49283
|
+
// Reserve the legend band for the mode actually drawn below (export shows
|
|
49284
|
+
// only the active group; preview keeps the inactive pills).
|
|
49285
|
+
legendMode: exportDims ? "export" : "preview",
|
|
49194
49286
|
...activeGroupOverride !== void 0 && {
|
|
49195
49287
|
activeGroup: activeGroupOverride
|
|
49196
49288
|
}
|
|
@@ -49482,30 +49574,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
|
|
|
49482
49574
|
if (layout.legend) {
|
|
49483
49575
|
const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
|
|
49484
49576
|
const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
|
|
49485
|
-
const
|
|
49486
|
-
const scoreGroup = ramp ? {
|
|
49487
|
-
name: ramp.metric?.trim() || "Value",
|
|
49488
|
-
entries: [],
|
|
49489
|
-
gradient: {
|
|
49490
|
-
min: ramp.min,
|
|
49491
|
-
max: ramp.max,
|
|
49492
|
-
hue: ramp.hue,
|
|
49493
|
-
base: ramp.base
|
|
49494
|
-
}
|
|
49495
|
-
} : null;
|
|
49496
|
-
const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
|
|
49497
|
-
const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
|
|
49577
|
+
const groups = mapLegendGroups(layout.legend);
|
|
49498
49578
|
if (groups.length > 0) {
|
|
49499
|
-
const config =
|
|
49579
|
+
const config = mapLegendConfig(
|
|
49500
49580
|
groups,
|
|
49501
|
-
|
|
49502
|
-
|
|
49503
|
-
showEmptyGroups: false,
|
|
49504
|
-
// Keep inactive siblings visible as pills so the user can click to flip
|
|
49505
|
-
// the active colouring dimension (preview only — export shows just the
|
|
49506
|
-
// active group).
|
|
49507
|
-
showInactivePills: true
|
|
49508
|
-
};
|
|
49581
|
+
exportDims ? "export" : "preview"
|
|
49582
|
+
);
|
|
49509
49583
|
const state = { activeGroup: layout.legend.activeGroup };
|
|
49510
49584
|
renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
|
|
49511
49585
|
}
|
|
@@ -49549,6 +49623,7 @@ var init_renderer16 = __esm({
|
|
|
49549
49623
|
init_title_constants();
|
|
49550
49624
|
init_color_utils();
|
|
49551
49625
|
init_legend_d3();
|
|
49626
|
+
init_legend_band();
|
|
49552
49627
|
init_layout15();
|
|
49553
49628
|
LABEL_FONT = 11;
|
|
49554
49629
|
}
|
|
@@ -49570,9 +49645,10 @@ function mapContentAspect(resolved, data, ref = REF) {
|
|
|
49570
49645
|
const aspect = w / h;
|
|
49571
49646
|
return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
|
|
49572
49647
|
}
|
|
49573
|
-
function mapExportDimensions(resolved, data, baseWidth = 1200) {
|
|
49574
|
-
const
|
|
49575
|
-
const
|
|
49648
|
+
function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
|
|
49649
|
+
const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
|
|
49650
|
+
const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
|
|
49651
|
+
const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
|
|
49576
49652
|
const width = baseWidth;
|
|
49577
49653
|
let height = Math.round(width / clamped);
|
|
49578
49654
|
let chromeReserve = 0;
|
|
@@ -49585,7 +49661,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
|
|
|
49585
49661
|
height = Math.round(chromeReserve + MIN_MAP_BAND);
|
|
49586
49662
|
floored = true;
|
|
49587
49663
|
}
|
|
49588
|
-
const preferContain = clamped !== raw || floored;
|
|
49664
|
+
const preferContain = useOverride ? floored : clamped !== raw || floored;
|
|
49589
49665
|
return { width, height, preferContain };
|
|
49590
49666
|
}
|
|
49591
49667
|
var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
|
|
@@ -55295,7 +55371,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
|
|
|
55295
55371
|
function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
|
|
55296
55372
|
const {
|
|
55297
55373
|
width,
|
|
55298
|
-
height,
|
|
55299
55374
|
tooltip,
|
|
55300
55375
|
solid,
|
|
55301
55376
|
textColor,
|
|
@@ -55344,8 +55419,7 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
|
|
|
55344
55419
|
const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
|
|
55345
55420
|
const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
|
|
55346
55421
|
const innerWidth = width - margin.left - margin.right;
|
|
55347
|
-
const
|
|
55348
|
-
const rowH = Math.min(ctx.structural(28), availInnerHeight / sorted.length);
|
|
55422
|
+
const rowH = ctx.structural(28);
|
|
55349
55423
|
const innerHeight = rowH * sorted.length;
|
|
55350
55424
|
const usedHeight = margin.top + innerHeight + margin.bottom;
|
|
55351
55425
|
const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
|
|
@@ -57926,7 +58000,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
|
|
|
57926
58000
|
}
|
|
57927
58001
|
}
|
|
57928
58002
|
const mapResolved = resolveMap2(mapParsed, mapData);
|
|
57929
|
-
const dims2 = mapExportDimensions2(
|
|
58003
|
+
const dims2 = mapExportDimensions2(
|
|
58004
|
+
mapResolved,
|
|
58005
|
+
mapData,
|
|
58006
|
+
EXPORT_WIDTH,
|
|
58007
|
+
options?.mapAspect
|
|
58008
|
+
);
|
|
57930
58009
|
const container2 = createExportContainer(dims2.width, dims2.height);
|
|
57931
58010
|
renderMapForExport2(
|
|
57932
58011
|
container2,
|
|
@@ -2786,7 +2786,8 @@ route Miami style: arc
|
|
|
2786
2786
|
- Title is the declaration line; `caption` (data-source attribution, travels with the exported PNG) is the only chrome directive. There is no `subtitle`.
|
|
2787
2787
|
- Legend auto-composes below the title: the value ramp + `region-metric` and each tag group are **selectable colouring groups** (collapse/activate to flip the fill); POI size (`poi-metric`) and edge thickness (`flow-metric`) are self-evident from scale and carry no legend key in v1. `no-legend` suppresses all of it.
|
|
2788
2788
|
- **Region and POI labels are on by default.** Region labels auto-fit **full → abbrev → hide** (a US-state 2-letter abbreviation is tried when the full name doesn't fit; other regions degrade full → hide); POI labels are collision-managed. Labels render **on the map** (export-safe), escalating inline → leader line → numbered pin in dense clusters; markers never move. A wide map in a narrow column (< ~480px) prefers abbreviations and drops reference relief, as if zoomed out.
|
|
2789
|
-
- **Cosmetic features are on by default**; the only switches are bare `no-*` opt-outs (no positive opt-in flag): `no-coastline`, `no-relief`, `no-context-labels`, `no-region-labels`, `no-poi-labels`, `no-legend`. A plain look = the four basemap flags together.
|
|
2789
|
+
- **Cosmetic features are on by default**; the only switches are bare `no-*` opt-outs (no positive opt-in flag): `no-coastline`, `no-relief`, `no-context-labels`, `no-region-labels`, `no-poi-labels`, `no-legend`, `no-colorize`. A plain look = the four basemap flags together (`no-colorize` is **not** one of the four — it toggles region *fill style*, not a basemap backdrop layer).
|
|
2790
|
+
- **Colorize (distinct political fills) is the default for any map without region data.** Unless a region carries data (a `value:` or a tag), every region drawn at the resolved extent is filled a **distinct light pastel** such that no two bordering regions share a hue — the conventional "colour the countries/states so neighbours separate" look, with zero config. It applies to named-region maps, POI/route-only maps, and even a bare `map` (the whole world colours as the backdrop). The fills are **non-semantic** (no legend entry) and **extent-independent** (a region's colour is the same at any width and in an inset). A direct trailing colour (`Texas red`) paints on top as a highlight and does not suppress colorize; adding any `value:`/tag flips the map to the data dress (colorize auto-suppressed, no error). `no-colorize` forces the plain green-land + blue-water dress — useful when many POIs/routes should pop against a calm map.
|
|
2790
2791
|
|
|
2791
2792
|
### Name resolution
|
|
2792
2793
|
|
|
@@ -2802,7 +2803,7 @@ route Miami style: arc
|
|
|
2802
2803
|
|
|
2803
2804
|
### Directives & reserved keys
|
|
2804
2805
|
|
|
2805
|
-
The directive set is **
|
|
2806
|
+
The directive set is **13, all colon-free**: six naming intent the renderer can't infer — `region-metric`, `poi-metric`, `flow-metric`, `locale`, `active-tag`, `caption` — and seven `no-*` cosmetic opt-outs — `no-legend`, `no-coastline`, `no-relief`, `no-context-labels`, `no-region-labels`, `no-poi-labels`, `no-colorize`. There is **no** `projection`, `scale`, `subtitle`, `surface`, `region`, or label-enum directive, and cosmetics have no positive opt-in form. Reserved metadata keys (need colons): `value`, `label`, `style` (`value` = the one numeric channel: region shade / POI size / edge thickness); `surface:` is no longer recognized. A bare US state postal code resolves to that state (`poi Portland OR` → Oregon; `CA` = California). Coordinates are positional (no `at:` key). Projection is inferred from extent + whether the map carries data (US → albers-usa; world data → Equal Earth; world reference → natural-earth; regional → mercator) and cannot be overridden.
|
|
2806
2807
|
|
|
2807
2808
|
---
|
|
2808
2809
|
|
package/package.json
CHANGED
|
@@ -35,7 +35,9 @@ import { ScaleContext } from '../utils/scaling';
|
|
|
35
35
|
|
|
36
36
|
// ── Constants (aligned with infra pattern) ─────────────────
|
|
37
37
|
const DIAGRAM_PADDING = 20;
|
|
38
|
-
|
|
38
|
+
// Box labels run smaller than the 13px org/infra use — boxes-and-lines nodes are
|
|
39
|
+
// narrower (~97px), so a smaller label fits more text per line before wrapping.
|
|
40
|
+
const NODE_FONT_SIZE = 11;
|
|
39
41
|
const MIN_NODE_FONT_SIZE = 9;
|
|
40
42
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
41
43
|
const EDGE_STROKE_WIDTH = 1.5;
|
|
@@ -429,7 +431,7 @@ export function renderBoxesAndLines(
|
|
|
429
431
|
// between a tag group and the metric label, the tag group wins (AC9).
|
|
430
432
|
const matchColorGroup = (v: string): string | null => {
|
|
431
433
|
const lv = v.trim().toLowerCase();
|
|
432
|
-
if (lv === 'none') return null;
|
|
434
|
+
if (lv === '' || lv === 'none') return null;
|
|
433
435
|
const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
|
|
434
436
|
if (tg) return tg.name;
|
|
435
437
|
if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
|
|
@@ -1087,6 +1089,61 @@ export function renderBoxesAndLines(
|
|
|
1087
1089
|
fullText.length > 200 ? fullText.slice(0, 199) + '\u2026' : fullText;
|
|
1088
1090
|
nodeG.append('title').text(tooltipText);
|
|
1089
1091
|
}
|
|
1092
|
+
} else if (parsed.showValues && node.value !== undefined) {
|
|
1093
|
+
// Plain node with show-values: label header + thin divider + a
|
|
1094
|
+
// "Metric: value" line below (org/infra card style), instead of a
|
|
1095
|
+
// vertically-centered label with a floating number.
|
|
1096
|
+
const valueLabel = parsed.boxMetric
|
|
1097
|
+
? `${parsed.boxMetric}: ${node.value}`
|
|
1098
|
+
: String(node.value);
|
|
1099
|
+
// Fixed header zone (not label-height-driven) so the divider sits at a
|
|
1100
|
+
// UNIFORM Y across every box, regardless of label line count (infra/org
|
|
1101
|
+
// both anchor the separator to a constant header height).
|
|
1102
|
+
const headerH = ln.height / 2;
|
|
1103
|
+
const sepY = -ln.height / 2 + headerH;
|
|
1104
|
+
const fitted = fitLabelToHeader(node.label, ln.width, 2);
|
|
1105
|
+
const labelLineH = fitted.fontSize * 1.3;
|
|
1106
|
+
const labelTotalH = fitted.lines.length * labelLineH;
|
|
1107
|
+
const headerCenterY = -ln.height / 2 + headerH / 2;
|
|
1108
|
+
for (let li = 0; li < fitted.lines.length; li++) {
|
|
1109
|
+
nodeG
|
|
1110
|
+
.append('text')
|
|
1111
|
+
.attr('x', 0)
|
|
1112
|
+
.attr(
|
|
1113
|
+
'y',
|
|
1114
|
+
headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
|
|
1115
|
+
)
|
|
1116
|
+
.attr('text-anchor', 'middle')
|
|
1117
|
+
.attr('dominant-baseline', 'central')
|
|
1118
|
+
.attr('font-size', fitted.fontSize)
|
|
1119
|
+
.attr('font-weight', '600')
|
|
1120
|
+
.attr('fill', colors.text)
|
|
1121
|
+
// In-bounds by loop guard.
|
|
1122
|
+
.text(fitted.lines[li]!);
|
|
1123
|
+
}
|
|
1124
|
+
// Thin divider under the title — a tint of the box's own stroke colour
|
|
1125
|
+
// (matches org / infra card separators), not a neutral text line.
|
|
1126
|
+
nodeG
|
|
1127
|
+
.append('line')
|
|
1128
|
+
.attr('x1', -ln.width / 2)
|
|
1129
|
+
.attr('y1', sepY)
|
|
1130
|
+
.attr('x2', ln.width / 2)
|
|
1131
|
+
.attr('y2', sepY)
|
|
1132
|
+
.attr('stroke', colors.stroke)
|
|
1133
|
+
.attr('stroke-opacity', 0.3)
|
|
1134
|
+
.attr('stroke-width', 1);
|
|
1135
|
+
// "Metric: value" centered in the space below the divider.
|
|
1136
|
+
nodeG
|
|
1137
|
+
.append('text')
|
|
1138
|
+
.attr('class', 'bl-node-value')
|
|
1139
|
+
.attr('x', 0)
|
|
1140
|
+
.attr('y', (sepY + ln.height / 2) / 2)
|
|
1141
|
+
.attr('text-anchor', 'middle')
|
|
1142
|
+
.attr('dominant-baseline', 'central')
|
|
1143
|
+
.attr('font-size', VALUE_FONT_SIZE)
|
|
1144
|
+
.attr('fill', colors.text)
|
|
1145
|
+
.attr('opacity', 0.85)
|
|
1146
|
+
.text(valueLabel);
|
|
1090
1147
|
} else {
|
|
1091
1148
|
const maxLabelLines = Math.max(
|
|
1092
1149
|
2,
|
|
@@ -1110,56 +1167,46 @@ export function renderBoxesAndLines(
|
|
|
1110
1167
|
}
|
|
1111
1168
|
}
|
|
1112
1169
|
|
|
1113
|
-
// ── show-values
|
|
1114
|
-
//
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
if (
|
|
1170
|
+
// ── show-values on a DESCRIBED node ── the body is already full, so the
|
|
1171
|
+
// value rides in a top-right corner badge (plain nodes are handled in the
|
|
1172
|
+
// header/divider branch above; a described node with descriptions hidden
|
|
1173
|
+
// also falls through to that plain branch).
|
|
1174
|
+
if (
|
|
1175
|
+
parsed.showValues &&
|
|
1176
|
+
node.value !== undefined &&
|
|
1177
|
+
desc &&
|
|
1178
|
+
desc.length > 0 &&
|
|
1179
|
+
!hideDescriptions
|
|
1180
|
+
) {
|
|
1118
1181
|
const valueText = String(node.value);
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
.text(valueText);
|
|
1148
|
-
} else {
|
|
1149
|
-
// Plain node: value centered just above the bottom edge.
|
|
1150
|
-
nodeG
|
|
1151
|
-
.append('text')
|
|
1152
|
-
.attr('class', 'bl-node-value')
|
|
1153
|
-
.attr('x', 0)
|
|
1154
|
-
.attr('y', ln.height / 2 - VALUE_FONT_SIZE)
|
|
1155
|
-
.attr('text-anchor', 'middle')
|
|
1156
|
-
.attr('dominant-baseline', 'central')
|
|
1157
|
-
.attr('font-size', VALUE_FONT_SIZE)
|
|
1158
|
-
.attr('font-weight', '600')
|
|
1159
|
-
.attr('fill', colors.text)
|
|
1160
|
-
.attr('opacity', 0.8)
|
|
1161
|
-
.text(valueText);
|
|
1162
|
-
}
|
|
1182
|
+
const padX = 6;
|
|
1183
|
+
const padY = 5;
|
|
1184
|
+
const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO + 8;
|
|
1185
|
+
const bh = VALUE_FONT_SIZE + 4;
|
|
1186
|
+
// Clamp to the left padding so a long value on a narrow node never
|
|
1187
|
+
// slides past the box edge / over the label (R2-6 / AC23).
|
|
1188
|
+
const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
|
|
1189
|
+
const by = -ln.height / 2 + 4;
|
|
1190
|
+
nodeG
|
|
1191
|
+
.append('rect')
|
|
1192
|
+
.attr('x', bx)
|
|
1193
|
+
.attr('y', by)
|
|
1194
|
+
.attr('width', bw)
|
|
1195
|
+
.attr('height', bh)
|
|
1196
|
+
.attr('rx', 3)
|
|
1197
|
+
.attr('fill', palette.bg)
|
|
1198
|
+
.attr('opacity', 0.85);
|
|
1199
|
+
nodeG
|
|
1200
|
+
.append('text')
|
|
1201
|
+
.attr('class', 'bl-node-value')
|
|
1202
|
+
.attr('x', bx + bw - padX)
|
|
1203
|
+
.attr('y', by + padY)
|
|
1204
|
+
.attr('text-anchor', 'end')
|
|
1205
|
+
.attr('dominant-baseline', 'central')
|
|
1206
|
+
.attr('font-size', VALUE_FONT_SIZE)
|
|
1207
|
+
.attr('font-weight', '600')
|
|
1208
|
+
.attr('fill', palette.textMuted)
|
|
1209
|
+
.text(valueText);
|
|
1163
1210
|
}
|
|
1164
1211
|
}
|
|
1165
1212
|
|
package/src/d3.ts
CHANGED
|
@@ -4238,7 +4238,6 @@ function renderTimelineHorizontalTimeSort(
|
|
|
4238
4238
|
): void {
|
|
4239
4239
|
const {
|
|
4240
4240
|
width,
|
|
4241
|
-
height,
|
|
4242
4241
|
tooltip,
|
|
4243
4242
|
solid,
|
|
4244
4243
|
textColor,
|
|
@@ -4301,14 +4300,18 @@ function renderTimelineHorizontalTimeSort(
|
|
|
4301
4300
|
? -(topScaleH + markerReserve + ERA_ROW_H / 2)
|
|
4302
4301
|
: 0;
|
|
4303
4302
|
const innerWidth = width - margin.left - margin.right;
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
//
|
|
4307
|
-
//
|
|
4308
|
-
//
|
|
4309
|
-
//
|
|
4310
|
-
//
|
|
4311
|
-
//
|
|
4303
|
+
// Each event gets a fixed comfortable row. The old behaviour compressed rowH
|
|
4304
|
+
// to fit the container height (`min(28, avail / n)`), but that only ever
|
|
4305
|
+
// shrank rows BELOW the 22px bar height — cramming events into overlap when
|
|
4306
|
+
// the host surface was shorter than the content required (e.g. the app's
|
|
4307
|
+
// fixed-height embedded-diagram surface). A constant rowH never overlaps:
|
|
4308
|
+
// when the container is taller than needed the SVG shrinks to the content
|
|
4309
|
+
// (top-aligned via preserveAspectRatio); when shorter, the SVG grows past it
|
|
4310
|
+
// and the host collapses/expands to the rendered height. This also makes the
|
|
4311
|
+
// interactive preview match the exported image, which already used rowH=28.
|
|
4312
|
+
const rowH = ctx.structural(28);
|
|
4313
|
+
// Draw the era bands and time axis to the content height (not the full
|
|
4314
|
+
// container) so the axis sits just below the last event.
|
|
4312
4315
|
const innerHeight = rowH * sorted.length;
|
|
4313
4316
|
const usedHeight = margin.top + innerHeight + margin.bottom;
|
|
4314
4317
|
|
|
@@ -7751,6 +7754,10 @@ export async function renderForExport(
|
|
|
7751
7754
|
// here — the Node fs `loadMapData()` seam can't run in a browser. CLI/SSR
|
|
7752
7755
|
// omit this and fall back to the fs loader.
|
|
7753
7756
|
mapData?: import('./map/resolved-types').MapData;
|
|
7757
|
+
// WYSIWYG map export: the live preview pane's displayed aspect (w/h). When
|
|
7758
|
+
// set, the map canvas adopts it + stretch-fills so the PNG matches the
|
|
7759
|
+
// on-screen map. The app passes this; headless consumers omit it.
|
|
7760
|
+
mapAspect?: number;
|
|
7754
7761
|
}
|
|
7755
7762
|
): Promise<string> {
|
|
7756
7763
|
const exportMode = options?.exportMode ?? false;
|
|
@@ -8491,7 +8498,12 @@ export async function renderForExport(
|
|
|
8491
8498
|
// aspect (world ~2.3:1, a region taller, etc.) instead of the fixed 800, so the
|
|
8492
8499
|
// export matches the content's natural shape — no vertical stretch, no
|
|
8493
8500
|
// letterbox bands. `preferContain` rides along to the renderer.
|
|
8494
|
-
const dims = mapExportDimensions(
|
|
8501
|
+
const dims = mapExportDimensions(
|
|
8502
|
+
mapResolved,
|
|
8503
|
+
mapData,
|
|
8504
|
+
EXPORT_WIDTH,
|
|
8505
|
+
options?.mapAspect
|
|
8506
|
+
);
|
|
8495
8507
|
const container = createExportContainer(dims.width, dims.height);
|
|
8496
8508
|
renderMapForExport(
|
|
8497
8509
|
container,
|