@diagrammo/dgmo 0.47.0 → 0.48.1
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.js +11 -11
- package/dist/auto.js +16 -16
- package/dist/auto.mjs +41 -12
- package/dist/block.js +4 -4
- package/dist/{chunk-FNB3PNGF.js → chunk-7DTWEFUE.js} +3 -3
- package/dist/{chunk-2DXMZRWJ.js → chunk-B65G5DNL.js} +8 -1
- package/dist/{chunk-RZCF2VVM.js → chunk-B7JUCQUA.js} +3 -1
- package/dist/{chunk-DU7W62N5.js → chunk-FANX6UJR.js} +3 -3
- package/dist/{chunk-6FZYCKXB.js → chunk-HSH73DP6.js} +2 -2
- package/dist/{chunk-P4RH4OSP.js → chunk-ILKORZ3H.js} +2 -2
- package/dist/{chunk-Y5QGVTVC.js → chunk-JTSMEYIZ.js} +7 -3
- package/dist/{chunk-RCERWOJR.js → chunk-ODZVHU3R.js} +1 -2
- package/dist/{chunk-DZAQBW35.js → chunk-SGZYXELT.js} +3 -7
- package/dist/{chunk-CUVHQHZ7.js → chunk-THXCFRTU.js} +3 -3
- package/dist/{chunk-IH6ZMVQ5.js → chunk-UC7TFHUM.js} +29 -11
- package/dist/{chunk-EMS2SK26.js → chunk-ZBWMSD5N.js} +3 -1
- package/dist/cli.cjs +15 -15
- package/dist/{dgmo-router-DZCIDUO5.js → dgmo-router-X35VLZLM.js} +3 -3
- package/dist/element.js +16 -16
- package/dist/element.mjs +41 -12
- package/dist/{flowchart-renderer-ZAEOL4DX.js → flowchart-renderer-SNDSII4D.js} +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -4
- package/dist/{parser-FUSKFGZN.js → parser-2EWECDSA.js} +2 -2
- package/dist/{renderer-UWA5XS74.js → renderer-4JHRUBE6.js} +3 -3
- package/dist/{renderer-WZRREQ2P.js → renderer-5MYBMBYO.js} +2 -2
- package/dist/{renderer-TOCOVLHS.js → renderer-B2OMQIQG.js} +3 -3
- package/dist/{renderer-2XQQQ6QR.js → renderer-N43WR5GV.js} +3 -3
- package/dist/{renderer-T7YKC5VP.js → renderer-ZRNG54CW.js} +3 -3
- package/dist/{state-renderer-RFYHSP4K.js → state-renderer-B4DQBM6M.js} +2 -2
- package/package.json +23 -71
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -5719
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.cjs +0 -526
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -160
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/dist/themes-1-CuKpeH.d.cts +0 -908
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/parser.ts +0 -427
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/layout.ts +0 -2441
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/calculator.ts +0 -979
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/time-ticks.ts +0 -253
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
package/src/pyramid/types.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { DgmoError } from '../diagnostics';
|
|
2
|
-
|
|
3
|
-
// ============================================================
|
|
4
|
-
// Pyramid Diagram — Parsed Types
|
|
5
|
-
// ============================================================
|
|
6
|
-
|
|
7
|
-
export interface PyramidLayer {
|
|
8
|
-
readonly label: string;
|
|
9
|
-
readonly lineNumber: number;
|
|
10
|
-
/** Optional palette color name (red/green/blue/…). */
|
|
11
|
-
readonly color?: string;
|
|
12
|
-
/** Description lines — from bare pipe shorthand or indented body. */
|
|
13
|
-
readonly description: readonly string[];
|
|
14
|
-
/** Unconsumed pipe metadata (reserved for future use). */
|
|
15
|
-
readonly metadata: Readonly<Record<string, string>>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ParsedPyramid {
|
|
19
|
-
readonly type: 'pyramid';
|
|
20
|
-
readonly title: string;
|
|
21
|
-
readonly titleLineNumber: number;
|
|
22
|
-
readonly layers: readonly PyramidLayer[];
|
|
23
|
-
/** When true, apex points down instead of up. */
|
|
24
|
-
readonly inverted: boolean;
|
|
25
|
-
readonly options: Readonly<Record<string, string>>;
|
|
26
|
-
readonly diagnostics: readonly DgmoError[];
|
|
27
|
-
readonly error: string | null;
|
|
28
|
-
}
|
package/src/quadrant/parser.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// quadrant parser door — Story 109.2 (arch-review).
|
|
3
|
-
//
|
|
4
|
-
// The six visualizations share one parse state machine (visualizations/parse.ts);
|
|
5
|
-
// this is quadrant's typed entry point into it, returning the narrowed
|
|
6
|
-
// ParsedQuadrant shape. The registry binds the 'quadrant' chart type to this door.
|
|
7
|
-
// ============================================================
|
|
8
|
-
|
|
9
|
-
import type { PaletteColors } from '../palettes';
|
|
10
|
-
import { parseVisualization } from '../visualizations/parse';
|
|
11
|
-
import type { ParsedQuadrant } from '../visualizations/types';
|
|
12
|
-
|
|
13
|
-
export function parseQuadrant(
|
|
14
|
-
content: string,
|
|
15
|
-
palette?: PaletteColors
|
|
16
|
-
): ParsedQuadrant {
|
|
17
|
-
return parseVisualization(content, palette) as ParsedQuadrant;
|
|
18
|
-
}
|
package/src/quadrant/renderer.ts
DELETED
|
@@ -1,638 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Quadrant renderer — Story 109.2 (arch-review). Extracted from d3.ts.
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import * as d3Scale from 'd3-scale';
|
|
6
|
-
import * as d3Selection from 'd3-selection';
|
|
7
|
-
import { computeQuadrantPointLabels, type LabelRect } from '../label-layout';
|
|
8
|
-
import { measureText, wrapTextToWidth } from '../utils/text-measure';
|
|
9
|
-
import type { D3ExportDimensions } from '../utils/d3-types';
|
|
10
|
-
import { ScaleContext } from '../utils/scaling';
|
|
11
|
-
import { initD3Chart, renderChartTitle } from '../utils/d3-helpers';
|
|
12
|
-
import type { ParsedQuadrant, QuadrantLabel } from '../visualizations/types';
|
|
13
|
-
import type { PaletteColors } from '../palettes';
|
|
14
|
-
import { mix, themeBaseBg } from '../palettes/color-utils';
|
|
15
|
-
|
|
16
|
-
// Quadrant Chart Renderer
|
|
17
|
-
// ============================================================
|
|
18
|
-
|
|
19
|
-
type QuadrantPosition =
|
|
20
|
-
| 'top-right'
|
|
21
|
-
| 'top-left'
|
|
22
|
-
| 'bottom-left'
|
|
23
|
-
| 'bottom-right';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Renders a quadrant chart using D3.
|
|
27
|
-
* Displays 4 colored quadrant regions, axis labels, quadrant labels, and data points.
|
|
28
|
-
*/
|
|
29
|
-
export function renderQuadrant(
|
|
30
|
-
container: HTMLDivElement,
|
|
31
|
-
parsed: ParsedQuadrant,
|
|
32
|
-
palette: PaletteColors,
|
|
33
|
-
isDark: boolean,
|
|
34
|
-
onClickItem?: (lineNumber: number) => void,
|
|
35
|
-
exportDims?: D3ExportDimensions
|
|
36
|
-
): void {
|
|
37
|
-
const {
|
|
38
|
-
quadrantLabels,
|
|
39
|
-
quadrantPoints,
|
|
40
|
-
quadrantXAxis,
|
|
41
|
-
quadrantYAxis,
|
|
42
|
-
quadrantTitleLineNumber,
|
|
43
|
-
quadrantXAxisLineNumber,
|
|
44
|
-
quadrantYAxisLineNumber,
|
|
45
|
-
} = parsed;
|
|
46
|
-
const title = parsed.noTitle ? null : parsed.title;
|
|
47
|
-
|
|
48
|
-
if (quadrantPoints.length === 0) return;
|
|
49
|
-
|
|
50
|
-
const init = initD3Chart(container, palette, exportDims);
|
|
51
|
-
if (!init) return;
|
|
52
|
-
const { svg, width, height, textColor } = init;
|
|
53
|
-
const borderColor = palette.border;
|
|
54
|
-
|
|
55
|
-
const idealWidth = 600;
|
|
56
|
-
const ctx = exportDims
|
|
57
|
-
? ScaleContext.identity()
|
|
58
|
-
: ScaleContext.from(width, idealWidth);
|
|
59
|
-
|
|
60
|
-
svg.attr('preserveAspectRatio', 'xMidYMid meet');
|
|
61
|
-
if (ctx.isBelowFloor) {
|
|
62
|
-
svg.attr('width', '100%');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const defaultColors = [
|
|
66
|
-
palette.colors.blue,
|
|
67
|
-
palette.colors.green,
|
|
68
|
-
palette.colors.yellow,
|
|
69
|
-
palette.colors.purple,
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
const hasXAxis = !!quadrantXAxis;
|
|
73
|
-
const hasYAxis = !!quadrantYAxis;
|
|
74
|
-
const margin = {
|
|
75
|
-
top: title ? ctx.aesthetic(60) : ctx.aesthetic(30),
|
|
76
|
-
right: ctx.aesthetic(30),
|
|
77
|
-
bottom: hasXAxis ? ctx.aesthetic(70) : ctx.aesthetic(40),
|
|
78
|
-
left: hasYAxis ? ctx.aesthetic(80) : ctx.aesthetic(40),
|
|
79
|
-
};
|
|
80
|
-
const chartWidth = width - margin.left - margin.right;
|
|
81
|
-
const chartHeight = height - margin.top - margin.bottom;
|
|
82
|
-
|
|
83
|
-
// Scales: data uses 0-1 range
|
|
84
|
-
const xScale = d3Scale.scaleLinear().domain([0, 1]).range([0, chartWidth]);
|
|
85
|
-
const yScale = d3Scale.scaleLinear().domain([0, 1]).range([chartHeight, 0]);
|
|
86
|
-
|
|
87
|
-
// Title
|
|
88
|
-
renderChartTitle(
|
|
89
|
-
svg,
|
|
90
|
-
title,
|
|
91
|
-
quadrantTitleLineNumber,
|
|
92
|
-
width,
|
|
93
|
-
textColor,
|
|
94
|
-
onClickItem
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
// Chart group (translated by margins)
|
|
98
|
-
const chartG = svg
|
|
99
|
-
.append('g')
|
|
100
|
-
.attr('transform', `translate(${margin.left}, ${margin.top})`);
|
|
101
|
-
|
|
102
|
-
const bg = themeBaseBg(palette, isDark);
|
|
103
|
-
|
|
104
|
-
// Full palette color for a quadrant (used for border and label tinting)
|
|
105
|
-
const getQuadrantColor = (
|
|
106
|
-
label: QuadrantLabel | null,
|
|
107
|
-
defaultIdx: number
|
|
108
|
-
): string => {
|
|
109
|
-
// defaultColors is non-empty; modulo guarantees in-bounds.
|
|
110
|
-
return label?.color ?? defaultColors[defaultIdx % defaultColors.length]!;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// Muted fill: palette color blended 30% toward bg — matches other chart fill style
|
|
114
|
-
const getQuadrantFill = (
|
|
115
|
-
label: QuadrantLabel | null,
|
|
116
|
-
defaultIdx: number
|
|
117
|
-
): string => {
|
|
118
|
-
return mix(getQuadrantColor(label, defaultIdx), bg, 30);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// Quadrant definitions: position, rect bounds, label position
|
|
122
|
-
const quadrantDefs: {
|
|
123
|
-
position: QuadrantPosition;
|
|
124
|
-
x: number;
|
|
125
|
-
y: number;
|
|
126
|
-
w: number;
|
|
127
|
-
h: number;
|
|
128
|
-
labelX: number;
|
|
129
|
-
labelY: number;
|
|
130
|
-
label: QuadrantLabel | null;
|
|
131
|
-
colorIdx: number;
|
|
132
|
-
}[] = [
|
|
133
|
-
{
|
|
134
|
-
position: 'top-left',
|
|
135
|
-
x: 0,
|
|
136
|
-
y: 0,
|
|
137
|
-
w: chartWidth / 2,
|
|
138
|
-
h: chartHeight / 2,
|
|
139
|
-
labelX: chartWidth / 4,
|
|
140
|
-
labelY: chartHeight / 4,
|
|
141
|
-
label: quadrantLabels.topLeft,
|
|
142
|
-
colorIdx: 1, // green
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
position: 'top-right',
|
|
146
|
-
x: chartWidth / 2,
|
|
147
|
-
y: 0,
|
|
148
|
-
w: chartWidth / 2,
|
|
149
|
-
h: chartHeight / 2,
|
|
150
|
-
labelX: (chartWidth * 3) / 4,
|
|
151
|
-
labelY: chartHeight / 4,
|
|
152
|
-
label: quadrantLabels.topRight,
|
|
153
|
-
colorIdx: 0, // blue
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
position: 'bottom-left',
|
|
157
|
-
x: 0,
|
|
158
|
-
y: chartHeight / 2,
|
|
159
|
-
w: chartWidth / 2,
|
|
160
|
-
h: chartHeight / 2,
|
|
161
|
-
labelX: chartWidth / 4,
|
|
162
|
-
labelY: (chartHeight * 3) / 4,
|
|
163
|
-
label: quadrantLabels.bottomLeft,
|
|
164
|
-
colorIdx: 2, // yellow
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
position: 'bottom-right',
|
|
168
|
-
x: chartWidth / 2,
|
|
169
|
-
y: chartHeight / 2,
|
|
170
|
-
w: chartWidth / 2,
|
|
171
|
-
h: chartHeight / 2,
|
|
172
|
-
labelX: (chartWidth * 3) / 4,
|
|
173
|
-
labelY: (chartHeight * 3) / 4,
|
|
174
|
-
label: quadrantLabels.bottomRight,
|
|
175
|
-
colorIdx: 3, // purple
|
|
176
|
-
},
|
|
177
|
-
];
|
|
178
|
-
|
|
179
|
-
// Draw quadrant rectangles
|
|
180
|
-
const quadrantRects = chartG
|
|
181
|
-
.selectAll('rect.quadrant')
|
|
182
|
-
.data(quadrantDefs)
|
|
183
|
-
.enter()
|
|
184
|
-
.append('rect')
|
|
185
|
-
.attr('class', 'quadrant')
|
|
186
|
-
.attr('x', (d) => d.x)
|
|
187
|
-
.attr('y', (d) => d.y)
|
|
188
|
-
.attr('width', (d) => d.w)
|
|
189
|
-
.attr('height', (d) => d.h)
|
|
190
|
-
.attr('fill', (d) => getQuadrantFill(d.label, d.colorIdx))
|
|
191
|
-
.attr('stroke', (d) => getQuadrantColor(d.label, d.colorIdx))
|
|
192
|
-
.attr('stroke-width', ctx.structural(2));
|
|
193
|
-
|
|
194
|
-
// White text for points; quadrant labels use a muted text color (consistent across all quadrants)
|
|
195
|
-
const shadowColor = 'rgba(0,0,0,0.4)';
|
|
196
|
-
|
|
197
|
-
// Single muted shade of textColor — watermark-style, readable against any quadrant fill
|
|
198
|
-
const quadrantLabelColor = mix(textColor, bg, 35);
|
|
199
|
-
|
|
200
|
-
// Scale label font size to fit within quadrant bounds, wrapping into multiple lines if needed
|
|
201
|
-
const LABEL_MAX_FONT = ctx.text(48);
|
|
202
|
-
const LABEL_MIN_FONT = ctx.text(14);
|
|
203
|
-
const LABEL_PAD = ctx.aesthetic(40);
|
|
204
|
-
|
|
205
|
-
interface QuadrantLabelLayout {
|
|
206
|
-
lines: string[];
|
|
207
|
-
fontSize: number;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const quadrantLabelLayout = (
|
|
211
|
-
text: string,
|
|
212
|
-
qw: number,
|
|
213
|
-
qh: number
|
|
214
|
-
): QuadrantLabelLayout => {
|
|
215
|
-
const availW = qw - LABEL_PAD;
|
|
216
|
-
const availH = qh - LABEL_PAD;
|
|
217
|
-
|
|
218
|
-
// Try single line first
|
|
219
|
-
if (measureText(text, LABEL_MAX_FONT) <= availW) {
|
|
220
|
-
const fs = Math.min(LABEL_MAX_FONT, availH);
|
|
221
|
-
return {
|
|
222
|
-
lines: [text],
|
|
223
|
-
fontSize: Math.max(LABEL_MIN_FONT, Math.round(fs)),
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Try wrapping into 2+ lines: greedily pack words so each line fits availW
|
|
228
|
-
const wrapLines = (fs: number): string[] =>
|
|
229
|
-
wrapTextToWidth(text, fs, availW);
|
|
230
|
-
|
|
231
|
-
// Binary-search for largest font size where wrapped text fits both width and height
|
|
232
|
-
let lo = LABEL_MIN_FONT;
|
|
233
|
-
let hi = LABEL_MAX_FONT;
|
|
234
|
-
let bestLines = wrapLines(lo);
|
|
235
|
-
let bestFs = lo;
|
|
236
|
-
while (lo <= hi) {
|
|
237
|
-
const mid = Math.round((lo + hi) / 2);
|
|
238
|
-
const lines = wrapLines(mid);
|
|
239
|
-
const totalH = lines.length * mid * 1.2; // line height ~1.2em
|
|
240
|
-
const maxLineW = Math.max(...lines.map((l) => measureText(l, mid)));
|
|
241
|
-
if (maxLineW <= availW && totalH <= availH) {
|
|
242
|
-
bestFs = mid;
|
|
243
|
-
bestLines = lines;
|
|
244
|
-
lo = mid + 1;
|
|
245
|
-
} else {
|
|
246
|
-
hi = mid - 1;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return { lines: bestLines, fontSize: Math.max(LABEL_MIN_FONT, bestFs) };
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
// Draw quadrant labels (large, centered, darkened shade of fill — recedes behind points)
|
|
253
|
-
// Pre-compute layout (lines + font size) for each quadrant label
|
|
254
|
-
const qw = chartWidth / 2;
|
|
255
|
-
const qh = chartHeight / 2;
|
|
256
|
-
const quadrantDefsWithLabel = quadrantDefs.filter((d) => d.label !== null);
|
|
257
|
-
const labelLayouts = new Map(
|
|
258
|
-
quadrantDefsWithLabel.map((d) => [
|
|
259
|
-
d.label!.text,
|
|
260
|
-
quadrantLabelLayout(d.label!.text, qw, qh),
|
|
261
|
-
])
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
// Renders the original watermark content (quadrant name) into a text element.
|
|
265
|
-
// Extracted so point hover can swap it for a hover state and restore on leave.
|
|
266
|
-
const renderWatermarkOriginal = (
|
|
267
|
-
textEl: SVGTextElement,
|
|
268
|
-
d: (typeof quadrantDefsWithLabel)[number]
|
|
269
|
-
) => {
|
|
270
|
-
const layout = labelLayouts.get(d.label!.text)!;
|
|
271
|
-
const el = d3Selection.select(textEl);
|
|
272
|
-
el.text(null)
|
|
273
|
-
.attr('font-size', `${layout.fontSize}px`)
|
|
274
|
-
.attr('font-weight', '700');
|
|
275
|
-
if (layout.lines.length === 1) {
|
|
276
|
-
// In-bounds by length === 1 check.
|
|
277
|
-
el.text(layout.lines[0]!);
|
|
278
|
-
} else {
|
|
279
|
-
const lineH = layout.fontSize * 1.2;
|
|
280
|
-
const totalH = layout.lines.length * lineH;
|
|
281
|
-
const startY = -totalH / 2 + lineH / 2;
|
|
282
|
-
layout.lines.forEach((line, i) => {
|
|
283
|
-
el.append('tspan')
|
|
284
|
-
.attr('x', d.labelX)
|
|
285
|
-
.attr('dy', i === 0 ? `${startY}px` : `${lineH}px`)
|
|
286
|
-
.text(line);
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const quadrantLabelTexts = chartG
|
|
292
|
-
.selectAll('text.quadrant-label')
|
|
293
|
-
.data(quadrantDefsWithLabel)
|
|
294
|
-
.enter()
|
|
295
|
-
.append('text')
|
|
296
|
-
.attr('class', 'quadrant-label')
|
|
297
|
-
.attr('x', (d) => d.labelX)
|
|
298
|
-
.attr('y', (d) => d.labelY)
|
|
299
|
-
.attr('text-anchor', 'middle')
|
|
300
|
-
.attr('dominant-baseline', 'central')
|
|
301
|
-
.attr('fill', quadrantLabelColor)
|
|
302
|
-
.attr('data-line-number', (d) =>
|
|
303
|
-
d.label?.lineNumber ? String(d.label.lineNumber) : null
|
|
304
|
-
)
|
|
305
|
-
.style('cursor', (d) =>
|
|
306
|
-
onClickItem && d.label?.lineNumber ? 'pointer' : 'default'
|
|
307
|
-
)
|
|
308
|
-
.each(function (d) {
|
|
309
|
-
renderWatermarkOriginal(this as SVGTextElement, d);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
if (onClickItem) {
|
|
313
|
-
quadrantLabelTexts
|
|
314
|
-
.on('click', (_, d) => {
|
|
315
|
-
if (d.label?.lineNumber) onClickItem(d.label.lineNumber);
|
|
316
|
-
})
|
|
317
|
-
.on('mouseenter', function () {
|
|
318
|
-
d3Selection.select(this).attr('opacity', 0.7);
|
|
319
|
-
})
|
|
320
|
-
.on('mouseleave', function () {
|
|
321
|
-
d3Selection.select(this).attr('opacity', 1);
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const sAxisFont = ctx.text(18);
|
|
326
|
-
const sAxisPad = ctx.aesthetic(20);
|
|
327
|
-
const sAxisX = ctx.aesthetic(22);
|
|
328
|
-
|
|
329
|
-
if (quadrantXAxis) {
|
|
330
|
-
const xLowLabel = svg
|
|
331
|
-
.append('text')
|
|
332
|
-
.attr('class', 'quadrant-axis-label')
|
|
333
|
-
.attr('x', margin.left + chartWidth / 4)
|
|
334
|
-
.attr('y', height - sAxisPad)
|
|
335
|
-
.attr('text-anchor', 'middle')
|
|
336
|
-
.attr('fill', textColor)
|
|
337
|
-
.attr('font-size', `${sAxisFont}px`)
|
|
338
|
-
.attr(
|
|
339
|
-
'data-line-number',
|
|
340
|
-
quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
|
|
341
|
-
)
|
|
342
|
-
.style(
|
|
343
|
-
'cursor',
|
|
344
|
-
onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
|
|
345
|
-
)
|
|
346
|
-
.text(quadrantXAxis[0]);
|
|
347
|
-
|
|
348
|
-
// High label (centered on right half)
|
|
349
|
-
const xHighLabel = svg
|
|
350
|
-
.append('text')
|
|
351
|
-
.attr('class', 'quadrant-axis-label')
|
|
352
|
-
.attr('x', margin.left + (chartWidth * 3) / 4)
|
|
353
|
-
.attr('y', height - sAxisPad)
|
|
354
|
-
.attr('text-anchor', 'middle')
|
|
355
|
-
.attr('fill', textColor)
|
|
356
|
-
.attr('font-size', `${sAxisFont}px`)
|
|
357
|
-
.attr(
|
|
358
|
-
'data-line-number',
|
|
359
|
-
quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
|
|
360
|
-
)
|
|
361
|
-
.style(
|
|
362
|
-
'cursor',
|
|
363
|
-
onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
|
|
364
|
-
)
|
|
365
|
-
.text(quadrantXAxis[1]);
|
|
366
|
-
|
|
367
|
-
if (onClickItem && quadrantXAxisLineNumber) {
|
|
368
|
-
[xLowLabel, xHighLabel].forEach((label) => {
|
|
369
|
-
label
|
|
370
|
-
.on('click', () => onClickItem(quadrantXAxisLineNumber))
|
|
371
|
-
.on('mouseenter', function () {
|
|
372
|
-
d3Selection.select(this).attr('opacity', 0.7);
|
|
373
|
-
})
|
|
374
|
-
.on('mouseleave', function () {
|
|
375
|
-
d3Selection.select(this).attr('opacity', 1);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Y-axis labels — centered on top/bottom halves
|
|
382
|
-
if (quadrantYAxis) {
|
|
383
|
-
const yMidBottom = margin.top + (chartHeight * 3) / 4;
|
|
384
|
-
const yMidTop = margin.top + chartHeight / 4;
|
|
385
|
-
|
|
386
|
-
// Low label (centered on bottom half)
|
|
387
|
-
const yLowLabel = svg
|
|
388
|
-
.append('text')
|
|
389
|
-
.attr('class', 'quadrant-axis-label')
|
|
390
|
-
.attr('x', sAxisX)
|
|
391
|
-
.attr('y', yMidBottom)
|
|
392
|
-
.attr('text-anchor', 'middle')
|
|
393
|
-
.attr('fill', textColor)
|
|
394
|
-
.attr('font-size', `${sAxisFont}px`)
|
|
395
|
-
.attr('transform', `rotate(-90, ${sAxisX}, ${yMidBottom})`)
|
|
396
|
-
.attr(
|
|
397
|
-
'data-line-number',
|
|
398
|
-
quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
|
|
399
|
-
)
|
|
400
|
-
.style(
|
|
401
|
-
'cursor',
|
|
402
|
-
onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
|
|
403
|
-
)
|
|
404
|
-
.text(quadrantYAxis[0]);
|
|
405
|
-
|
|
406
|
-
// High label (centered on top half)
|
|
407
|
-
const yHighLabel = svg
|
|
408
|
-
.append('text')
|
|
409
|
-
.attr('class', 'quadrant-axis-label')
|
|
410
|
-
.attr('x', sAxisX)
|
|
411
|
-
.attr('y', yMidTop)
|
|
412
|
-
.attr('text-anchor', 'middle')
|
|
413
|
-
.attr('fill', textColor)
|
|
414
|
-
.attr('font-size', `${sAxisFont}px`)
|
|
415
|
-
.attr('transform', `rotate(-90, ${sAxisX}, ${yMidTop})`)
|
|
416
|
-
.attr(
|
|
417
|
-
'data-line-number',
|
|
418
|
-
quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
|
|
419
|
-
)
|
|
420
|
-
.style(
|
|
421
|
-
'cursor',
|
|
422
|
-
onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
|
|
423
|
-
)
|
|
424
|
-
.text(quadrantYAxis[1]);
|
|
425
|
-
|
|
426
|
-
if (onClickItem && quadrantYAxisLineNumber) {
|
|
427
|
-
[yLowLabel, yHighLabel].forEach((label) => {
|
|
428
|
-
label
|
|
429
|
-
.on('click', () => onClickItem(quadrantYAxisLineNumber))
|
|
430
|
-
.on('mouseenter', function () {
|
|
431
|
-
d3Selection.select(this).attr('opacity', 0.7);
|
|
432
|
-
})
|
|
433
|
-
.on('mouseleave', function () {
|
|
434
|
-
d3Selection.select(this).attr('opacity', 1);
|
|
435
|
-
});
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Draw center cross lines
|
|
441
|
-
chartG
|
|
442
|
-
.append('line')
|
|
443
|
-
.attr('x1', chartWidth / 2)
|
|
444
|
-
.attr('y1', 0)
|
|
445
|
-
.attr('x2', chartWidth / 2)
|
|
446
|
-
.attr('y2', chartHeight)
|
|
447
|
-
.attr('stroke', borderColor)
|
|
448
|
-
.attr('stroke-width', 1);
|
|
449
|
-
|
|
450
|
-
chartG
|
|
451
|
-
.append('line')
|
|
452
|
-
.attr('x1', 0)
|
|
453
|
-
.attr('y1', chartHeight / 2)
|
|
454
|
-
.attr('x2', chartWidth)
|
|
455
|
-
.attr('y2', chartHeight / 2)
|
|
456
|
-
.attr('stroke', borderColor)
|
|
457
|
-
.attr('stroke-width', 1);
|
|
458
|
-
|
|
459
|
-
// Get which quadrant a point belongs to
|
|
460
|
-
const getPointQuadrant = (x: number, y: number): QuadrantPosition => {
|
|
461
|
-
if (x >= 0.5 && y >= 0.5) return 'top-right';
|
|
462
|
-
if (x < 0.5 && y >= 0.5) return 'top-left';
|
|
463
|
-
if (x < 0.5 && y < 0.5) return 'bottom-left';
|
|
464
|
-
return 'bottom-right';
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
// Build obstacle rects from quadrant watermark labels for collision avoidance
|
|
468
|
-
const POINT_RADIUS = ctx.structural(6);
|
|
469
|
-
const POINT_LABEL_FONT_SIZE = ctx.text(12);
|
|
470
|
-
const quadrantLabelObstacles: LabelRect[] = quadrantDefsWithLabel.map((d) => {
|
|
471
|
-
const layout = labelLayouts.get(d.label!.text)!;
|
|
472
|
-
const totalW = Math.max(
|
|
473
|
-
...layout.lines.map((l) => measureText(l, layout.fontSize))
|
|
474
|
-
);
|
|
475
|
-
const totalH = layout.lines.length * layout.fontSize * 1.2;
|
|
476
|
-
return {
|
|
477
|
-
x: d.labelX - totalW / 2,
|
|
478
|
-
y: d.labelY - totalH / 2,
|
|
479
|
-
w: totalW,
|
|
480
|
-
h: totalH,
|
|
481
|
-
};
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
// Compute collision-free label positions for all points
|
|
485
|
-
const pointPixels = quadrantPoints.map((point) => ({
|
|
486
|
-
label: point.label,
|
|
487
|
-
cx: xScale(point.x),
|
|
488
|
-
cy: yScale(point.y),
|
|
489
|
-
}));
|
|
490
|
-
|
|
491
|
-
const placedPointLabels = computeQuadrantPointLabels(
|
|
492
|
-
pointPixels,
|
|
493
|
-
{ left: 0, top: 0, right: chartWidth, bottom: chartHeight },
|
|
494
|
-
quadrantLabelObstacles,
|
|
495
|
-
POINT_RADIUS,
|
|
496
|
-
POINT_LABEL_FONT_SIZE
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
// Draw data points (circles and labels)
|
|
500
|
-
const pointsG = chartG.append('g').attr('class', 'points');
|
|
501
|
-
|
|
502
|
-
quadrantPoints.forEach((point, i) => {
|
|
503
|
-
const cx = xScale(point.x);
|
|
504
|
-
const cy = yScale(point.y);
|
|
505
|
-
const quadrant = getPointQuadrant(point.x, point.y);
|
|
506
|
-
const quadDef = quadrantDefs.find((d) => d.position === quadrant);
|
|
507
|
-
// defaultColors is non-empty; in-bounds by modulo / fallback to index 0.
|
|
508
|
-
const pointColor =
|
|
509
|
-
quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0]!;
|
|
510
|
-
// placedPointLabels was produced by computeQuadrantPointLabels from pointPixels,
|
|
511
|
-
// which has the same length as quadrantPoints.
|
|
512
|
-
const placed = placedPointLabels[i]!;
|
|
513
|
-
|
|
514
|
-
const pointG = pointsG
|
|
515
|
-
.append('g')
|
|
516
|
-
.attr('class', 'point-group')
|
|
517
|
-
.attr('data-line-number', String(point.lineNumber))
|
|
518
|
-
// Computed quadrant for baked-CSS cross-highlight (hover a point → dim
|
|
519
|
-
// points in other quadrants).
|
|
520
|
-
.attr('data-quadrant', quadrant);
|
|
521
|
-
|
|
522
|
-
// Connector line (drawn first so it renders behind circle and label)
|
|
523
|
-
if (placed.connectorLine) {
|
|
524
|
-
pointG
|
|
525
|
-
.append('line')
|
|
526
|
-
.attr('x1', placed.connectorLine.x1)
|
|
527
|
-
.attr('y1', placed.connectorLine.y1)
|
|
528
|
-
.attr('x2', placed.connectorLine.x2)
|
|
529
|
-
.attr('y2', placed.connectorLine.y2)
|
|
530
|
-
.attr('stroke', pointColor)
|
|
531
|
-
.attr('stroke-width', 1)
|
|
532
|
-
.attr('opacity', 0.5);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Circle with white fill and colored border for visibility on opaque quadrants
|
|
536
|
-
pointG
|
|
537
|
-
.append('circle')
|
|
538
|
-
.attr('cx', cx)
|
|
539
|
-
.attr('cy', cy)
|
|
540
|
-
.attr('r', POINT_RADIUS)
|
|
541
|
-
.attr('fill', '#ffffff')
|
|
542
|
-
.attr('stroke', pointColor)
|
|
543
|
-
.attr('stroke-width', ctx.structural(2));
|
|
544
|
-
|
|
545
|
-
// Label at computed position. Name is always shown; coords sit below
|
|
546
|
-
// (smaller, lighter weight) and only appear on hover.
|
|
547
|
-
const labelText = pointG
|
|
548
|
-
.append('text')
|
|
549
|
-
.attr('x', placed.x)
|
|
550
|
-
.attr('y', placed.y)
|
|
551
|
-
.attr('text-anchor', placed.anchor)
|
|
552
|
-
.attr('dominant-baseline', 'central')
|
|
553
|
-
.attr('fill', textColor)
|
|
554
|
-
.attr('font-size', `${POINT_LABEL_FONT_SIZE}px`)
|
|
555
|
-
.attr('font-weight', '700')
|
|
556
|
-
.style('text-shadow', `0 1px 2px ${shadowColor}`)
|
|
557
|
-
.style('transition', 'y 120ms ease-out');
|
|
558
|
-
|
|
559
|
-
labelText.append('tspan').text(point.label);
|
|
560
|
-
|
|
561
|
-
const coordsTspan = labelText
|
|
562
|
-
.append('tspan')
|
|
563
|
-
.attr('class', 'point-coords')
|
|
564
|
-
.attr('x', placed.x)
|
|
565
|
-
.attr('dy', `${POINT_LABEL_FONT_SIZE}px`)
|
|
566
|
-
.attr('font-size', `${ctx.text(10)}px`)
|
|
567
|
-
.attr('font-weight', '500')
|
|
568
|
-
.attr('opacity', 0)
|
|
569
|
-
.text(`${point.x.toFixed(2)}, ${point.y.toFixed(2)}`);
|
|
570
|
-
|
|
571
|
-
const COORDS_LINE_H = ctx.structural(14);
|
|
572
|
-
const bumpDy = placed.y < cy ? -COORDS_LINE_H : COORDS_LINE_H;
|
|
573
|
-
|
|
574
|
-
pointG
|
|
575
|
-
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
576
|
-
.on('mouseenter', () => {
|
|
577
|
-
pointG.select('circle').attr('r', ctx.structural(8));
|
|
578
|
-
labelText.attr('y', placed.y + bumpDy);
|
|
579
|
-
coordsTspan.attr('opacity', 1);
|
|
580
|
-
quadrantRects.attr('opacity', (qd) =>
|
|
581
|
-
qd.position === quadrant ? 1 : 0.3
|
|
582
|
-
);
|
|
583
|
-
quadrantLabelTexts.attr('opacity', (qd) =>
|
|
584
|
-
qd.position === quadrant ? 1 : 0.3
|
|
585
|
-
);
|
|
586
|
-
pointsG
|
|
587
|
-
.selectAll<SVGGElement, unknown>('g.point-group')
|
|
588
|
-
.filter((_, j) => j !== i)
|
|
589
|
-
.attr('opacity', 0.3);
|
|
590
|
-
})
|
|
591
|
-
.on('mouseleave', () => {
|
|
592
|
-
pointG.select('circle').attr('r', POINT_RADIUS);
|
|
593
|
-
labelText.attr('y', placed.y);
|
|
594
|
-
coordsTspan.attr('opacity', 0);
|
|
595
|
-
quadrantRects.attr('opacity', 1);
|
|
596
|
-
quadrantLabelTexts.attr('opacity', 1);
|
|
597
|
-
pointsG.selectAll('g.point-group').attr('opacity', 1);
|
|
598
|
-
})
|
|
599
|
-
.on('click', () => {
|
|
600
|
-
if (onClickItem && point.lineNumber) onClickItem(point.lineNumber);
|
|
601
|
-
});
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
// Quadrant highlighting on hover and click-to-navigate
|
|
605
|
-
quadrantRects
|
|
606
|
-
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
607
|
-
.on('mouseenter', function (_, d) {
|
|
608
|
-
// Dim other quadrants
|
|
609
|
-
quadrantRects.attr('opacity', (qd) =>
|
|
610
|
-
qd.position === d.position ? 1 : 0.3
|
|
611
|
-
);
|
|
612
|
-
quadrantLabelTexts.attr('opacity', (qd) =>
|
|
613
|
-
qd.position === d.position ? 1 : 0.3
|
|
614
|
-
);
|
|
615
|
-
// Dim points not in this quadrant
|
|
616
|
-
pointsG.selectAll('g.point-group').each(function (_, i) {
|
|
617
|
-
// selectAll iterates over point-group elements, so i indexes into quadrantPoints.
|
|
618
|
-
const pt = quadrantPoints[i]!;
|
|
619
|
-
const ptQuad = getPointQuadrant(pt.x, pt.y);
|
|
620
|
-
d3Selection
|
|
621
|
-
.select(this)
|
|
622
|
-
.attr('opacity', ptQuad === d.position ? 1 : 0.2);
|
|
623
|
-
});
|
|
624
|
-
})
|
|
625
|
-
.on('mouseleave', () => {
|
|
626
|
-
quadrantRects.attr('opacity', 1);
|
|
627
|
-
quadrantLabelTexts.attr('opacity', 1);
|
|
628
|
-
pointsG.selectAll('g.point-group').attr('opacity', 1);
|
|
629
|
-
})
|
|
630
|
-
.on('click', (_, d) => {
|
|
631
|
-
// Navigate to the quadrant label's line in the source
|
|
632
|
-
if (onClickItem && d.label?.lineNumber) {
|
|
633
|
-
onClickItem(d.label.lineNumber);
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// ============================================================
|