@tellegen/svelte 0.4.3 → 0.4.5
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 +45 -0
- package/dist/TellegenMap.svelte +63 -22
- package/dist/TellegenProvider.svelte +7 -4
- package/dist/TellegenShell.svelte +12 -1
- package/dist/TellegenViewer.svelte +2 -0
- package/dist/basemap.d.ts +14 -0
- package/dist/basemap.js +27 -0
- package/dist/colors.d.ts +6 -1
- package/dist/colors.js +11 -4
- package/dist/components/DiagnosticLogView.svelte +176 -0
- package/dist/components/DiagnosticLogView.svelte.d.ts +11 -0
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +18 -3
- package/dist/components/LogEntryView.svelte +188 -0
- package/dist/components/LogEntryView.svelte.d.ts +11 -0
- package/dist/components/LogPanel.svelte +25 -0
- package/dist/components/LogPanel.svelte.d.ts +3 -0
- package/dist/components/McResults.svelte +18 -3
- package/dist/components/McWorkbench.svelte +688 -0
- package/dist/components/McWorkbench.svelte.d.ts +38 -0
- package/dist/components/MultiWire.svelte +136 -0
- package/dist/components/MultiWire.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +7 -0
- package/dist/components/NotificationCenter.svelte +37 -6
- package/dist/components/OmniSearch.svelte +163 -0
- package/dist/components/OmniSearch.svelte.d.ts +14 -0
- package/dist/components/ResultsTable.svelte +259 -0
- package/dist/components/ResultsTable.svelte.d.ts +13 -0
- package/dist/components/SchematicCanvas.svelte +1896 -0
- package/dist/components/SchematicCanvas.svelte.d.ts +13 -0
- package/dist/components/SolveCard.svelte +6 -0
- package/dist/components/schematic/PickCard.svelte +126 -0
- package/dist/components/schematic/PickCard.svelte.d.ts +56 -0
- package/dist/components/schematic/export.d.ts +21 -0
- package/dist/components/schematic/export.js +110 -0
- package/dist/components/schematic/layers.d.ts +161 -0
- package/dist/components/schematic/layers.js +756 -0
- package/dist/components/schematic/paint.d.ts +28 -0
- package/dist/components/schematic/paint.js +69 -0
- package/dist/components/schematic/palette.d.ts +34 -0
- package/dist/components/schematic/palette.js +54 -0
- package/dist/components/schematic/text.d.ts +83 -0
- package/dist/components/schematic/text.js +190 -0
- package/dist/components/schematic/view-prefs.svelte.d.ts +30 -0
- package/dist/components/schematic/view-prefs.svelte.js +77 -0
- package/dist/components.d.ts +4 -0
- package/dist/components.js +4 -0
- package/dist/context.svelte.d.ts +2 -0
- package/dist/context.svelte.js +2 -0
- package/dist/controller.svelte.d.ts +40 -5
- package/dist/controller.svelte.js +230 -25
- package/dist/diagram/basemap.d.ts +28 -0
- package/dist/diagram/basemap.js +39 -0
- package/dist/diagram/bus-stacks.d.ts +19 -0
- package/dist/diagram/bus-stacks.js +99 -0
- package/dist/diagram/colour-modes.d.ts +18 -0
- package/dist/diagram/colour-modes.js +33 -0
- package/dist/diagram/connections.d.ts +30 -0
- package/dist/diagram/connections.js +129 -0
- package/dist/diagram/display-tree.d.ts +21 -0
- package/dist/diagram/display-tree.js +65 -0
- package/dist/diagram/energisation.d.ts +10 -0
- package/dist/diagram/energisation.js +31 -0
- package/dist/diagram/feeders.d.ts +30 -0
- package/dist/diagram/feeders.js +57 -0
- package/dist/diagram/galvanic-zones.d.ts +60 -0
- package/dist/diagram/galvanic-zones.js +210 -0
- package/dist/diagram/graph-inputs.d.ts +18 -0
- package/dist/diagram/graph-inputs.js +103 -0
- package/dist/diagram/index.d.ts +28 -0
- package/dist/diagram/index.js +28 -0
- package/dist/diagram/landmarks.d.ts +50 -0
- package/dist/diagram/landmarks.js +153 -0
- package/dist/diagram/layout-morph.d.ts +15 -0
- package/dist/diagram/layout-morph.js +32 -0
- package/dist/diagram/multi-wire.d.ts +44 -0
- package/dist/diagram/multi-wire.js +65 -0
- package/dist/diagram/phase-bars.d.ts +35 -0
- package/dist/diagram/phase-bars.js +44 -0
- package/dist/diagram/results-table.d.ts +47 -0
- package/dist/diagram/results-table.js +173 -0
- package/dist/diagram/schematic-equipment.d.ts +133 -0
- package/dist/diagram/schematic-equipment.js +645 -0
- package/dist/diagram/schematic-layout.d.ts +51 -0
- package/dist/diagram/schematic-layout.js +240 -0
- package/dist/diagram/search.d.ts +36 -0
- package/dist/diagram/search.js +48 -0
- package/dist/diagram/shared-routes.d.ts +21 -0
- package/dist/diagram/shared-routes.js +40 -0
- package/dist/diagram/short-names.d.ts +11 -0
- package/dist/diagram/short-names.js +54 -0
- package/dist/diagram/snapshot.d.ts +115 -0
- package/dist/diagram/snapshot.js +242 -0
- package/dist/diagram/summary.d.ts +65 -0
- package/dist/diagram/summary.js +144 -0
- package/dist/diagram/svg-export.d.ts +80 -0
- package/dist/diagram/svg-export.js +108 -0
- package/dist/diagram/tree-walk.d.ts +13 -0
- package/dist/diagram/tree-walk.js +83 -0
- package/dist/diagram/value-labels.d.ts +24 -0
- package/dist/diagram/value-labels.js +69 -0
- package/dist/diagram/view-state.d.ts +12 -0
- package/dist/diagram/view-state.js +22 -0
- package/dist/diagram/voltage-levels.d.ts +14 -0
- package/dist/diagram/voltage-levels.js +20 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/log.svelte.d.ts +77 -0
- package/dist/log.svelte.js +149 -0
- package/dist/notices.svelte.d.ts +3 -0
- package/dist/state.svelte.d.ts +42 -5
- package/dist/state.svelte.js +56 -4
- package/dist/transformer-icon.d.ts +22 -13
- package/dist/transformer-icon.js +23 -13
- package/package.json +2 -2
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DistGraph } from '@tellegen/engine';
|
|
3
|
+
import MultiWire from './MultiWire.svelte';
|
|
4
|
+
import OmniSearch from './OmniSearch.svelte';
|
|
5
|
+
import ResultsTable from './ResultsTable.svelte';
|
|
6
|
+
import {
|
|
7
|
+
busState,
|
|
8
|
+
conductorBars,
|
|
9
|
+
LOADING_WARNING,
|
|
10
|
+
loadingState,
|
|
11
|
+
magnitudeUnbalance,
|
|
12
|
+
multiWireBranch,
|
|
13
|
+
pairBar,
|
|
14
|
+
queryTable,
|
|
15
|
+
rankIssues,
|
|
16
|
+
snapshotSummary,
|
|
17
|
+
tableCsv,
|
|
18
|
+
tableModel,
|
|
19
|
+
type GraphInputs,
|
|
20
|
+
type Issue,
|
|
21
|
+
type ResultSnapshot,
|
|
22
|
+
type SearchIndex,
|
|
23
|
+
type SearchMatch,
|
|
24
|
+
type TableKind
|
|
25
|
+
} from '../diagram/index.js';
|
|
26
|
+
import { neutralTerminal } from '../multiconductor.js';
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
snapshot,
|
|
30
|
+
graph,
|
|
31
|
+
inputs,
|
|
32
|
+
search,
|
|
33
|
+
loading = false,
|
|
34
|
+
selectedBus,
|
|
35
|
+
selectedEdge,
|
|
36
|
+
onselectbus,
|
|
37
|
+
onselectedge,
|
|
38
|
+
label = 'Multiconductor power flow',
|
|
39
|
+
onanalyse,
|
|
40
|
+
schematic = false,
|
|
41
|
+
onschematic,
|
|
42
|
+
hovered = null,
|
|
43
|
+
onhover,
|
|
44
|
+
onreveal
|
|
45
|
+
}: {
|
|
46
|
+
/** The operating point projected onto `inputs` (`snapshotFromMcPf`); `null` while it is being
|
|
47
|
+
* fetched. `MulticonductorCase.overviewSnapshot` holds a case's, shared with the diagram. */
|
|
48
|
+
snapshot: ResultSnapshot | null;
|
|
49
|
+
graph: DistGraph;
|
|
50
|
+
/** `graphInputs(graph)`; `MulticonductorCase.overviewInputs` holds a case's. */
|
|
51
|
+
inputs: GraphInputs;
|
|
52
|
+
/** `buildSearchIndex(inputs.entities)`; `MulticonductorCase.overviewSearch` holds a case's. */
|
|
53
|
+
search: SearchIndex;
|
|
54
|
+
loading?: boolean;
|
|
55
|
+
selectedBus: string | null;
|
|
56
|
+
selectedEdge: string | null;
|
|
57
|
+
onselectbus: (id: string) => void;
|
|
58
|
+
onselectedge: (id: string) => void;
|
|
59
|
+
/** Case name, used in exported file names and provenance. */
|
|
60
|
+
label?: string;
|
|
61
|
+
/** When set, the overview is computed on request rather than after every solve. */
|
|
62
|
+
onanalyse?: () => void;
|
|
63
|
+
/** Whether the case is shown as a schematic, and how to toggle it. */
|
|
64
|
+
schematic?: boolean;
|
|
65
|
+
onschematic?: (show: boolean) => void;
|
|
66
|
+
/** Bus or branch under the pointer in any linked view. */
|
|
67
|
+
hovered?: { kind: 'bus' | 'branch'; id: string } | null;
|
|
68
|
+
onhover?: (h: { kind: 'bus' | 'branch'; id: string } | null) => void;
|
|
69
|
+
/** Bring the current selection into view again, for an issue clicked while selected. */
|
|
70
|
+
onreveal?: () => void;
|
|
71
|
+
} = $props();
|
|
72
|
+
|
|
73
|
+
const ISSUE_LIMIT = 50;
|
|
74
|
+
const issues = $derived(snapshot ? rankIssues(snapshot) : []);
|
|
75
|
+
const summary = $derived(snapshot ? snapshotSummary(snapshot, issues) : null);
|
|
76
|
+
const limitsDeclared = $derived(
|
|
77
|
+
inputs.buses.some((b) => b.limits) || inputs.branches.some((b) => b.rating)
|
|
78
|
+
);
|
|
79
|
+
/** Declared voltage limits the overview does not rank: sequence or neutral-to-ground bounds, a
|
|
80
|
+
* bound without its other side, or non-uniform phase–phase bounds without named pairs. */
|
|
81
|
+
const unrankedLimits = $derived(!limitsDeclared && graph.buses.some((b) => b.voltage_limits));
|
|
82
|
+
const busIndex = $derived(new Map(graph.buses.map((b, i) => [b.id, i])));
|
|
83
|
+
/** The first edge of each id: a multi-winding transformer's winding-pair edges share its id, and
|
|
84
|
+
* the selection, tables and conductor panel show its first (primary) edge. */
|
|
85
|
+
const edgeIndex = $derived.by(() => {
|
|
86
|
+
const index = new Map<string, number>();
|
|
87
|
+
graph.edges.forEach((e, i) => {
|
|
88
|
+
if (!index.has(e.id)) index.set(e.id, i);
|
|
89
|
+
});
|
|
90
|
+
return index;
|
|
91
|
+
});
|
|
92
|
+
let tableKind = $state<TableKind>('bus');
|
|
93
|
+
const tables = $derived(
|
|
94
|
+
snapshot
|
|
95
|
+
? {
|
|
96
|
+
bus: tableModel(snapshot, inputs.names, 'bus'),
|
|
97
|
+
branch: tableModel(snapshot, inputs.names, 'branch')
|
|
98
|
+
}
|
|
99
|
+
: null
|
|
100
|
+
);
|
|
101
|
+
const tableSelected = $derived(
|
|
102
|
+
tableKind === 'bus'
|
|
103
|
+
? selectedBus !== null
|
|
104
|
+
? (busIndex.get(selectedBus) ?? -1)
|
|
105
|
+
: -1
|
|
106
|
+
: selectedEdge !== null
|
|
107
|
+
? (edgeIndex.get(selectedEdge) ?? -1)
|
|
108
|
+
: -1
|
|
109
|
+
);
|
|
110
|
+
const tableHovered = $derived(
|
|
111
|
+
hovered?.kind === tableKind
|
|
112
|
+
? ((tableKind === 'bus' ? busIndex : edgeIndex).get(hovered.id) ?? -1)
|
|
113
|
+
: -1
|
|
114
|
+
);
|
|
115
|
+
/** Search results show the bus voltage nearest a limit, or the branch loading. */
|
|
116
|
+
function searchHint(m: SearchMatch) {
|
|
117
|
+
if (!snapshot) return null;
|
|
118
|
+
if (m.kind === 'branch') {
|
|
119
|
+
const loading = snapshot.branches[m.index].loading;
|
|
120
|
+
return loading === null ? null : { text: `${n(loading, 1)} %`, state: loadingState(loading) };
|
|
121
|
+
}
|
|
122
|
+
const bus = snapshot.buses[m.index];
|
|
123
|
+
if (!bus.pairs.length) return null;
|
|
124
|
+
const lowest = bus.pairs.reduce((a, b) => (b.volts < a.volts ? b : a));
|
|
125
|
+
return { text: `${n(lowest.volts)} V`, state: busState(bus) };
|
|
126
|
+
}
|
|
127
|
+
/** Selecting what is already selected would toggle it off in the controller. */
|
|
128
|
+
function selectOnce(kind: 'bus' | 'branch', index: number) {
|
|
129
|
+
const id = kind === 'bus' ? graph.buses[index].id : graph.edges[index].id;
|
|
130
|
+
if (id !== (kind === 'bus' ? selectedBus : selectedEdge)) select(kind, index);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const n = (v: number, digits = 2) =>
|
|
134
|
+
v.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
|
135
|
+
/** Compact range: kV above 1 kV, four significant figures; exact values are in the tables. */
|
|
136
|
+
function range(a: number, b: number): string {
|
|
137
|
+
const k = Math.max(Math.abs(a), Math.abs(b)) >= 1000;
|
|
138
|
+
const f = (v: number) => Number((k ? v / 1000 : v).toPrecision(4)).toLocaleString();
|
|
139
|
+
return `${f(a)}–${f(b)} ${k ? 'kV' : 'V'}`;
|
|
140
|
+
}
|
|
141
|
+
const zoneLabel = (zone: string) =>
|
|
142
|
+
zone === 'phase–neutral'
|
|
143
|
+
? 'V phase–neutral'
|
|
144
|
+
: zone === 'phase–phase'
|
|
145
|
+
? 'V phase–phase'
|
|
146
|
+
: `V ${zone}`;
|
|
147
|
+
|
|
148
|
+
/** An issue click selects it; clicking the selected issue again shows it rather than clearing it. */
|
|
149
|
+
function revealOrSelect(kind: 'bus' | 'branch', index: number) {
|
|
150
|
+
const id = kind === 'bus' ? graph.buses[index].id : graph.edges[index].id;
|
|
151
|
+
if (id === (kind === 'bus' ? selectedBus : selectedEdge)) onreveal?.();
|
|
152
|
+
else select(kind, index);
|
|
153
|
+
}
|
|
154
|
+
function select(kind: 'bus' | 'branch', index: number) {
|
|
155
|
+
if (kind === 'bus') onselectbus(graph.buses[index].id);
|
|
156
|
+
else onselectedge(graph.edges[index].id);
|
|
157
|
+
}
|
|
158
|
+
function issueText(issue: Issue) {
|
|
159
|
+
if (issue.kind === 'voltage') {
|
|
160
|
+
const id = graph.buses[issue.target.index].id;
|
|
161
|
+
return {
|
|
162
|
+
id,
|
|
163
|
+
value: `${n(issue.value)} V`,
|
|
164
|
+
detail: `${issue.pair?.join('–')} ${issue.low ? 'below' : 'above'} ${n(issue.limit)} V by ${n(issue.score)} %`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const edge = graph.edges[issue.target.index];
|
|
168
|
+
return {
|
|
169
|
+
id: edge.id,
|
|
170
|
+
value: `${n(issue.value, 1)} %`,
|
|
171
|
+
detail: `${edge.kind} · ${issue.severity === 'violation' ? 'over rating' : `≥ ${LOADING_WARNING} % of rating`}`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const isSelected = (issue: Issue) =>
|
|
175
|
+
issue.target.kind === 'bus'
|
|
176
|
+
? graph.buses[issue.target.index]?.id === selectedBus
|
|
177
|
+
: graph.edges[issue.target.index]?.id === selectedEdge;
|
|
178
|
+
|
|
179
|
+
const busSelection = $derived(
|
|
180
|
+
snapshot && selectedBus !== null && busIndex.has(selectedBus)
|
|
181
|
+
? snapshot.buses[busIndex.get(selectedBus) as number]
|
|
182
|
+
: null
|
|
183
|
+
);
|
|
184
|
+
// Phase–neutral magnitudes also move with neutral shift, which VUF excludes; show both.
|
|
185
|
+
const busSpread = $derived(
|
|
186
|
+
busSelection?.unbalance ? magnitudeUnbalance(busSelection.pairs) : null
|
|
187
|
+
);
|
|
188
|
+
const multiWire = $derived(
|
|
189
|
+
selectedEdge !== null && edgeIndex.has(selectedEdge)
|
|
190
|
+
? multiWireBranch(graph, edgeIndex.get(selectedEdge) as number, snapshot)
|
|
191
|
+
: null
|
|
192
|
+
);
|
|
193
|
+
const edgeSelection = $derived.by(() => {
|
|
194
|
+
if (!snapshot || selectedEdge === null || !edgeIndex.has(selectedEdge)) return null;
|
|
195
|
+
const k = edgeIndex.get(selectedEdge) as number;
|
|
196
|
+
// The neutral as the from bus declares it, whatever it is named (PMD numbers it 4).
|
|
197
|
+
const from = graph.buses[busIndex.get(graph.edges[k].from) ?? -1];
|
|
198
|
+
const neutral = from ? neutralTerminal(from.terminals, from.neutral_terminal) : null;
|
|
199
|
+
return conductorBars(snapshot.branches[k].ends, neutral ?? '');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
function exportCsv(kind: TableKind) {
|
|
203
|
+
if (!snapshot) return;
|
|
204
|
+
const model = tableModel(snapshot, inputs.names, kind);
|
|
205
|
+
const csv = tableCsv(model, queryTable(model), [
|
|
206
|
+
`Tellegen export ${new Date().toISOString()}`,
|
|
207
|
+
`Case: ${label}`,
|
|
208
|
+
`${snapshot.label}; ${snapshot.iterations ?? '?'} iterations`,
|
|
209
|
+
'P/Q are positive into the equipment at each end; voltages are terminal-pair magnitudes'
|
|
210
|
+
]);
|
|
211
|
+
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
|
212
|
+
const a = document.createElement('a');
|
|
213
|
+
a.href = url;
|
|
214
|
+
a.download = `${label.replace(/[^\w.-]+/g, '_')}-${kind === 'bus' ? 'buses' : 'branches'}.csv`;
|
|
215
|
+
a.click();
|
|
216
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
217
|
+
}
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<section class="workbench" aria-label="Operating point overview">
|
|
221
|
+
<div class="heading">
|
|
222
|
+
<h4>Overview</h4>
|
|
223
|
+
{#if loading}<span role="status">Reading the full operating point…</span>{/if}
|
|
224
|
+
{#if onschematic}<button
|
|
225
|
+
class="schematic-toggle"
|
|
226
|
+
aria-pressed={schematic}
|
|
227
|
+
onclick={() => onschematic(!schematic)}>{schematic ? 'Show map' : 'Show diagram'}</button
|
|
228
|
+
>{/if}
|
|
229
|
+
</div>
|
|
230
|
+
<OmniSearch
|
|
231
|
+
index={search}
|
|
232
|
+
hint={searchHint}
|
|
233
|
+
onselect={(m) => selectOnce(m.kind === 'bus' ? 'bus' : 'branch', m.index)}
|
|
234
|
+
/>
|
|
235
|
+
{#if summary}
|
|
236
|
+
{@const { voltage, overload, warning } = summary.counts}
|
|
237
|
+
<div class="kpis">
|
|
238
|
+
<button
|
|
239
|
+
class="kpi"
|
|
240
|
+
class:violation={voltage + overload > 0}
|
|
241
|
+
class:warning={voltage + overload === 0 && warning > 0}
|
|
242
|
+
disabled={!issues.length}
|
|
243
|
+
onclick={() => issues[0] && select(issues[0].target.kind, issues[0].target.index)}
|
|
244
|
+
title="Select the worst issue"
|
|
245
|
+
>
|
|
246
|
+
<span class="label">Violations</span>
|
|
247
|
+
<span class="value"
|
|
248
|
+
>{voltage + overload ? (voltage + overload).toLocaleString() : 'None'}</span
|
|
249
|
+
>
|
|
250
|
+
<span class="sub"
|
|
251
|
+
>{warning
|
|
252
|
+
? `+ ${warning} warning${warning === 1 ? '' : 's'}`
|
|
253
|
+
: limitsDeclared
|
|
254
|
+
? 'within declared limits'
|
|
255
|
+
: unrankedLimits
|
|
256
|
+
? 'no ranked limits'
|
|
257
|
+
: 'no limits declared'}</span
|
|
258
|
+
>
|
|
259
|
+
</button>
|
|
260
|
+
<div class="kpi" title="Sum of both into-equipment end powers over active branches">
|
|
261
|
+
<span class="label">Losses</span>
|
|
262
|
+
<span class="value">{n(summary.loss.p)} kW</span>
|
|
263
|
+
<span class="sub">{n(summary.loss.q)} kvar</span>
|
|
264
|
+
</div>
|
|
265
|
+
{#each summary.zones as zone (zone.zone)}
|
|
266
|
+
{@const bad =
|
|
267
|
+
zone.min.state === 'violation'
|
|
268
|
+
? zone.min
|
|
269
|
+
: zone.max.state === 'violation'
|
|
270
|
+
? zone.max
|
|
271
|
+
: null}
|
|
272
|
+
<button
|
|
273
|
+
class="kpi"
|
|
274
|
+
class:violation={!!bad}
|
|
275
|
+
onclick={() => select('bus', (bad ?? zone.min).bus)}
|
|
276
|
+
title={`Minimum at ${graph.buses[zone.min.bus].id}, maximum at ${graph.buses[zone.max.bus].id}`}
|
|
277
|
+
>
|
|
278
|
+
<span class="label">{zoneLabel(zone.zone)}</span>
|
|
279
|
+
<span class="value">{range(zone.min.volts, zone.max.volts)}</span>
|
|
280
|
+
<span class="sub">{bad ? 'limit violated' : `${zone.buses.toLocaleString()} buses`}</span>
|
|
281
|
+
</button>
|
|
282
|
+
{/each}
|
|
283
|
+
{#if summary.loading}
|
|
284
|
+
{@const top = summary.loading}
|
|
285
|
+
<button
|
|
286
|
+
class="kpi"
|
|
287
|
+
class:violation={top.value > 100}
|
|
288
|
+
class:warning={top.value >= 90 && top.value <= 100}
|
|
289
|
+
onclick={() => select('branch', top.branch)}
|
|
290
|
+
title={snapshot?.branches[top.branch].basis ?? ''}
|
|
291
|
+
>
|
|
292
|
+
<span class="label">Max loading</span>
|
|
293
|
+
<span class="value">{n(top.value, 1)} %</span>
|
|
294
|
+
<span class="sub">{graph.edges[top.branch].id}</span>
|
|
295
|
+
</button>
|
|
296
|
+
{/if}
|
|
297
|
+
{#if summary.unbalance}
|
|
298
|
+
{@const u = summary.unbalance}
|
|
299
|
+
<button
|
|
300
|
+
class="kpi"
|
|
301
|
+
onclick={() => select('bus', u.bus)}
|
|
302
|
+
title={u.definition === 'VUF'
|
|
303
|
+
? 'Voltage unbalance factor |V2| / |V1| from terminal phasors'
|
|
304
|
+
: 'Magnitude-only unbalance: max deviation from the mean / mean'}
|
|
305
|
+
>
|
|
306
|
+
<span class="label">Max unbalance</span>
|
|
307
|
+
<span class="value">{n(u.percent)} %</span>
|
|
308
|
+
<span class="sub">{u.definition} · {graph.buses[u.bus].id}</span>
|
|
309
|
+
</button>
|
|
310
|
+
{/if}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<h4>Issues <span class="count">{issues.length.toLocaleString()}</span></h4>
|
|
314
|
+
{#if unrankedLimits}
|
|
315
|
+
<p class="hint">
|
|
316
|
+
This case declares no ratings, and none of its voltage limits is a band the overview ranks
|
|
317
|
+
(phase–neutral, phase–phase or phase–ground), so nothing is ranked against them.
|
|
318
|
+
</p>
|
|
319
|
+
{:else if !limitsDeclared}
|
|
320
|
+
<p class="hint">
|
|
321
|
+
This case declares no voltage limits or ratings, so nothing is ranked against them.
|
|
322
|
+
</p>
|
|
323
|
+
{:else if !issues.length}
|
|
324
|
+
<p class="hint ok">No limit violations or loading warnings.</p>
|
|
325
|
+
{:else}
|
|
326
|
+
<ol class="issues">
|
|
327
|
+
{#each issues.slice(0, ISSUE_LIMIT) as issue (`${issue.target.kind}:${issue.target.index}`)}
|
|
328
|
+
{@const t = issueText(issue)}
|
|
329
|
+
<li class={issue.severity}>
|
|
330
|
+
<button
|
|
331
|
+
aria-current={isSelected(issue)}
|
|
332
|
+
onclick={() => revealOrSelect(issue.target.kind, issue.target.index)}
|
|
333
|
+
title={`${t.id} · ${t.detail}`}
|
|
334
|
+
>
|
|
335
|
+
<span class="sev" aria-label={issue.severity}
|
|
336
|
+
>{issue.severity === 'violation' ? '!' : '▲'}</span
|
|
337
|
+
>
|
|
338
|
+
<span class="id">{t.id}</span>
|
|
339
|
+
<span class="num">{t.value}</span>
|
|
340
|
+
<span class="detail">{t.detail}</span>
|
|
341
|
+
</button>
|
|
342
|
+
</li>
|
|
343
|
+
{/each}
|
|
344
|
+
</ol>
|
|
345
|
+
{#if issues.length > ISSUE_LIMIT}<p class="hint">
|
|
346
|
+
Showing the worst {ISSUE_LIMIT} of {issues.length.toLocaleString()}; the CSV has all rows.
|
|
347
|
+
</p>{/if}
|
|
348
|
+
{/if}
|
|
349
|
+
|
|
350
|
+
{#if busSelection}
|
|
351
|
+
<h4>Phases at {selectedBus}</h4>
|
|
352
|
+
{#if busSelection.pairs.length}
|
|
353
|
+
<div class="bars" role="list" aria-label="Pair voltages against declared limits">
|
|
354
|
+
{#each busSelection.pairs.map(pairBar) as bar, k (k)}
|
|
355
|
+
<div class="bar {bar.state}" role="listitem">
|
|
356
|
+
<span class="mono">{bar.pair.join('–')}</span>
|
|
357
|
+
<span class="track">
|
|
358
|
+
{#if bar.band}<i
|
|
359
|
+
class="band"
|
|
360
|
+
style:left="{bar.band.left}%"
|
|
361
|
+
style:width="{bar.band.width}%"
|
|
362
|
+
></i>{/if}
|
|
363
|
+
<i class="mark" style:left="{bar.marker}%"></i>
|
|
364
|
+
</span>
|
|
365
|
+
<span class="num">{n(bar.volts)} V{bar.state === 'violation' ? ' !' : ''}</span>
|
|
366
|
+
</div>
|
|
367
|
+
{/each}
|
|
368
|
+
</div>
|
|
369
|
+
<p class="hint">
|
|
370
|
+
Track: the declared band ± 25 %{busSelection.unbalance
|
|
371
|
+
? ` · unbalance ${n(busSelection.unbalance.percent)} % (${busSelection.unbalance.definition})`
|
|
372
|
+
: ''}{busSpread
|
|
373
|
+
? ` · magnitude spread ${n(busSpread.percent)} % (${busSpread.definition})`
|
|
374
|
+
: ''}
|
|
375
|
+
</p>
|
|
376
|
+
{#if busSelection.unbalance && busSpread && busSpread.percent > 2 * busSelection.unbalance.percent}
|
|
377
|
+
<p class="hint">
|
|
378
|
+
The spread is mostly neutral shift (zero sequence), which VUF excludes.
|
|
379
|
+
</p>
|
|
380
|
+
{/if}
|
|
381
|
+
{:else}<p class="hint">No terminal-pair voltages at this bus.</p>{/if}
|
|
382
|
+
{:else if edgeSelection}
|
|
383
|
+
<h4>Conductors on {selectedEdge}</h4>
|
|
384
|
+
{#each edgeSelection.ends as end, side (side)}
|
|
385
|
+
{#if end}
|
|
386
|
+
<p class="end">{side ? 'To' : 'From'} end · {n(end.p)} kW · {n(end.q)} kvar</p>
|
|
387
|
+
<div
|
|
388
|
+
class="bars"
|
|
389
|
+
role="list"
|
|
390
|
+
aria-label={`${side ? 'To' : 'From'} end conductor currents`}
|
|
391
|
+
>
|
|
392
|
+
{#each end.bars as bar, k (k)}
|
|
393
|
+
<div class="bar" class:neutral={bar.neutral} role="listitem">
|
|
394
|
+
<span class="mono">{bar.terminal}</span>
|
|
395
|
+
<span class="track"><i class="fill" style:width="{bar.fill}%"></i></span>
|
|
396
|
+
<span class="num">{n(bar.amps, 1)} A</span>
|
|
397
|
+
</div>
|
|
398
|
+
{/each}
|
|
399
|
+
</div>
|
|
400
|
+
{/if}
|
|
401
|
+
{/each}
|
|
402
|
+
<p class="hint">
|
|
403
|
+
One scale for both ends (max {n(edgeSelection.scaleAmps, 1)} A){edgeSelection.neutralShare !==
|
|
404
|
+
null
|
|
405
|
+
? ` · neutral carries ${n(edgeSelection.neutralShare, 0)} % of the largest phase current`
|
|
406
|
+
: ''}
|
|
407
|
+
</p>
|
|
408
|
+
{#if multiWire}
|
|
409
|
+
<h4>Multi-wire view</h4>
|
|
410
|
+
<MultiWire view={multiWire} />
|
|
411
|
+
{/if}
|
|
412
|
+
{/if}
|
|
413
|
+
|
|
414
|
+
{#if tables}
|
|
415
|
+
<details class="tables">
|
|
416
|
+
<summary>Results table</summary>
|
|
417
|
+
<div class="tabs" role="tablist" aria-label="Results table">
|
|
418
|
+
{#each ['bus', 'branch'] as const as kind (kind)}
|
|
419
|
+
<button role="tab" aria-selected={tableKind === kind} onclick={() => (tableKind = kind)}
|
|
420
|
+
>{kind === 'bus' ? 'Buses' : 'Branches'}</button
|
|
421
|
+
>
|
|
422
|
+
{/each}
|
|
423
|
+
</div>
|
|
424
|
+
{#key tableKind}
|
|
425
|
+
<ResultsTable
|
|
426
|
+
model={tables[tableKind]}
|
|
427
|
+
selected={tableSelected}
|
|
428
|
+
onselect={(i) => selectOnce(tableKind, i)}
|
|
429
|
+
hovered={tableHovered}
|
|
430
|
+
onhover={(i) =>
|
|
431
|
+
onhover?.(
|
|
432
|
+
i < 0
|
|
433
|
+
? null
|
|
434
|
+
: {
|
|
435
|
+
kind: tableKind,
|
|
436
|
+
id: tableKind === 'bus' ? graph.buses[i].id : graph.edges[i].id
|
|
437
|
+
}
|
|
438
|
+
)}
|
|
439
|
+
/>
|
|
440
|
+
{/key}
|
|
441
|
+
</details>
|
|
442
|
+
{/if}
|
|
443
|
+
|
|
444
|
+
<div class="actions">
|
|
445
|
+
<button onclick={() => exportCsv('bus')}>Bus CSV</button>
|
|
446
|
+
<button onclick={() => exportCsv('branch')}>Branch CSV</button>
|
|
447
|
+
</div>
|
|
448
|
+
{:else if !loading && onanalyse}
|
|
449
|
+
<p class="hint">
|
|
450
|
+
This case is large, so the overview reads the full operating point on request.
|
|
451
|
+
</p>
|
|
452
|
+
<div class="actions"><button onclick={onanalyse}>Analyse operating point</button></div>
|
|
453
|
+
{:else if !loading}
|
|
454
|
+
<p class="hint">Run the power flow to see the overview.</p>
|
|
455
|
+
{/if}
|
|
456
|
+
</section>
|
|
457
|
+
|
|
458
|
+
<style>
|
|
459
|
+
.workbench {
|
|
460
|
+
margin: 14px 0;
|
|
461
|
+
padding-top: 12px;
|
|
462
|
+
border-top: 1px solid var(--line);
|
|
463
|
+
font-size: 11px;
|
|
464
|
+
}
|
|
465
|
+
.heading {
|
|
466
|
+
display: flex;
|
|
467
|
+
justify-content: space-between;
|
|
468
|
+
align-items: baseline;
|
|
469
|
+
gap: 8px;
|
|
470
|
+
}
|
|
471
|
+
.heading span,
|
|
472
|
+
.hint,
|
|
473
|
+
.count,
|
|
474
|
+
.end,
|
|
475
|
+
.kpi .label,
|
|
476
|
+
.kpi .sub,
|
|
477
|
+
.detail {
|
|
478
|
+
color: var(--text-secondary);
|
|
479
|
+
}
|
|
480
|
+
h4 {
|
|
481
|
+
margin: 12px 0 6px;
|
|
482
|
+
font-size: 11px;
|
|
483
|
+
font-weight: 600;
|
|
484
|
+
}
|
|
485
|
+
.kpis {
|
|
486
|
+
display: grid;
|
|
487
|
+
grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
|
|
488
|
+
gap: 1px;
|
|
489
|
+
background: var(--line);
|
|
490
|
+
border: 1px solid var(--line);
|
|
491
|
+
border-radius: 3px;
|
|
492
|
+
overflow: hidden;
|
|
493
|
+
}
|
|
494
|
+
.kpi {
|
|
495
|
+
display: flex;
|
|
496
|
+
flex-direction: column;
|
|
497
|
+
gap: 2px;
|
|
498
|
+
min-width: 0;
|
|
499
|
+
padding: 6px 8px;
|
|
500
|
+
text-align: left;
|
|
501
|
+
font: inherit;
|
|
502
|
+
color: var(--ink);
|
|
503
|
+
background: var(--paper);
|
|
504
|
+
border: 0;
|
|
505
|
+
border-radius: 0;
|
|
506
|
+
}
|
|
507
|
+
button.kpi {
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
}
|
|
510
|
+
button.kpi:disabled {
|
|
511
|
+
cursor: default;
|
|
512
|
+
}
|
|
513
|
+
.kpi .label {
|
|
514
|
+
font: 9px var(--font-mono);
|
|
515
|
+
letter-spacing: 0.5px;
|
|
516
|
+
text-transform: uppercase;
|
|
517
|
+
}
|
|
518
|
+
.kpi .value {
|
|
519
|
+
font: 12px var(--font-mono);
|
|
520
|
+
}
|
|
521
|
+
.kpi .label,
|
|
522
|
+
.kpi .value,
|
|
523
|
+
.kpi .sub {
|
|
524
|
+
overflow: hidden;
|
|
525
|
+
text-overflow: ellipsis;
|
|
526
|
+
white-space: nowrap;
|
|
527
|
+
}
|
|
528
|
+
.kpi.violation .value,
|
|
529
|
+
.violation .sev,
|
|
530
|
+
.violation .num {
|
|
531
|
+
color: var(--text-danger);
|
|
532
|
+
}
|
|
533
|
+
.kpi.warning .value,
|
|
534
|
+
.warning .sev,
|
|
535
|
+
.warning .num {
|
|
536
|
+
color: var(--text-accent);
|
|
537
|
+
}
|
|
538
|
+
.ok {
|
|
539
|
+
color: var(--pos);
|
|
540
|
+
}
|
|
541
|
+
.issues {
|
|
542
|
+
list-style: none;
|
|
543
|
+
margin: 0;
|
|
544
|
+
padding: 0;
|
|
545
|
+
max-height: 240px;
|
|
546
|
+
overflow: auto;
|
|
547
|
+
}
|
|
548
|
+
.issues button {
|
|
549
|
+
display: grid;
|
|
550
|
+
grid-template-columns: 12px 1fr auto;
|
|
551
|
+
gap: 1px 6px;
|
|
552
|
+
width: 100%;
|
|
553
|
+
padding: 5px 6px;
|
|
554
|
+
text-align: left;
|
|
555
|
+
font: inherit;
|
|
556
|
+
color: var(--ink);
|
|
557
|
+
background: transparent;
|
|
558
|
+
border: 0;
|
|
559
|
+
border-radius: 3px;
|
|
560
|
+
cursor: pointer;
|
|
561
|
+
}
|
|
562
|
+
.issues button:hover,
|
|
563
|
+
.issues button[aria-current='true'] {
|
|
564
|
+
background: var(--accent-soft);
|
|
565
|
+
}
|
|
566
|
+
.sev {
|
|
567
|
+
font: 700 11px var(--font-mono);
|
|
568
|
+
}
|
|
569
|
+
.id,
|
|
570
|
+
.num,
|
|
571
|
+
.mono {
|
|
572
|
+
font-family: var(--font-mono);
|
|
573
|
+
white-space: nowrap;
|
|
574
|
+
}
|
|
575
|
+
.id {
|
|
576
|
+
overflow: hidden;
|
|
577
|
+
text-overflow: ellipsis;
|
|
578
|
+
}
|
|
579
|
+
.detail {
|
|
580
|
+
grid-column: 2 / 4;
|
|
581
|
+
}
|
|
582
|
+
.bars {
|
|
583
|
+
display: flex;
|
|
584
|
+
flex-direction: column;
|
|
585
|
+
gap: 4px;
|
|
586
|
+
}
|
|
587
|
+
.bar {
|
|
588
|
+
display: grid;
|
|
589
|
+
grid-template-columns: 30px 1fr 84px;
|
|
590
|
+
gap: 6px;
|
|
591
|
+
align-items: center;
|
|
592
|
+
}
|
|
593
|
+
.bar .num {
|
|
594
|
+
text-align: right;
|
|
595
|
+
}
|
|
596
|
+
.track {
|
|
597
|
+
position: relative;
|
|
598
|
+
height: 9px;
|
|
599
|
+
background: var(--surface-control);
|
|
600
|
+
border-radius: 2px;
|
|
601
|
+
overflow: hidden;
|
|
602
|
+
}
|
|
603
|
+
.band {
|
|
604
|
+
position: absolute;
|
|
605
|
+
inset: 0 auto;
|
|
606
|
+
background: color-mix(in srgb, var(--pos) 25%, transparent);
|
|
607
|
+
border-inline: 1px solid var(--text-secondary);
|
|
608
|
+
}
|
|
609
|
+
.mark {
|
|
610
|
+
position: absolute;
|
|
611
|
+
inset: -1px auto;
|
|
612
|
+
width: 3px;
|
|
613
|
+
margin-left: -1.5px;
|
|
614
|
+
background: var(--ink);
|
|
615
|
+
}
|
|
616
|
+
.violation .mark {
|
|
617
|
+
background: var(--text-danger);
|
|
618
|
+
}
|
|
619
|
+
.fill {
|
|
620
|
+
position: absolute;
|
|
621
|
+
inset: 0 auto 0 0;
|
|
622
|
+
background: var(--text-secondary);
|
|
623
|
+
}
|
|
624
|
+
.neutral .fill {
|
|
625
|
+
background: var(--accent);
|
|
626
|
+
}
|
|
627
|
+
.end {
|
|
628
|
+
margin: 8px 0 3px;
|
|
629
|
+
}
|
|
630
|
+
.schematic-toggle {
|
|
631
|
+
margin-left: auto;
|
|
632
|
+
font: inherit;
|
|
633
|
+
color: var(--ink);
|
|
634
|
+
background: var(--paper);
|
|
635
|
+
border: 1px solid var(--line);
|
|
636
|
+
border-radius: 3px;
|
|
637
|
+
padding: 3px 8px;
|
|
638
|
+
cursor: pointer;
|
|
639
|
+
}
|
|
640
|
+
.schematic-toggle[aria-pressed='true'] {
|
|
641
|
+
color: var(--accent);
|
|
642
|
+
background: var(--accent-soft);
|
|
643
|
+
}
|
|
644
|
+
.workbench :global(.omni) {
|
|
645
|
+
margin: 6px 0 10px;
|
|
646
|
+
}
|
|
647
|
+
.tables {
|
|
648
|
+
margin-top: 12px;
|
|
649
|
+
}
|
|
650
|
+
.tables summary {
|
|
651
|
+
cursor: pointer;
|
|
652
|
+
font-weight: 600;
|
|
653
|
+
}
|
|
654
|
+
.tabs {
|
|
655
|
+
display: flex;
|
|
656
|
+
gap: 12px;
|
|
657
|
+
margin: 8px 0 6px;
|
|
658
|
+
}
|
|
659
|
+
.tabs button {
|
|
660
|
+
font: 10px var(--font-mono);
|
|
661
|
+
text-transform: uppercase;
|
|
662
|
+
letter-spacing: 0.5px;
|
|
663
|
+
color: var(--text-secondary);
|
|
664
|
+
background: transparent;
|
|
665
|
+
border: 0;
|
|
666
|
+
border-bottom: 2px solid transparent;
|
|
667
|
+
padding: 2px 0;
|
|
668
|
+
cursor: pointer;
|
|
669
|
+
}
|
|
670
|
+
.tabs button[aria-selected='true'] {
|
|
671
|
+
color: var(--ink);
|
|
672
|
+
border-bottom-color: var(--accent);
|
|
673
|
+
}
|
|
674
|
+
.actions {
|
|
675
|
+
display: flex;
|
|
676
|
+
gap: 6px;
|
|
677
|
+
margin-top: 12px;
|
|
678
|
+
}
|
|
679
|
+
.actions button {
|
|
680
|
+
font: inherit;
|
|
681
|
+
color: var(--ink);
|
|
682
|
+
background: var(--paper);
|
|
683
|
+
border: 1px solid var(--line);
|
|
684
|
+
border-radius: 3px;
|
|
685
|
+
padding: 5px 7px;
|
|
686
|
+
cursor: pointer;
|
|
687
|
+
}
|
|
688
|
+
</style>
|