@tellegen/svelte 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -5
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +518 -202
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +40 -4
- package/dist/TellegenShell.svelte +24 -46
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +18 -3
- package/dist/api.js +8 -2
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +25 -7
- package/dist/colors.js +39 -13
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +44 -19
- package/dist/components/BindingLines.svelte +4 -7
- package/dist/components/BusPicker.svelte +11 -5
- package/dist/components/ControlPanel.svelte +106 -343
- package/dist/components/DemandSlider.svelte +36 -29
- package/dist/components/DiagramCanvas.svelte +486 -0
- package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
- package/dist/components/DisplayControls.svelte +8 -2
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +9 -4
- package/dist/components/LocalCaseDetails.svelte +61 -48
- package/dist/components/McResults.svelte +428 -0
- package/dist/components/McResults.svelte.d.ts +17 -0
- package/dist/components/ModelDetails.svelte +230 -0
- package/dist/components/ModelDetails.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +232 -83
- package/dist/components/NetworkStats.svelte +20 -18
- package/dist/components/NotificationCenter.svelte +245 -0
- package/dist/components/NotificationCenter.svelte.d.ts +3 -0
- package/dist/components/PanelCard.svelte +384 -0
- package/dist/components/PanelCard.svelte.d.ts +9 -0
- package/dist/components/PanelFrame.svelte +46 -0
- package/dist/components/PanelFrame.svelte.d.ts +16 -0
- package/dist/components/PanelHost.svelte +237 -0
- package/dist/components/PanelHost.svelte.d.ts +6 -0
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +99 -0
- package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
- package/dist/components/RatingSlider.svelte +19 -14
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +214 -0
- package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
- package/dist/components/SensitivityReadout.svelte +26 -18
- package/dist/components/ShellEnvironment.svelte +51 -0
- package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
- package/dist/components/SizeLegend.svelte +2 -2
- package/dist/components/SolveCard.svelte +48 -51
- package/dist/components/TopMovers.svelte +9 -4
- package/dist/components/TopMovers.svelte.d.ts +2 -17
- package/dist/components.d.ts +5 -0
- package/dist/components.js +5 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +117 -36
- package/dist/controller.svelte.js +1133 -253
- package/dist/diagram-labels.d.ts +9 -0
- package/dist/diagram-labels.js +40 -0
- package/dist/display.d.ts +3 -7
- package/dist/display.js +31 -27
- package/dist/format.d.ts +9 -0
- package/dist/format.js +17 -4
- package/dist/index.d.ts +15 -5
- package/dist/index.js +9 -2
- package/dist/map.d.ts +4 -2
- package/dist/map.js +2 -1
- package/dist/maplibre-worker.d.ts +5 -0
- package/dist/maplibre-worker.dev.d.ts +11 -0
- package/dist/maplibre-worker.dev.js +15 -0
- package/dist/maplibre-worker.js +8 -0
- package/dist/multiconductor.d.ts +11 -2
- package/dist/multiconductor.js +107 -14
- package/dist/notices.svelte.d.ts +29 -0
- package/dist/notices.svelte.js +86 -0
- package/dist/panels.svelte.d.ts +71 -0
- package/dist/panels.svelte.js +245 -0
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +107 -23
- package/dist/state.svelte.js +106 -20
- package/dist/styles.css +59 -6
- package/dist/synthetic-layout.js +1 -0
- package/package.json +29 -11
- package/dist/study-package.d.ts +0 -8
- package/dist/study-package.js +0 -17
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, untrack } from 'svelte';
|
|
3
|
+
import { getAppState } from '../context.svelte.js';
|
|
4
|
+
import { getPanelLayout } from '../panels.svelte.js';
|
|
5
|
+
import { getNoticeCenter } from '../notices.svelte.js';
|
|
6
|
+
import PanelCard from './PanelCard.svelte';
|
|
7
|
+
|
|
8
|
+
// The dock insets come from `--tellegen-dock-left/right/bottom` on an
|
|
9
|
+
// ancestor (defaults 20px, 20px, 100px), so a custom shell with its own
|
|
10
|
+
// rail or status bar can move the docks without restyling them.
|
|
11
|
+
let { showToolbar = true }: { showToolbar?: boolean } = $props();
|
|
12
|
+
|
|
13
|
+
const app = getAppState();
|
|
14
|
+
const layout = getPanelLayout();
|
|
15
|
+
const notices = getNoticeCenter();
|
|
16
|
+
const bottomReserve = $derived(notices.entries.length ? 120 : 88);
|
|
17
|
+
const left = $derived(layout.ordered('left').filter((p) => p.open));
|
|
18
|
+
const right = $derived(layout.ordered('right').filter((p) => p.open));
|
|
19
|
+
const floating = $derived(
|
|
20
|
+
layout.ordered().filter((p) => p.open && layout.position(p) === 'floating')
|
|
21
|
+
);
|
|
22
|
+
const drawer = $derived(layout.panels.find((p) => p.id === layout.drawer));
|
|
23
|
+
const toolbarTop = $derived(app.headerInset + 6);
|
|
24
|
+
const panelTop = $derived(toolbarTop + 42);
|
|
25
|
+
const drawerHeight = $derived(
|
|
26
|
+
Math.max(
|
|
27
|
+
1,
|
|
28
|
+
Math.min(
|
|
29
|
+
layout.viewportHeight * 0.6,
|
|
30
|
+
(layout.viewportHeight - panelTop - bottomReserve - 8) * 0.75
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
function resize() {
|
|
35
|
+
layout.resizeViewport(window.innerWidth, window.innerHeight, panelTop);
|
|
36
|
+
}
|
|
37
|
+
onMount(() => {
|
|
38
|
+
resize();
|
|
39
|
+
try {
|
|
40
|
+
layout.initialize(window.localStorage);
|
|
41
|
+
} catch {
|
|
42
|
+
layout.initialize();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
$effect(() => {
|
|
46
|
+
const top = panelTop;
|
|
47
|
+
const bottom = bottomReserve;
|
|
48
|
+
untrack(() => {
|
|
49
|
+
layout.bottom = bottom;
|
|
50
|
+
layout.resizeViewport(window.innerWidth, window.innerHeight, top);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
$effect(() => {
|
|
54
|
+
app.sheetInset = layout.compact && drawer ? drawerHeight + bottomReserve : 0;
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<svelte:window
|
|
59
|
+
onresize={resize}
|
|
60
|
+
onpointermove={(event) => layout.moveGesture(event.clientX, event.clientY)}
|
|
61
|
+
onpointerup={() => layout.finishGesture()}
|
|
62
|
+
onpointercancel={() => layout.finishGesture()}
|
|
63
|
+
/>
|
|
64
|
+
{#if showToolbar}
|
|
65
|
+
<div class="panel-toolbar" style:top="{toolbarTop}px" aria-label="Panels">
|
|
66
|
+
<div class="panel-buttons">
|
|
67
|
+
{#each layout.ordered().filter((p) => p.side === 'left') as panel (panel.id)}
|
|
68
|
+
<button
|
|
69
|
+
aria-label={panel.title}
|
|
70
|
+
aria-pressed={layout.compact ? layout.drawer === panel.id : panel.open}
|
|
71
|
+
onclick={() => layout.toggle(panel)}>{panel.title}</button
|
|
72
|
+
>
|
|
73
|
+
{/each}
|
|
74
|
+
</div>
|
|
75
|
+
<div class="panel-buttons right">
|
|
76
|
+
{#each layout.ordered().filter((p) => p.side === 'right') as panel (panel.id)}
|
|
77
|
+
<button
|
|
78
|
+
aria-label={panel.title}
|
|
79
|
+
aria-pressed={layout.compact ? layout.drawer === panel.id : panel.open}
|
|
80
|
+
onclick={() => layout.toggle(panel)}>{panel.title}</button
|
|
81
|
+
>
|
|
82
|
+
{/each}
|
|
83
|
+
<button
|
|
84
|
+
class="reset-layout"
|
|
85
|
+
onclick={() => layout.reset()}
|
|
86
|
+
title="Restore the default panel positions">Reset layout</button
|
|
87
|
+
>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
91
|
+
{#if layout.storageError}<p class="layout-error" role="status" style:top="{panelTop}px">
|
|
92
|
+
{layout.storageError}
|
|
93
|
+
</p>{/if}
|
|
94
|
+
<!-- The docks and the drawer take focus (tabindex -1) when a closed panel has
|
|
95
|
+
no toolbar button or opener to hand it back to. The drawer stays rendered,
|
|
96
|
+
at zero height, while no panel is open in it. -->
|
|
97
|
+
{#if layout.compact}
|
|
98
|
+
<div
|
|
99
|
+
class="drawer"
|
|
100
|
+
data-panel-drawer
|
|
101
|
+
role="region"
|
|
102
|
+
aria-label="Panels"
|
|
103
|
+
tabindex="-1"
|
|
104
|
+
style:bottom="{bottomReserve}px"
|
|
105
|
+
style:height="{drawer ? drawerHeight : 0}px"
|
|
106
|
+
>
|
|
107
|
+
{#if drawer}<PanelCard panel={drawer} mobile />{/if}
|
|
108
|
+
</div>
|
|
109
|
+
{:else}
|
|
110
|
+
<div
|
|
111
|
+
class="dock left"
|
|
112
|
+
style:bottom="max(var(--tellegen-dock-bottom, 100px), {bottomReserve}px)"
|
|
113
|
+
data-panel-dock="left"
|
|
114
|
+
role="region"
|
|
115
|
+
aria-label="Left panels"
|
|
116
|
+
tabindex="-1"
|
|
117
|
+
style:top="{panelTop}px"
|
|
118
|
+
style:width="{layout.dockWidth('left')}px"
|
|
119
|
+
>
|
|
120
|
+
{#each left as panel (panel.id)}<PanelCard {panel} />{/each}
|
|
121
|
+
</div>
|
|
122
|
+
<div
|
|
123
|
+
class="dock right"
|
|
124
|
+
style:bottom="max(var(--tellegen-dock-bottom, 100px), {bottomReserve}px)"
|
|
125
|
+
data-panel-dock="right"
|
|
126
|
+
role="region"
|
|
127
|
+
aria-label="Right panels"
|
|
128
|
+
tabindex="-1"
|
|
129
|
+
style:top="{panelTop}px"
|
|
130
|
+
style:width="{layout.dockWidth('right')}px"
|
|
131
|
+
>
|
|
132
|
+
{#each right as panel (panel.id)}<PanelCard {panel} />{/each}
|
|
133
|
+
</div>
|
|
134
|
+
{#each floating as panel (panel.id)}<PanelCard {panel} floating />{/each}
|
|
135
|
+
{/if}
|
|
136
|
+
|
|
137
|
+
<style>
|
|
138
|
+
.panel-toolbar {
|
|
139
|
+
position: fixed;
|
|
140
|
+
z-index: 30;
|
|
141
|
+
left: var(--tellegen-dock-left, 20px);
|
|
142
|
+
right: var(--tellegen-dock-right, 20px);
|
|
143
|
+
display: flex;
|
|
144
|
+
justify-content: space-between;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 12px;
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
height: 34px;
|
|
149
|
+
}
|
|
150
|
+
.panel-buttons {
|
|
151
|
+
display: flex;
|
|
152
|
+
gap: 6px;
|
|
153
|
+
align-items: center;
|
|
154
|
+
pointer-events: auto;
|
|
155
|
+
}
|
|
156
|
+
button {
|
|
157
|
+
border: 1px solid var(--line);
|
|
158
|
+
border-radius: 4px;
|
|
159
|
+
background: var(--paper);
|
|
160
|
+
color: var(--ink);
|
|
161
|
+
font: 12px/1.4 var(--font-display);
|
|
162
|
+
min-height: 32px;
|
|
163
|
+
padding: 6px 12px;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
box-shadow: var(--elev-1);
|
|
166
|
+
}
|
|
167
|
+
button[aria-pressed='true'] {
|
|
168
|
+
border-color: var(--accent);
|
|
169
|
+
background: var(--accent-soft);
|
|
170
|
+
}
|
|
171
|
+
button:focus-visible {
|
|
172
|
+
outline: 2px solid var(--focus-ring);
|
|
173
|
+
outline-offset: 2px;
|
|
174
|
+
}
|
|
175
|
+
button:hover {
|
|
176
|
+
border-color: var(--accent);
|
|
177
|
+
}
|
|
178
|
+
.reset-layout {
|
|
179
|
+
color: var(--text-secondary);
|
|
180
|
+
font-size: 11px;
|
|
181
|
+
padding: 6px 8px;
|
|
182
|
+
background: var(--panel);
|
|
183
|
+
}
|
|
184
|
+
.dock {
|
|
185
|
+
position: fixed;
|
|
186
|
+
z-index: 25;
|
|
187
|
+
bottom: var(--tellegen-dock-bottom, 100px);
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
align-items: stretch;
|
|
191
|
+
gap: 12px;
|
|
192
|
+
pointer-events: none;
|
|
193
|
+
}
|
|
194
|
+
.dock.left {
|
|
195
|
+
left: var(--tellegen-dock-left, 20px);
|
|
196
|
+
}
|
|
197
|
+
.dock.right {
|
|
198
|
+
right: var(--tellegen-dock-right, 20px);
|
|
199
|
+
}
|
|
200
|
+
.drawer {
|
|
201
|
+
position: fixed;
|
|
202
|
+
bottom: 88px;
|
|
203
|
+
left: 12px;
|
|
204
|
+
right: 12px;
|
|
205
|
+
z-index: 25;
|
|
206
|
+
}
|
|
207
|
+
.layout-error {
|
|
208
|
+
position: fixed;
|
|
209
|
+
left: 50%;
|
|
210
|
+
transform: translateX(-50%);
|
|
211
|
+
z-index: 42;
|
|
212
|
+
max-width: 400px;
|
|
213
|
+
margin: 0;
|
|
214
|
+
padding: 8px 12px;
|
|
215
|
+
font: 12px/1.5 var(--font-display);
|
|
216
|
+
color: var(--ink);
|
|
217
|
+
background: var(--paper);
|
|
218
|
+
border: 1px solid var(--line);
|
|
219
|
+
}
|
|
220
|
+
@media (max-width: 960px) {
|
|
221
|
+
.panel-toolbar {
|
|
222
|
+
left: var(--tellegen-dock-left, 12px);
|
|
223
|
+
right: var(--tellegen-dock-right, 12px);
|
|
224
|
+
gap: 6px;
|
|
225
|
+
}
|
|
226
|
+
.panel-buttons {
|
|
227
|
+
gap: 4px;
|
|
228
|
+
}
|
|
229
|
+
button {
|
|
230
|
+
padding: 6px 8px;
|
|
231
|
+
font-size: 11px;
|
|
232
|
+
}
|
|
233
|
+
.reset-layout {
|
|
234
|
+
display: none;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
</style>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getController } from '../context.svelte.js';
|
|
3
|
+
import { caseDeltas } from '../display.js';
|
|
4
|
+
import { fmt } from '../format.js';
|
|
5
|
+
|
|
6
|
+
const ctrl = getController();
|
|
7
|
+
const active = $derived(ctrl.activeSolvable);
|
|
8
|
+
const bus = $derived(ctrl.selectedBusData);
|
|
9
|
+
const branch = $derived(ctrl.selectedBranchData);
|
|
10
|
+
const voltage = $derived(active?.solution?.vm?.find((value) => value.bus === bus?.id)?.value);
|
|
11
|
+
const angle = $derived(active?.solution?.va.find((value) => value.bus === bus?.id)?.value);
|
|
12
|
+
const flow = $derived(active?.solution?.flows.find((value) => value.branch === branch?.id));
|
|
13
|
+
const demand = $derived.by(() => {
|
|
14
|
+
if (!bus || !active) return null;
|
|
15
|
+
const deltas = caseDeltas(active);
|
|
16
|
+
return bus.demand_mw + (deltas[bus.uid ?? bus.id] ?? deltas[bus.id] ?? 0);
|
|
17
|
+
});
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
{#if bus || branch}
|
|
21
|
+
<section class="equipment" aria-label="Power-flow equipment results">
|
|
22
|
+
{#if bus}
|
|
23
|
+
<h3>
|
|
24
|
+
Bus {bus.id}{#if bus.name}<span>{bus.name}</span>{/if}
|
|
25
|
+
</h3>
|
|
26
|
+
<dl>
|
|
27
|
+
{#if demand !== null}<div>
|
|
28
|
+
<dt>Demand</dt>
|
|
29
|
+
<dd>{fmt.format(demand)} MW</dd>
|
|
30
|
+
</div>{/if}
|
|
31
|
+
{#if voltage != null}<div>
|
|
32
|
+
<dt>Voltage</dt>
|
|
33
|
+
<dd>{voltage.toFixed(4)} pu</dd>
|
|
34
|
+
</div>{/if}
|
|
35
|
+
{#if angle != null}<div>
|
|
36
|
+
<dt>Voltage angle</dt>
|
|
37
|
+
<dd>{angle.toFixed(4)} rad</dd>
|
|
38
|
+
</div>{/if}
|
|
39
|
+
</dl>
|
|
40
|
+
{:else if branch}
|
|
41
|
+
<h3>Line {branch.id}<span>Bus {branch.from} to bus {branch.to}</span></h3>
|
|
42
|
+
<dl>
|
|
43
|
+
<div>
|
|
44
|
+
<dt>Capacity</dt>
|
|
45
|
+
<dd>{branch.rate_mw > 0 ? `${fmt.format(branch.rate_mw)} MVA` : 'Unlimited'}</dd>
|
|
46
|
+
</div>
|
|
47
|
+
{#if flow}
|
|
48
|
+
<div>
|
|
49
|
+
<dt>Active power</dt>
|
|
50
|
+
<dd>{fmt.format(flow.mw)} MW</dd>
|
|
51
|
+
</div>
|
|
52
|
+
{#if branch.rate_mw > 0}<div>
|
|
53
|
+
<dt>Loading</dt>
|
|
54
|
+
<dd>{fmt.format(flow.loading * 100)}%</dd>
|
|
55
|
+
</div>{/if}
|
|
56
|
+
{/if}
|
|
57
|
+
</dl>
|
|
58
|
+
{/if}
|
|
59
|
+
</section>
|
|
60
|
+
{/if}
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.equipment {
|
|
64
|
+
border-top: 1px solid var(--line);
|
|
65
|
+
margin-top: 14px;
|
|
66
|
+
padding-top: 14px;
|
|
67
|
+
}
|
|
68
|
+
h3 {
|
|
69
|
+
margin: 0 0 12px;
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
}
|
|
73
|
+
h3 span {
|
|
74
|
+
display: block;
|
|
75
|
+
margin-top: 3px;
|
|
76
|
+
font-size: 13px;
|
|
77
|
+
font-weight: 400;
|
|
78
|
+
color: var(--text-secondary);
|
|
79
|
+
}
|
|
80
|
+
dl {
|
|
81
|
+
margin: 0;
|
|
82
|
+
display: grid;
|
|
83
|
+
gap: 6px;
|
|
84
|
+
font: 12px/1.5 var(--font-mono);
|
|
85
|
+
}
|
|
86
|
+
dl > div {
|
|
87
|
+
display: flex;
|
|
88
|
+
justify-content: space-between;
|
|
89
|
+
align-items: baseline;
|
|
90
|
+
gap: 12px;
|
|
91
|
+
}
|
|
92
|
+
dt {
|
|
93
|
+
color: var(--text-secondary);
|
|
94
|
+
}
|
|
95
|
+
dd {
|
|
96
|
+
margin: 0;
|
|
97
|
+
text-align: right;
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getController } from '../context.svelte.js';
|
|
3
3
|
import { fmt, signed } from '../format.js';
|
|
4
|
+
import { sliderKeyValue } from '../slider.js';
|
|
4
5
|
|
|
5
6
|
const ctrl = getController();
|
|
6
7
|
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
'The black tick marks the last committed rating change. Drag the knob to preview a new thermal rating; release to solve and move the tick to that point.';
|
|
26
27
|
const disabledTip = "this line's rating is synthesized; not adjustable";
|
|
27
28
|
const scoreTip =
|
|
28
|
-
'Gradient
|
|
29
|
+
'Gradient estimates the declared objective change versus base before the solve finishes, from the engine preview at the committed point. Exact is the resolved OPF objective change.';
|
|
29
30
|
</script>
|
|
30
31
|
|
|
31
32
|
{#if ctrl.activeSolvable}
|
|
@@ -39,9 +40,7 @@
|
|
|
39
40
|
<div class="line-row mono">
|
|
40
41
|
{#if branch}
|
|
41
42
|
<span class="dim"
|
|
42
|
-
>line {branch.from} – {branch.to}
|
|
43
|
-
branch.rate_mw
|
|
44
|
-
)} MW</span
|
|
43
|
+
>line {branch.from} – {branch.to}, {fmt.format(branch.rate_mw)} MW</span
|
|
45
44
|
>
|
|
46
45
|
{/if}
|
|
47
46
|
<span class="dim">
|
|
@@ -66,7 +65,15 @@
|
|
|
66
65
|
bind:value={ctrl.ratingSliderCurrent, ctrl.setRatingPreview}
|
|
67
66
|
aria-label="rating delta at selected line"
|
|
68
67
|
onpointerdown={() => ctrl.setRatingPreview(ctrl.ratingSliderValue)}
|
|
69
|
-
onkeydown={() =>
|
|
68
|
+
onkeydown={(e) => {
|
|
69
|
+
const next = sliderKeyValue(e.key, ctrl.ratingSliderValue, bounds.min, bounds.max);
|
|
70
|
+
if (next === null) {
|
|
71
|
+
ctrl.setRatingPreview(ctrl.ratingSliderValue);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
ctrl.setRatingPreview(next);
|
|
76
|
+
}}
|
|
70
77
|
onpointerup={(e) => ctrl.finishRatingInput(Number(e.currentTarget.value))}
|
|
71
78
|
onkeyup={(e) => ctrl.finishRatingInput(Number(e.currentTarget.value))}
|
|
72
79
|
onchange={(e) => ctrl.finishRatingInput(Number(e.currentTarget.value))}
|
|
@@ -75,7 +82,7 @@
|
|
|
75
82
|
<div class="rating-feedback" class:idle={!ctrl.previewing && !ctrl.isPerturbed(c)}>
|
|
76
83
|
<p class="pred mono dim" aria-hidden={!(ctrl.predictedDeltaObj !== null && ctrl.previewing)}>
|
|
77
84
|
{#if ctrl.predictedDeltaObj !== null && ctrl.previewing}
|
|
78
|
-
predicted Δ
|
|
85
|
+
predicted Δobjective {signed(ctrl.predictedDeltaObj)}
|
|
79
86
|
{:else}
|
|
80
87
|
|
|
81
88
|
{/if}
|
|
@@ -83,10 +90,8 @@
|
|
|
83
90
|
<p class="score mono" aria-hidden={!(ctrl.gradientScore && ctrl.isPerturbed(c))}>
|
|
84
91
|
{#if ctrl.gradientScore && ctrl.isPerturbed(c)}
|
|
85
92
|
<span title={scoreTip}>
|
|
86
|
-
gradient {signed(ctrl.gradientScore.pred)}
|
|
87
|
-
|
|
88
|
-
)}
|
|
89
|
-
$/h
|
|
93
|
+
gradient {signed(ctrl.gradientScore.pred)}, exact {signed(ctrl.gradientScore.exact)} objective
|
|
94
|
+
units
|
|
90
95
|
</span>
|
|
91
96
|
{:else}
|
|
92
97
|
|
|
@@ -109,7 +114,7 @@
|
|
|
109
114
|
.slider-head {
|
|
110
115
|
display: flex;
|
|
111
116
|
justify-content: space-between;
|
|
112
|
-
font-size:
|
|
117
|
+
font-size: 13.5px;
|
|
113
118
|
color: var(--text-secondary);
|
|
114
119
|
margin-bottom: 4px;
|
|
115
120
|
}
|
|
@@ -124,7 +129,7 @@
|
|
|
124
129
|
justify-content: space-between;
|
|
125
130
|
gap: 8px;
|
|
126
131
|
margin: 6px 0 7px;
|
|
127
|
-
font-size:
|
|
132
|
+
font-size: 12.5px;
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
.slider-track {
|
|
@@ -240,13 +245,13 @@
|
|
|
240
245
|
|
|
241
246
|
.pred {
|
|
242
247
|
margin: 2px 0 0;
|
|
243
|
-
font-size:
|
|
248
|
+
font-size: 13px;
|
|
244
249
|
min-height: 16px;
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
.score {
|
|
248
253
|
margin: 8px 0 0;
|
|
249
|
-
font-size:
|
|
254
|
+
font-size: 13px;
|
|
250
255
|
color: var(--ink);
|
|
251
256
|
min-height: 16px;
|
|
252
257
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getAppState } from '../context.svelte.js';
|
|
3
|
+
import { fmt, formulationLabel, signed } from '../format.js';
|
|
4
|
+
import BusPicker from './BusPicker.svelte';
|
|
5
|
+
import ModelDetails from './ModelDetails.svelte';
|
|
6
|
+
|
|
7
|
+
const app = getAppState();
|
|
8
|
+
const saved = $derived(app.studyView);
|
|
9
|
+
const bus = $derived(saved?.network.buses.find((value) => value.id === app.selectedBus));
|
|
10
|
+
const branch = $derived(saved?.network.branches.find((value) => value.id === app.selectedBranch));
|
|
11
|
+
const lmp = $derived(saved?.solution?.lmp?.find((value) => value.bus === bus?.id)?.value);
|
|
12
|
+
const voltage = $derived(saved?.solution?.vm?.find((value) => value.bus === bus?.id)?.value);
|
|
13
|
+
const squaredVoltage = $derived(
|
|
14
|
+
saved?.solution?.w?.find((value) => value.bus === bus?.id)?.value
|
|
15
|
+
);
|
|
16
|
+
const angle = $derived(saved?.solution?.va?.find((value) => value.bus === bus?.id)?.value);
|
|
17
|
+
const flow = $derived(saved?.solution?.flows?.find((value) => value.branch === branch?.id));
|
|
18
|
+
const baseDemand = $derived(bus ? saved?.baseDemandMw?.[String(bus.id)] : undefined);
|
|
19
|
+
function live() {
|
|
20
|
+
app.studyView = null;
|
|
21
|
+
app.selectedBus = null;
|
|
22
|
+
app.selectedBranch = null;
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
{#if saved}
|
|
27
|
+
<section aria-label="Saved network details">
|
|
28
|
+
<h2>{saved.network.name}</h2>
|
|
29
|
+
<p class="state-label">{saved.label}<span>Saved state</span></p>
|
|
30
|
+
<dl>
|
|
31
|
+
<div>
|
|
32
|
+
<dt>Buses</dt>
|
|
33
|
+
<dd>{saved.network.buses.length.toLocaleString()}</dd>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<dt>Lines</dt>
|
|
37
|
+
<dd>{saved.network.branches.length.toLocaleString()}</dd>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<dt>Calculation</dt>
|
|
41
|
+
<dd>{formulationLabel(saved.formulation)}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<dt>Result</dt>
|
|
45
|
+
<dd>{saved.solution?.status ?? 'Not solved'}</dd>
|
|
46
|
+
</div>
|
|
47
|
+
{#if saved.solution?.objective != null}
|
|
48
|
+
<div>
|
|
49
|
+
<dt>OPF objective</dt>
|
|
50
|
+
<dd>{fmt.format(saved.solution.objective)}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
{/if}
|
|
53
|
+
</dl>
|
|
54
|
+
<button class="live" onclick={live}>Return to live case</button>
|
|
55
|
+
<ModelDetails details={saved.network.model_details} />
|
|
56
|
+
<BusPicker inline />
|
|
57
|
+
{#if saved.solution}
|
|
58
|
+
<label class="map-values"
|
|
59
|
+
>{saved.network.coordinate_space === 'diagram' ? 'Diagram values' : 'Map values'}
|
|
60
|
+
<select bind:value={app.displayMode}>
|
|
61
|
+
{#if saved.solution.lmp?.length}<option value="price">LMP</option>{/if}
|
|
62
|
+
{#if saved.solution.va?.length}<option value="angle">Voltage angle</option>{/if}
|
|
63
|
+
{#if saved.solution.vm?.length || saved.solution.w?.length}<option value="voltage"
|
|
64
|
+
>Voltage magnitude</option
|
|
65
|
+
>{/if}
|
|
66
|
+
</select>
|
|
67
|
+
</label>
|
|
68
|
+
{/if}
|
|
69
|
+
{#if bus}
|
|
70
|
+
<div class="equipment">
|
|
71
|
+
<h3>
|
|
72
|
+
Bus {bus.id}{#if bus.name}<span>{bus.name}</span>{/if}
|
|
73
|
+
</h3>
|
|
74
|
+
<dl>
|
|
75
|
+
{#if baseDemand != null}<div>
|
|
76
|
+
<dt>Base demand</dt>
|
|
77
|
+
<dd>{fmt.format(baseDemand)} MW</dd>
|
|
78
|
+
</div>{/if}
|
|
79
|
+
<div>
|
|
80
|
+
<dt>Current demand</dt>
|
|
81
|
+
<dd>{fmt.format(bus.demand_mw)} MW</dd>
|
|
82
|
+
</div>
|
|
83
|
+
{#if baseDemand != null}<div>
|
|
84
|
+
<dt>Change</dt>
|
|
85
|
+
<dd>{signed(bus.demand_mw - baseDemand)} MW</dd>
|
|
86
|
+
</div>{/if}
|
|
87
|
+
{#if lmp != null}<div>
|
|
88
|
+
<dt>LMP</dt>
|
|
89
|
+
<dd>{fmt.format(lmp)}<small>objective units/MW</small></dd>
|
|
90
|
+
</div>{/if}
|
|
91
|
+
{#if voltage != null || squaredVoltage != null}<div>
|
|
92
|
+
<dt>Voltage</dt>
|
|
93
|
+
<dd>{(voltage ?? Math.sqrt(Math.max(0, squaredVoltage!))).toFixed(4)} pu</dd>
|
|
94
|
+
</div>{/if}
|
|
95
|
+
{#if angle != null}<div>
|
|
96
|
+
<dt>Voltage angle</dt>
|
|
97
|
+
<dd>{angle.toFixed(4)} rad</dd>
|
|
98
|
+
</div>{/if}
|
|
99
|
+
</dl>
|
|
100
|
+
</div>
|
|
101
|
+
{:else if branch}
|
|
102
|
+
<div class="equipment">
|
|
103
|
+
<h3>Line {branch.id}<span>Bus {branch.from} to bus {branch.to}</span></h3>
|
|
104
|
+
<dl>
|
|
105
|
+
<div>
|
|
106
|
+
<dt>Capacity</dt>
|
|
107
|
+
<dd>
|
|
108
|
+
{branch.rate_mw > 0
|
|
109
|
+
? `${fmt.format(branch.rate_mw)} ${saved.formulation === 'dcopf' ? 'MW' : 'MVA'}`
|
|
110
|
+
: 'Unlimited'}
|
|
111
|
+
</dd>
|
|
112
|
+
</div>
|
|
113
|
+
{#if flow}<div>
|
|
114
|
+
<dt>Active power</dt>
|
|
115
|
+
<dd>{fmt.format(flow.pf)} MW</dd>
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<dt>Loading</dt>
|
|
119
|
+
<dd>{fmt.format(flow.loading * 100)}%</dd>
|
|
120
|
+
</div>{/if}
|
|
121
|
+
</dl>
|
|
122
|
+
</div>
|
|
123
|
+
{/if}
|
|
124
|
+
</section>
|
|
125
|
+
{/if}
|
|
126
|
+
|
|
127
|
+
<style>
|
|
128
|
+
h2 {
|
|
129
|
+
margin: 0 0 8px;
|
|
130
|
+
font-size: 15px;
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
}
|
|
133
|
+
.state-label {
|
|
134
|
+
display: flex;
|
|
135
|
+
justify-content: space-between;
|
|
136
|
+
gap: 12px;
|
|
137
|
+
margin: 0 0 16px;
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
}
|
|
140
|
+
.state-label span {
|
|
141
|
+
flex: none;
|
|
142
|
+
color: var(--text-secondary);
|
|
143
|
+
font-size: 13px;
|
|
144
|
+
}
|
|
145
|
+
dl {
|
|
146
|
+
margin: 0;
|
|
147
|
+
display: grid;
|
|
148
|
+
gap: 6px;
|
|
149
|
+
font: 12px/1.5 var(--font-mono);
|
|
150
|
+
}
|
|
151
|
+
dl > div {
|
|
152
|
+
display: flex;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
align-items: baseline;
|
|
155
|
+
gap: 12px;
|
|
156
|
+
}
|
|
157
|
+
dt {
|
|
158
|
+
color: var(--text-secondary);
|
|
159
|
+
}
|
|
160
|
+
dd {
|
|
161
|
+
margin: 0;
|
|
162
|
+
text-align: right;
|
|
163
|
+
}
|
|
164
|
+
small {
|
|
165
|
+
display: block;
|
|
166
|
+
font-size: 12px;
|
|
167
|
+
color: var(--text-secondary);
|
|
168
|
+
}
|
|
169
|
+
.live {
|
|
170
|
+
margin: 16px 0;
|
|
171
|
+
}
|
|
172
|
+
button,
|
|
173
|
+
select {
|
|
174
|
+
border: 1px solid var(--line);
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
color: var(--ink);
|
|
177
|
+
background: var(--paper);
|
|
178
|
+
padding: 6px 8px;
|
|
179
|
+
font: 12px/1.4 var(--font-display);
|
|
180
|
+
}
|
|
181
|
+
button {
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
}
|
|
184
|
+
button:focus-visible,
|
|
185
|
+
select:focus-visible {
|
|
186
|
+
outline: 2px solid var(--focus-ring);
|
|
187
|
+
outline-offset: 2px;
|
|
188
|
+
}
|
|
189
|
+
.map-values {
|
|
190
|
+
display: flex;
|
|
191
|
+
justify-content: space-between;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 12px;
|
|
194
|
+
margin: 16px 0;
|
|
195
|
+
font-size: 12px;
|
|
196
|
+
}
|
|
197
|
+
.equipment {
|
|
198
|
+
border-top: 1px solid var(--line);
|
|
199
|
+
margin-top: 16px;
|
|
200
|
+
padding-top: 16px;
|
|
201
|
+
}
|
|
202
|
+
h3 {
|
|
203
|
+
margin: 0 0 12px;
|
|
204
|
+
font-size: 13px;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
}
|
|
207
|
+
h3 span {
|
|
208
|
+
display: block;
|
|
209
|
+
margin-top: 3px;
|
|
210
|
+
color: var(--text-secondary);
|
|
211
|
+
font-size: 13px;
|
|
212
|
+
font-weight: 400;
|
|
213
|
+
}
|
|
214
|
+
</style>
|