@rokkit/graph 1.8.0 → 1.8.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/README.md +59 -0
- package/dist/Graph.svelte +108 -134
- package/dist/Graph.svelte.d.ts +1 -1
- package/dist/GraphLegend.svelte +81 -10
- package/dist/GraphLegend.svelte.d.ts +15 -0
- package/dist/GraphState.svelte.d.ts +161 -52
- package/dist/GraphState.svelte.js +380 -304
- package/dist/actions/canvas.d.ts +18 -0
- package/dist/actions/canvas.js +59 -0
- package/dist/actions/choices.d.ts +12 -0
- package/dist/actions/choices.js +23 -0
- package/dist/actions/interactions.d.ts +20 -0
- package/dist/actions/interactions.js +57 -0
- package/dist/actions/listen.d.ts +2 -0
- package/dist/actions/listen.js +10 -0
- package/dist/canvas/frame.d.ts +41 -0
- package/dist/canvas/frame.js +34 -0
- package/dist/controls/BundleControl.svelte +11 -10
- package/dist/controls/BundleControl.svelte.d.ts +0 -7
- package/dist/controls/DensityControl.svelte +17 -9
- package/dist/controls/DensityControl.svelte.d.ts +0 -8
- package/dist/controls/DepthControl.svelte +11 -4
- package/dist/controls/DepthControl.svelte.d.ts +0 -9
- package/dist/controls/DrillBar.svelte +63 -0
- package/dist/controls/DrillBar.svelte.d.ts +9 -0
- package/dist/controls/EdgeStyleControl.svelte +10 -10
- package/dist/controls/EdgeStyleControl.svelte.d.ts +0 -6
- package/dist/controls/HiddenControl.svelte +27 -0
- package/dist/controls/HiddenControl.svelte.d.ts +7 -0
- package/dist/controls/MeasureControl.svelte +39 -0
- package/dist/controls/MeasureControl.svelte.d.ts +12 -0
- package/dist/controls/ToggleControl.svelte +33 -0
- package/dist/controls/ToggleControl.svelte.d.ts +13 -0
- package/dist/controls/ViolationsControl.svelte +27 -0
- package/dist/controls/ViolationsControl.svelte.d.ts +7 -0
- package/dist/controls/ZoomControl.svelte +21 -18
- package/dist/controls/zoom.d.ts +3 -0
- package/dist/controls/zoom.js +6 -0
- package/dist/diagrams/ArcDiagram.svelte +116 -0
- package/dist/diagrams/ArcDiagram.svelte.d.ts +31 -0
- package/dist/diagrams/CallTree.svelte +17 -5
- package/dist/diagrams/CallTree.svelte.d.ts +5 -1
- package/dist/diagrams/DependencyDiagram.svelte +18 -6
- package/dist/diagrams/DependencyDiagram.svelte.d.ts +5 -1
- package/dist/diagrams/DependencyMatrix.svelte +11 -32
- package/dist/diagrams/ErDiagram.svelte +18 -6
- package/dist/diagrams/ErDiagram.svelte.d.ts +5 -1
- package/dist/diagrams/LayersDiagram.svelte +124 -0
- package/dist/diagrams/LayersDiagram.svelte.d.ts +30 -0
- package/dist/diagrams/Neighborhood.svelte +26 -6
- package/dist/diagrams/Neighborhood.svelte.d.ts +11 -1
- package/dist/diagrams/PolymetricTree.svelte +116 -0
- package/dist/diagrams/PolymetricTree.svelte.d.ts +28 -0
- package/dist/diagrams/StructureDiagram.svelte +38 -8
- package/dist/diagrams/StructureDiagram.svelte.d.ts +9 -1
- package/dist/diagrams/Sunburst.svelte +44 -8
- package/dist/diagrams/Sunburst.svelte.d.ts +12 -2
- package/dist/diagrams/Treemap.svelte +44 -8
- package/dist/diagrams/Treemap.svelte.d.ts +12 -2
- package/dist/index.d.ts +10 -3
- package/dist/index.js +9 -2
- package/dist/layout/arcs.d.ts +6 -0
- package/dist/layout/arcs.js +141 -0
- package/dist/layout/cards.js +9 -2
- package/dist/layout/edges.d.ts +13 -1
- package/dist/layout/edges.js +26 -2
- package/dist/layout/extent.d.ts +17 -0
- package/dist/layout/extent.js +27 -0
- package/dist/layout/flow.js +3 -1
- package/dist/layout/index.d.ts +4 -1
- package/dist/layout/index.js +17 -2
- package/dist/layout/layers.d.ts +6 -0
- package/dist/layout/layers.js +180 -0
- package/dist/layout/matrix.d.ts +12 -0
- package/dist/layout/matrix.js +20 -0
- package/dist/layout/neighborhood.js +16 -7
- package/dist/layout/options.d.ts +1 -1
- package/dist/layout/options.js +16 -3
- package/dist/layout/percentile.d.ts +8 -0
- package/dist/layout/percentile.js +13 -0
- package/dist/layout/polymetric.d.ts +3 -0
- package/dist/layout/polymetric.js +221 -0
- package/dist/layout/radial.js +3 -1
- package/dist/layout/structure.js +7 -1
- package/dist/layout/sunburst.js +11 -4
- package/dist/layout/types.d.ts +79 -0
- package/dist/layout/world.js +11 -4
- package/dist/messages.d.ts +14 -0
- package/dist/messages.js +17 -0
- package/dist/model/condense.d.ts +8 -0
- package/dist/model/condense.js +114 -0
- package/dist/model/entities.d.ts +12 -0
- package/dist/model/entities.js +12 -0
- package/dist/model/normalize.d.ts +2 -6
- package/dist/model/normalize.js +64 -2
- package/dist/model/relationships.d.ts +30 -0
- package/dist/model/relationships.js +51 -0
- package/dist/model/share.d.ts +9 -0
- package/dist/model/share.js +43 -0
- package/dist/model/tree.d.ts +7 -1
- package/dist/model/tree.js +11 -4
- package/dist/preset.d.ts +18 -0
- package/dist/preset.js +34 -3
- package/dist/schema/EntitiesView.svelte +8 -6
- package/dist/schema/EntityView.svelte +13 -10
- package/dist/state/GraphConfig.svelte.d.ts +124 -0
- package/dist/state/GraphConfig.svelte.js +253 -0
- package/dist/state/GraphDrill.svelte.d.ts +52 -0
- package/dist/state/GraphDrill.svelte.js +115 -0
- package/dist/state/GraphGroups.svelte.d.ts +35 -0
- package/dist/state/GraphGroups.svelte.js +73 -0
- package/dist/state/GraphSelection.svelte.d.ts +53 -0
- package/dist/state/GraphSelection.svelte.js +94 -0
- package/dist/state/intents.d.ts +33 -0
- package/dist/state/intents.js +30 -0
- package/dist/state/text.d.ts +28 -0
- package/dist/state/text.js +56 -0
- package/dist/types.d.ts +72 -1
- package/package.json +3 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving around the canvas: drag the background to pan, ctrl/⌘ + wheel to zoom.
|
|
3
|
+
*
|
|
4
|
+
* Ctrl/⌘ + wheel is what a trackpad pinch reports as; without preventDefault the browser
|
|
5
|
+
* zooms the whole PAGE instead of the diagram. A plain wheel is left alone — the canvas
|
|
6
|
+
* scrolls, which is how panning works here, and the drag is the direct-manipulation path on
|
|
7
|
+
* top of it. The paper carries `data-graph-panning` while a drag lasts.
|
|
8
|
+
*/
|
|
9
|
+
import type { ActionReturn } from 'svelte/action';
|
|
10
|
+
export type CanvasParams = {
|
|
11
|
+
zoomable?: boolean;
|
|
12
|
+
/** A pinch: which way, and where — relative to the paper — so the zoom can keep that point still. */
|
|
13
|
+
onzoom?: (direction: 'in' | 'out', at: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}) => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function canvasNavigation(paper: HTMLElement, initial: CanvasParams): ActionReturn<CanvasParams>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { listen } from './listen.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a press can start on without being a pan: a node card, and anything interactive. A pan
|
|
4
|
+
* captures the pointer, and the browser then delivers the CLICK to the canvas instead of the
|
|
5
|
+
* box — so a treemap box could be neither opened nor selected by mouse.
|
|
6
|
+
*/
|
|
7
|
+
const OWNS_ITS_PRESS = '[data-graph-node], button, [role="button"], a[href], input, select, textarea';
|
|
8
|
+
/** Drag-to-pan on the background: scroll the paper against the pointer while it is held. */
|
|
9
|
+
function panHandlers(paper) {
|
|
10
|
+
let from = null;
|
|
11
|
+
const end = () => {
|
|
12
|
+
from = null;
|
|
13
|
+
paper.removeAttribute('data-graph-panning');
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
pointerdown(event) {
|
|
17
|
+
if (event.target.closest(OWNS_ITS_PRESS))
|
|
18
|
+
return;
|
|
19
|
+
from = { x: event.clientX, y: event.clientY, left: paper.scrollLeft, top: paper.scrollTop };
|
|
20
|
+
paper.setAttribute('data-graph-panning', '');
|
|
21
|
+
paper.setPointerCapture(event.pointerId);
|
|
22
|
+
},
|
|
23
|
+
pointermove(event) {
|
|
24
|
+
if (!from)
|
|
25
|
+
return;
|
|
26
|
+
paper.scrollLeft = from.left - (event.clientX - from.x);
|
|
27
|
+
paper.scrollTop = from.top - (event.clientY - from.y);
|
|
28
|
+
},
|
|
29
|
+
pointerup: end,
|
|
30
|
+
pointercancel: end
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** A pinch (ctrl/⌘ + wheel) zooms; a plain wheel is left to scroll. */
|
|
34
|
+
function zoomHandler(params) {
|
|
35
|
+
return (event) => {
|
|
36
|
+
if (!params().zoomable || !(event.ctrlKey || event.metaKey))
|
|
37
|
+
return;
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
40
|
+
params().onzoom?.(event.deltaY < 0 ? 'in' : 'out', { x: event.clientX - box.left, y: event.clientY - box.top });
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function canvasNavigation(paper, initial) {
|
|
44
|
+
let params = initial;
|
|
45
|
+
const pan = panHandlers(paper);
|
|
46
|
+
const stopPan = listen(paper, pan);
|
|
47
|
+
// Not passive: preventing the page zoom is the point of the wheel listener.
|
|
48
|
+
const stopZoom = listen(paper, { wheel: zoomHandler(() => params) }, { passive: false });
|
|
49
|
+
return {
|
|
50
|
+
update(next) {
|
|
51
|
+
params = next;
|
|
52
|
+
},
|
|
53
|
+
destroy() {
|
|
54
|
+
stopPan();
|
|
55
|
+
stopZoom();
|
|
56
|
+
pan.pointercancel();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a control reports: the value of the choice the reader made, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* A button names its choice in `data-graph-choice`; a `<select>` reports its value on change.
|
|
5
|
+
* The control maps the string back to its own type and calls its `onchange` — so no button
|
|
6
|
+
* carries a handler, and a disabled choice never fires.
|
|
7
|
+
*/
|
|
8
|
+
import type { ActionReturn } from 'svelte/action';
|
|
9
|
+
export type ChoicesParams = {
|
|
10
|
+
onchoose: (value: string) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function choices(root: HTMLElement, initial: ChoicesParams): ActionReturn<ChoicesParams>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { listen } from './listen.js';
|
|
2
|
+
export function choices(root, initial) {
|
|
3
|
+
let params = initial;
|
|
4
|
+
const stop = listen(root, {
|
|
5
|
+
click(event) {
|
|
6
|
+
const chosen = event.target.closest('[data-graph-choice]');
|
|
7
|
+
if (!chosen || !root.contains(chosen) || chosen.disabled)
|
|
8
|
+
return;
|
|
9
|
+
params.onchoose(chosen.getAttribute('data-graph-choice') ?? '');
|
|
10
|
+
},
|
|
11
|
+
change(event) {
|
|
12
|
+
const target = event.target;
|
|
13
|
+
if (target.tagName === 'SELECT')
|
|
14
|
+
params.onchoose(target.value);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
update(next) {
|
|
19
|
+
params = next;
|
|
20
|
+
},
|
|
21
|
+
destroy: stop
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every press on the graph, through one delegated listener.
|
|
3
|
+
*
|
|
4
|
+
* The state writes what a press MEANS onto the element — `data-graph-press` for a click (and
|
|
5
|
+
* Enter / Space), `data-graph-open` for a double-click, `data-graph-key` for what it is about —
|
|
6
|
+
* and this reads it back and calls `state.act`. No element carries a handler of its own, so a
|
|
7
|
+
* new interactive element is an attribute, and the whole mapping is testable on bare DOM.
|
|
8
|
+
*
|
|
9
|
+
* The innermost intent wins: an "Expand" control inside a card is its own press, not the card's.
|
|
10
|
+
*/
|
|
11
|
+
import type { ActionReturn } from 'svelte/action';
|
|
12
|
+
import type { Intent } from '../state/intents.js';
|
|
13
|
+
export type InteractionsParams = {
|
|
14
|
+
state: {
|
|
15
|
+
act(intent: Intent, key: string | null): void;
|
|
16
|
+
};
|
|
17
|
+
/** A click on nothing interactive, or Escape, clears the selection — the canvas does this. */
|
|
18
|
+
clearOnBackground?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare function interactions(root: HTMLElement, initial: InteractionsParams): ActionReturn<InteractionsParams>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isIntent } from '../state/intents.js';
|
|
2
|
+
import { listen } from './listen.js';
|
|
3
|
+
const PRESS = '[data-graph-press]';
|
|
4
|
+
const OPEN = '[data-graph-open]';
|
|
5
|
+
/** The innermost element inside `root` that carries `selector`, from the event's target. */
|
|
6
|
+
function carrier(root, event, selector) {
|
|
7
|
+
const found = event.target?.closest(selector) ?? null;
|
|
8
|
+
return found && root.contains(found) ? found : null;
|
|
9
|
+
}
|
|
10
|
+
/** Act on the intent `element` declares under `attribute`, if it is one the state knows. */
|
|
11
|
+
function perform(params, element, attribute) {
|
|
12
|
+
const intent = element.getAttribute(attribute);
|
|
13
|
+
if (isIntent(intent))
|
|
14
|
+
params.state.act(intent, element.getAttribute('data-graph-key'));
|
|
15
|
+
}
|
|
16
|
+
const isActivation = (event) => event.key === 'Enter' || event.key === ' ';
|
|
17
|
+
/** The three delegated listeners, reading the latest params on every event. */
|
|
18
|
+
function handlers(root, params) {
|
|
19
|
+
return {
|
|
20
|
+
click(event) {
|
|
21
|
+
const pressed = carrier(root, event, PRESS);
|
|
22
|
+
if (pressed)
|
|
23
|
+
perform(params(), pressed, 'data-graph-press');
|
|
24
|
+
else if (params().clearOnBackground)
|
|
25
|
+
params().state.act('clear', null);
|
|
26
|
+
},
|
|
27
|
+
dblclick(event) {
|
|
28
|
+
const opened = carrier(root, event, OPEN);
|
|
29
|
+
if (opened)
|
|
30
|
+
perform(params(), opened, 'data-graph-open');
|
|
31
|
+
},
|
|
32
|
+
keydown(event) {
|
|
33
|
+
if (event.key === 'Escape') {
|
|
34
|
+
if (params().clearOnBackground)
|
|
35
|
+
params().state.act('clear', null);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const pressed = carrier(root, event, PRESS);
|
|
39
|
+
// A real <button> turns Enter and Space into a click by itself; acting here too would
|
|
40
|
+
// press it twice.
|
|
41
|
+
if (!pressed || pressed.tagName === 'BUTTON' || !isActivation(event))
|
|
42
|
+
return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
perform(params(), pressed, 'data-graph-press');
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function interactions(root, initial) {
|
|
49
|
+
let params = initial;
|
|
50
|
+
const stop = listen(root, handlers(root, () => params));
|
|
51
|
+
return {
|
|
52
|
+
update(next) {
|
|
53
|
+
params = next;
|
|
54
|
+
},
|
|
55
|
+
destroy: stop
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Attach every listener in `listeners` to `target`; the returned function detaches them all. */
|
|
2
|
+
export function listen(target, listeners, options) {
|
|
3
|
+
const entries = Object.entries(listeners);
|
|
4
|
+
for (const [type, listener] of entries)
|
|
5
|
+
target.addEventListener(type, listener, options);
|
|
6
|
+
return () => {
|
|
7
|
+
for (const [type, listener] of entries)
|
|
8
|
+
target.removeEventListener(type, listener, options);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Size } from '../layout/types.js';
|
|
2
|
+
export type Frame = {
|
|
3
|
+
/** The scale that fits the content in the viewport, before the reader's zoom. */
|
|
4
|
+
fit: number;
|
|
5
|
+
/** fit × zoom — what the world is drawn at. */
|
|
6
|
+
scale: number;
|
|
7
|
+
/** Where the scaled world's top-left sits inside the scrollable extent. */
|
|
8
|
+
tx: number;
|
|
9
|
+
ty: number;
|
|
10
|
+
/** The scrollable extent: the scaled drawing plus `pad` all round, never less than the viewport. */
|
|
11
|
+
extent: Size;
|
|
12
|
+
};
|
|
13
|
+
type FrameInput = {
|
|
14
|
+
vw: number;
|
|
15
|
+
vh: number;
|
|
16
|
+
content: Size;
|
|
17
|
+
zoom: number;
|
|
18
|
+
pad: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function frameOf({ vw, vh, content, zoom, pad }: FrameInput): Frame;
|
|
21
|
+
type Scroll = {
|
|
22
|
+
left: number;
|
|
23
|
+
top: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The scroll that keeps the content point under `anchor` (viewport coordinates) where it was,
|
|
27
|
+
* across a change of frame. A pinch anchors at the pointer; a zoom button at the viewport centre.
|
|
28
|
+
*
|
|
29
|
+
* It holds on an axis that scrolls. On an axis where the drawing still fits, the drawing stays
|
|
30
|
+
* centred instead (`offset`), and no scroll can move it — fitting wins over anchoring there.
|
|
31
|
+
*/
|
|
32
|
+
export declare function anchoredScroll(input: {
|
|
33
|
+
before: Frame;
|
|
34
|
+
after: Frame;
|
|
35
|
+
scroll: Scroll;
|
|
36
|
+
anchor: {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
};
|
|
40
|
+
}): Scroll;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The smallest scale fit will choose: below it a drawing is noise, however large it is. */
|
|
2
|
+
const MIN_SCALE = 0.08;
|
|
3
|
+
/** Centred while the drawing is smaller than the viewport; `pad` in from the edge once it is not. */
|
|
4
|
+
const offset = (viewport, drawn, pad) => Math.max(pad, (viewport - drawn) / 2);
|
|
5
|
+
export function frameOf({ vw, vh, content, zoom, pad }) {
|
|
6
|
+
const fit = Math.max(MIN_SCALE, Math.min((vw - pad * 2) / content.w, (vh - pad * 2) / content.h, 1)) || 0.5;
|
|
7
|
+
const scale = fit * zoom;
|
|
8
|
+
const drawn = { w: content.w * scale, h: content.h * scale };
|
|
9
|
+
return {
|
|
10
|
+
fit,
|
|
11
|
+
scale,
|
|
12
|
+
tx: offset(vw, drawn.w, pad),
|
|
13
|
+
ty: offset(vh, drawn.h, pad),
|
|
14
|
+
// The same pad on the trailing edge as the leading one, so the far side is reachable
|
|
15
|
+
// with room to spare — and exactly the viewport when the drawing fits, so nothing scrolls.
|
|
16
|
+
extent: { w: Math.max(vw, drawn.w + pad * 2), h: Math.max(vh, drawn.h + pad * 2) }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The scroll that keeps the content point under `anchor` (viewport coordinates) where it was,
|
|
21
|
+
* across a change of frame. A pinch anchors at the pointer; a zoom button at the viewport centre.
|
|
22
|
+
*
|
|
23
|
+
* It holds on an axis that scrolls. On an axis where the drawing still fits, the drawing stays
|
|
24
|
+
* centred instead (`offset`), and no scroll can move it — fitting wins over anchoring there.
|
|
25
|
+
*/
|
|
26
|
+
export function anchoredScroll(input) {
|
|
27
|
+
const { before, after, scroll, anchor } = input;
|
|
28
|
+
const x = (scroll.left + anchor.x - before.tx) / before.scale;
|
|
29
|
+
const y = (scroll.top + anchor.y - before.ty) / before.scale;
|
|
30
|
+
return {
|
|
31
|
+
left: Math.max(0, x * after.scale + after.tx - anchor.x),
|
|
32
|
+
top: Math.max(0, y * after.scale + after.ty - anchor.y)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* the view is readable at all — a few hundred straight chords across a rim are a solid disc
|
|
7
7
|
* of ink — and straight is what you switch to when you want to see one edge exactly.
|
|
8
8
|
*/
|
|
9
|
+
import ToggleControl from './ToggleControl.svelte'
|
|
10
|
+
import { say } from '../messages.js'
|
|
11
|
+
|
|
9
12
|
type Props = {
|
|
10
13
|
bundleTension?: number
|
|
11
14
|
/** Tension used for the bundled state. Holten's 0.85 by default. */
|
|
@@ -18,13 +21,11 @@
|
|
|
18
21
|
const on = $derived(bundleTension > 0)
|
|
19
22
|
</script>
|
|
20
23
|
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
data-graph-bundle
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<span data-graph-control-label>{on ? 'Bundled' : 'Straight'}</span>
|
|
30
|
-
</button>
|
|
24
|
+
<ToggleControl
|
|
25
|
+
pressed={on}
|
|
26
|
+
hooks={{ 'data-graph-bundle': on ? 'bundled' : 'straight' }}
|
|
27
|
+
glyph={on ? '❨' : '/'}
|
|
28
|
+
label={say(on ? 'bundled' : 'straight')}
|
|
29
|
+
title={say(on ? 'bundledTitle' : 'straightTitle')}
|
|
30
|
+
onchange={(next) => onchange?.(next ? bundled : 0)}
|
|
31
|
+
/>
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Straight chords or bundled ones.
|
|
3
|
-
*
|
|
4
|
-
* A toggle rather than a slider: the useful values are the two ends. Bundled is the reason
|
|
5
|
-
* the view is readable at all — a few hundred straight chords across a rim are a solid disc
|
|
6
|
-
* of ink — and straight is what you switch to when you want to see one edge exactly.
|
|
7
|
-
*/
|
|
8
1
|
type Props = {
|
|
9
2
|
bundleTension?: number;
|
|
10
3
|
/** Tension used for the bundled state. Holten's 0.85 by default. */
|
|
@@ -6,34 +6,42 @@
|
|
|
6
6
|
* control that moves while the picture does not reads as a broken view. The diagram that
|
|
7
7
|
* wants it places it; the rest never render it.
|
|
8
8
|
*/
|
|
9
|
+
import { choices } from '../actions/choices.js'
|
|
10
|
+
import { say } from '../messages.js'
|
|
11
|
+
import type { GraphMessageKey } from '../messages.js'
|
|
9
12
|
import type { Density } from '../layout/types.js'
|
|
10
13
|
|
|
11
14
|
type Props = {
|
|
12
15
|
density?: Density
|
|
13
|
-
/** Fires with the chosen value. The caller owns the value — this control stores none. */
|
|
14
16
|
onchange?: (value: Density) => void
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
let { density = 'keys', onchange }: Props = $props()
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
['
|
|
22
|
-
['
|
|
21
|
+
/** Each level, and the words for it. */
|
|
22
|
+
const LEVELS: [Density, GraphMessageKey, GraphMessageKey][] = [
|
|
23
|
+
['names', 'densityNames', 'densityNamesTitle'],
|
|
24
|
+
['keys', 'densityKeys', 'densityKeysTitle'],
|
|
25
|
+
['full', 'densityFull', 'densityFullTitle']
|
|
23
26
|
]
|
|
24
27
|
</script>
|
|
25
28
|
|
|
26
29
|
<!-- Named, because three unlabelled buttons announce as "Names Keys All" and say nothing
|
|
27
30
|
about what they control. -->
|
|
28
|
-
<div
|
|
29
|
-
|
|
31
|
+
<div
|
|
32
|
+
data-graph-density-controls
|
|
33
|
+
role="group"
|
|
34
|
+
aria-label={say('detailLevel')}
|
|
35
|
+
use:choices={{ onchoose: (value) => onchange?.(value as Density) }}
|
|
36
|
+
>
|
|
37
|
+
{#each LEVELS as [value, short, title] (value)}
|
|
30
38
|
<button
|
|
31
39
|
type="button"
|
|
32
40
|
data-graph-density={value}
|
|
41
|
+
data-graph-choice={value}
|
|
33
42
|
data-selected={density === value ? '' : undefined}
|
|
34
43
|
aria-pressed={density === value}
|
|
35
|
-
{title}
|
|
36
|
-
onclick={() => onchange?.(value)}>{short}</button
|
|
44
|
+
title={say(title)}>{say(short)}</button
|
|
37
45
|
>
|
|
38
46
|
{/each}
|
|
39
47
|
</div>
|
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How much of each node's row list a card shows.
|
|
3
|
-
*
|
|
4
|
-
* A component rather than canvas chrome: only some diagrams have rows to thin, and a
|
|
5
|
-
* control that moves while the picture does not reads as a broken view. The diagram that
|
|
6
|
-
* wants it places it; the rest never render it.
|
|
7
|
-
*/
|
|
8
1
|
import type { Density } from '../layout/types.js';
|
|
9
2
|
type Props = {
|
|
10
3
|
density?: Density;
|
|
11
|
-
/** Fires with the chosen value. The caller owns the value — this control stores none. */
|
|
12
4
|
onchange?: (value: Density) => void;
|
|
13
5
|
};
|
|
14
6
|
declare const DensityControl: import("svelte").Component<Props, {}, "">;
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* nothing is legible — show the crates first, then drill. Depth and drilling are the same
|
|
8
8
|
* idea from two directions: this caps how far DOWN, `focusPath` moves where you start.
|
|
9
9
|
*/
|
|
10
|
+
import { choices } from '../actions/choices.js'
|
|
11
|
+
import { counted, say } from '../messages.js'
|
|
12
|
+
|
|
10
13
|
type Props = {
|
|
11
14
|
levels?: number
|
|
12
|
-
/** Deepest level offered. Beyond the tree's own depth the extra buttons do nothing. */
|
|
13
15
|
max?: number
|
|
14
16
|
onchange?: (value: number) => void
|
|
15
17
|
}
|
|
@@ -20,15 +22,20 @@
|
|
|
20
22
|
const steps = $derived(Array.from({ length: Math.max(1, max) }, (_, i) => i + 1))
|
|
21
23
|
</script>
|
|
22
24
|
|
|
23
|
-
<div
|
|
25
|
+
<div
|
|
26
|
+
data-graph-depth-controls
|
|
27
|
+
role="group"
|
|
28
|
+
aria-label={say('levelsShown')}
|
|
29
|
+
use:choices={{ onchoose: (value) => onchange?.(Number(value)) }}
|
|
30
|
+
>
|
|
24
31
|
{#each steps as level (level)}
|
|
25
32
|
<button
|
|
26
33
|
type="button"
|
|
27
34
|
data-graph-depth={level}
|
|
35
|
+
data-graph-choice={level}
|
|
28
36
|
data-selected={levels === level ? '' : undefined}
|
|
29
37
|
aria-pressed={levels === level}
|
|
30
|
-
title=
|
|
31
|
-
onclick={() => onchange?.(level)}>{level}</button
|
|
38
|
+
title={counted(level, 'levelOne', 'levelMany')}>{level}</button
|
|
32
39
|
>
|
|
33
40
|
{/each}
|
|
34
41
|
</div>
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How many levels of a containment tree to materialise.
|
|
3
|
-
*
|
|
4
|
-
* The control that makes a large tree readable at all. A radial dendrogram over a whole
|
|
5
|
-
* codebase renders every leaf on one rim, where each is a fraction of a degree wide and
|
|
6
|
-
* nothing is legible — show the crates first, then drill. Depth and drilling are the same
|
|
7
|
-
* idea from two directions: this caps how far DOWN, `focusPath` moves where you start.
|
|
8
|
-
*/
|
|
9
1
|
type Props = {
|
|
10
2
|
levels?: number;
|
|
11
|
-
/** Deepest level offered. Beyond the tree's own depth the extra buttons do nothing. */
|
|
12
3
|
max?: number;
|
|
13
4
|
onchange?: (value: number) => void;
|
|
14
5
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Where the reader has drilled to, and the way back (#165).
|
|
4
|
+
*
|
|
5
|
+
* Breadcrumbs from the root to the current level (each an ancestor to climb to), a status
|
|
6
|
+
* while the host loads a level, the error when a load fails, "Open" for the selected box
|
|
7
|
+
* when it can be drilled — the keyboard route into a leaf, since Enter on a leaf selects it —
|
|
8
|
+
* and Expand / Collapse for the selected group or group member (#166).
|
|
9
|
+
* Renders nothing when there is nothing to show. Each control declares its intent; the
|
|
10
|
+
* `interactions` action performs it, so this reads the state and computes nothing.
|
|
11
|
+
*/
|
|
12
|
+
import { interactions } from '../actions/interactions.js'
|
|
13
|
+
import { say } from '../messages.js'
|
|
14
|
+
import type { GraphState } from '../GraphState.svelte.js'
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
state: GraphState
|
|
18
|
+
/** What the root crumb says. Defaults to the locale's (`All`). */
|
|
19
|
+
rootLabel?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { state: graph, rootLabel = undefined }: Props = $props()
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
{#if graph.showsDrillBar}
|
|
26
|
+
<nav data-graph-drill aria-label={say('drillPath')} use:interactions={{ state: graph }}>
|
|
27
|
+
<ol data-graph-drill-trail>
|
|
28
|
+
{#each graph.breadcrumbs as crumb, i (crumb.path.join('/'))}
|
|
29
|
+
<li>
|
|
30
|
+
{#if crumb.current}
|
|
31
|
+
<span data-graph-drill-crumb aria-current="location"
|
|
32
|
+
>{crumb.label ?? rootLabel ?? say('drillRoot')}</span
|
|
33
|
+
>
|
|
34
|
+
{:else}
|
|
35
|
+
<button type="button" data-graph-drill-crumb data-graph-press="crumb" data-graph-key={String(i)}
|
|
36
|
+
>{crumb.label ?? rootLabel ?? say('drillRoot')}</button
|
|
37
|
+
>
|
|
38
|
+
{/if}
|
|
39
|
+
</li>
|
|
40
|
+
{/each}
|
|
41
|
+
</ol>
|
|
42
|
+
{#if graph.groupAction}
|
|
43
|
+
<!-- The keyboard route through a group: Enter selects a node, then this opens or folds it. -->
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
data-graph-group-action={graph.groupAction.kind}
|
|
47
|
+
data-graph-press="group-selected"
|
|
48
|
+
>{say(graph.groupAction.kind, { name: graph.groupAction.label })}</button
|
|
49
|
+
>
|
|
50
|
+
{/if}
|
|
51
|
+
{#if graph.drillTarget}
|
|
52
|
+
<button type="button" data-graph-drill-open data-graph-press="open-selected"
|
|
53
|
+
>{say('open', { name: graph.drillTarget.name })}</button
|
|
54
|
+
>
|
|
55
|
+
{/if}
|
|
56
|
+
{#if graph.pending}
|
|
57
|
+
<span data-graph-drill-status role="status">{say('drillLoading')}</span>
|
|
58
|
+
{/if}
|
|
59
|
+
{#if graph.drillErrorText}
|
|
60
|
+
<span data-graph-drill-error role="alert">{graph.drillErrorText}</span>
|
|
61
|
+
{/if}
|
|
62
|
+
</nav>
|
|
63
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GraphState } from '../GraphState.svelte.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
state: GraphState;
|
|
4
|
+
/** What the root crumb says. Defaults to the locale's (`All`). */
|
|
5
|
+
rootLabel?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const DrillBar: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type DrillBar = ReturnType<typeof DrillBar>;
|
|
9
|
+
export default DrillBar;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* A toggle rather than a picker: there are exactly two, and a two-option `<select>` costs a
|
|
6
6
|
* reader more interaction than the thing it is choosing between is worth.
|
|
7
7
|
*/
|
|
8
|
+
import ToggleControl from './ToggleControl.svelte'
|
|
9
|
+
import { say } from '../messages.js'
|
|
8
10
|
import type { EdgeStyle } from '../layout/types.js'
|
|
9
11
|
|
|
10
12
|
type Props = {
|
|
@@ -19,13 +21,11 @@
|
|
|
19
21
|
const orthogonal = $derived(edgeStyle === 'orthogonal')
|
|
20
22
|
</script>
|
|
21
23
|
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
data-graph-edge-style
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<span data-graph-control-label>{orthogonal ? 'Angled' : 'Curved'}</span>
|
|
31
|
-
</button>
|
|
24
|
+
<ToggleControl
|
|
25
|
+
pressed={orthogonal}
|
|
26
|
+
hooks={{ 'data-graph-edge-style': edgeStyle }}
|
|
27
|
+
glyph={orthogonal ? '⌐' : '︵'}
|
|
28
|
+
label={say(orthogonal ? 'angled' : 'curved')}
|
|
29
|
+
title={say(orthogonal ? 'angledTitle' : 'curvedTitle')}
|
|
30
|
+
onchange={(next) => onchange?.(next ? 'orthogonal' : 'curved')}
|
|
31
|
+
/>
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Curved or orthogonal connectors.
|
|
3
|
-
*
|
|
4
|
-
* A toggle rather than a picker: there are exactly two, and a two-option `<select>` costs a
|
|
5
|
-
* reader more interaction than the thing it is choosing between is worth.
|
|
6
|
-
*/
|
|
7
1
|
import type { EdgeStyle } from '../layout/types.js';
|
|
8
2
|
type Props = {
|
|
9
3
|
edgeStyle?: EdgeStyle;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Every pair, or only the hidden coupling (#169).
|
|
4
|
+
*
|
|
5
|
+
* A toggle: on a real repo the ordinary pairs vastly outnumber the ones nothing in the code
|
|
6
|
+
* explains, and those are the only rows anyone acts on.
|
|
7
|
+
*/
|
|
8
|
+
import ToggleControl from './ToggleControl.svelte'
|
|
9
|
+
import { say } from '../messages.js'
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
showEdges?: 'all' | 'hidden'
|
|
13
|
+
onchange?: (value: 'all' | 'hidden') => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { showEdges = 'all', onchange }: Props = $props()
|
|
17
|
+
|
|
18
|
+
const only = $derived(showEdges === 'hidden')
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<ToggleControl
|
|
22
|
+
pressed={only}
|
|
23
|
+
hooks={{ 'data-graph-hidden-only': showEdges }}
|
|
24
|
+
label={say(only ? 'hiddenOnly' : 'allPairs')}
|
|
25
|
+
title={say(only ? 'hiddenOnlyTitle' : 'allPairsTitle')}
|
|
26
|
+
onchange={(on) => onchange?.(on ? 'hidden' : 'all')}
|
|
27
|
+
/>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Which measure a channel encodes (#168) — one picker per channel of a polymetric view.
|
|
4
|
+
*
|
|
5
|
+
* The measures offered are the ones the data has (`GraphState.measureKeys`): a picker that
|
|
6
|
+
* lists a metric no node carries offers a box of minimum size everywhere.
|
|
7
|
+
*/
|
|
8
|
+
import { choices } from '../actions/choices.js'
|
|
9
|
+
import { say } from '../messages.js'
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
channel: 'width' | 'height' | 'color'
|
|
13
|
+
value?: string
|
|
14
|
+
/** The measures to offer — `GraphState.measureKeys`, what the data has. */
|
|
15
|
+
options: string[]
|
|
16
|
+
/** Offer "none" — a host with two metrics yet can leave shade off. */
|
|
17
|
+
allowNone?: boolean
|
|
18
|
+
onchange?: (value: string | undefined) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let { channel, value = undefined, options, allowNone = false, onchange }: Props = $props()
|
|
22
|
+
|
|
23
|
+
const LABEL = { width: 'width', height: 'height', color: 'shade' } as const
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<label
|
|
27
|
+
data-graph-measure-control
|
|
28
|
+
use:choices={{ onchoose: (picked) => onchange?.(picked === '' ? undefined : picked) }}
|
|
29
|
+
>
|
|
30
|
+
<span data-graph-control-label>{say(LABEL[channel])}</span>
|
|
31
|
+
<select data-graph-measure={channel} value={value ?? ''}>
|
|
32
|
+
{#if allowNone}
|
|
33
|
+
<option value="">{say('measureNone')}</option>
|
|
34
|
+
{/if}
|
|
35
|
+
{#each options as option (option)}
|
|
36
|
+
<option value={option}>{option}</option>
|
|
37
|
+
{/each}
|
|
38
|
+
</select>
|
|
39
|
+
</label>
|