@tellegen/svelte 0.4.0 → 0.4.2
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 +38 -10
- package/dist/Sparkline.svelte +4 -4
- package/dist/TellegenMap.svelte +80 -66
- package/dist/TellegenProvider.svelte +29 -13
- package/dist/colors.d.ts +37 -1
- package/dist/colors.js +63 -0
- package/dist/components/AppFooter.svelte +4 -4
- package/dist/components/AppHeader.svelte +15 -19
- package/dist/components/BindingLines.svelte +10 -9
- package/dist/components/BusPicker.svelte +5 -6
- package/dist/components/ControlPanel.svelte +16 -11
- package/dist/components/ControlPanel.svelte.d.ts +6 -1
- package/dist/components/DemandSlider.svelte +26 -29
- package/dist/components/DiagramCanvas.svelte +16 -4
- package/dist/components/DisplayControls.svelte +5 -24
- package/dist/components/DropZone.svelte +7 -3
- package/dist/components/DropZone.svelte.d.ts +6 -17
- package/dist/components/FormulationSelector.svelte +2 -7
- package/dist/components/LocalCaseDetails.svelte +126 -126
- package/dist/components/LocalCaseDetails.svelte.d.ts +6 -1
- package/dist/components/MulticonductorDetails.svelte +48 -38
- package/dist/components/NetworkStats.svelte +14 -16
- package/dist/components/PanelCard.svelte +1 -1
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/RatingSlider.svelte +22 -23
- package/dist/components/RestoreDefaultsButton.svelte +2 -3
- package/dist/components/SensitivityReadout.svelte +9 -27
- package/dist/components/SolveCard.svelte +69 -35
- package/dist/components/TopMovers.svelte +5 -5
- package/dist/context.svelte.d.ts +6 -2
- package/dist/context.svelte.js +4 -2
- package/dist/controller.svelte.d.ts +7 -0
- package/dist/controller.svelte.js +37 -8
- package/dist/display.d.ts +3 -2
- package/dist/display.js +28 -11
- package/dist/format.d.ts +1 -9
- package/dist/format.js +1 -14
- package/dist/index.d.ts +1 -1
- package/dist/map.d.ts +1 -1
- package/dist/map.js +1 -1
- package/dist/state.svelte.d.ts +6 -1
- package/dist/state.svelte.js +1 -0
- package/dist/styles.css +3 -0
- package/dist/types.d.ts +1 -1
- package/package.json +10 -6
package/dist/colors.js
CHANGED
|
@@ -31,6 +31,24 @@ const PRICE_STOPS = [
|
|
|
31
31
|
];
|
|
32
32
|
export const priceColor = (t) => ramp(PRICE_STOPS, t);
|
|
33
33
|
export const priceGradient = cssGradient(PRICE_STOPS);
|
|
34
|
+
/** Sequential demand ramp: the upper three quarters of viridis, slate blue
|
|
35
|
+
* through teal and green to yellow. Lightness rises monotonically, the dimmest
|
|
36
|
+
* stop still reads on a dark basemap, and the cool hues cannot be mistaken for
|
|
37
|
+
* the warm nodal value ramp. */
|
|
38
|
+
const DEMAND_STOPS = [
|
|
39
|
+
[59, 82, 139, 235],
|
|
40
|
+
[44, 114, 142, 235],
|
|
41
|
+
[33, 145, 140, 240],
|
|
42
|
+
[94, 201, 98, 245],
|
|
43
|
+
[253, 231, 37, 250]
|
|
44
|
+
];
|
|
45
|
+
/** Demand colour for a fraction `t` of the demand domain, on a square-root
|
|
46
|
+
* scale: bus demand is heavy tailed (a few large cities, many small towns),
|
|
47
|
+
* and a linear scale would paint everything but the largest buses one hue. */
|
|
48
|
+
export const demandColor = (t) => ramp(DEMAND_STOPS, Math.sqrt(Math.max(0, t)));
|
|
49
|
+
/** The demand ramp as a CSS gradient whose stops sit where the square-root
|
|
50
|
+
* scale puts them, so a legend bar reads linearly from its low to high label. */
|
|
51
|
+
export const demandGradient = `linear-gradient(90deg, ${DEMAND_STOPS.map((s, i) => `rgb(${s[0]} ${s[1]} ${s[2]}) ${((i / (DEMAND_STOPS.length - 1)) ** 2 * 100).toFixed(2)}%`).join(', ')})`;
|
|
34
52
|
/** Trimmed nodal value color domain: 5th to 95th percentile, with a one-unit
|
|
35
53
|
* minimum span. Nodal values are heavy tailed when one binding line pins a
|
|
36
54
|
* handful of buses far from the pack) so min-max scaling compresses the pack
|
|
@@ -48,12 +66,25 @@ export function priceDomain(values) {
|
|
|
48
66
|
const span = Math.max(hi - lo, 1);
|
|
49
67
|
return { lo: mid - span / 2, hi: mid + span / 2 };
|
|
50
68
|
}
|
|
69
|
+
/** Demand colour domain: zero to the 98th percentile, at least 1 MW. Zero
|
|
70
|
+
* anchors the scale so a bus without load reads as none; the percentile keeps
|
|
71
|
+
* one metropolis from washing out every other bus, and the few above it clamp
|
|
72
|
+
* to the top of the ramp. */
|
|
73
|
+
export function demandDomain(values) {
|
|
74
|
+
if (values.length === 0)
|
|
75
|
+
return { lo: 0, hi: 1 };
|
|
76
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
77
|
+
const hi = sorted[Math.min(sorted.length - 1, Math.floor(0.98 * sorted.length))];
|
|
78
|
+
return { lo: 0, hi: Math.max(hi, 1) };
|
|
79
|
+
}
|
|
51
80
|
/** Center a scalar display variable (bus voltage angle, |V|) on a symmetric domain
|
|
52
81
|
* with a per-mode minimum span; nodal values keep their own robust quantile domain. Shared by
|
|
53
82
|
* the control panel readout and the map color scale. */
|
|
54
83
|
export function scalarDomain(mode, values) {
|
|
55
84
|
if (mode === 'price')
|
|
56
85
|
return priceDomain(values);
|
|
86
|
+
if (mode === 'demand')
|
|
87
|
+
return demandDomain(values);
|
|
57
88
|
if (values.length === 0)
|
|
58
89
|
return { lo: 0, hi: 1 };
|
|
59
90
|
const { min: rawLo, max: rawHi } = extent(values);
|
|
@@ -164,3 +195,35 @@ export function branchWidth(loading) {
|
|
|
164
195
|
export function busRadius(maxMw) {
|
|
165
196
|
return 3.2 + 0.45 * Math.sqrt(Math.max(maxMw, 1));
|
|
166
197
|
}
|
|
198
|
+
/** Radius in px of every bus marker under `uniform` sizing, and the most a
|
|
199
|
+
* uniform marker grows to on the map. */
|
|
200
|
+
export const UNIFORM_BUS_RADIUS = 4.5;
|
|
201
|
+
/** The least a uniform marker shrinks to on the map, in px. */
|
|
202
|
+
export const UNIFORM_BUS_MIN_RADIUS = 2;
|
|
203
|
+
/** The ground radius of a uniform marker on the map, in metres. */
|
|
204
|
+
export const UNIFORM_BUS_GROUND_RADIUS = 9000;
|
|
205
|
+
/** Bus radius in px under a sizing choice: `power` scales with max(load, gen)
|
|
206
|
+
* through `busRadius`, `uniform` draws every bus alike. */
|
|
207
|
+
export function busRadiusFor(sizing, maxMw) {
|
|
208
|
+
return sizing === 'uniform' ? UNIFORM_BUS_RADIUS : busRadius(maxMw);
|
|
209
|
+
}
|
|
210
|
+
/** Radius settings for the map's bus markers under a sizing choice. `power`
|
|
211
|
+
* sizes in pixels through `busRadius`. `uniform` gives every bus one ground
|
|
212
|
+
* radius, drawn between `UNIFORM_BUS_MIN_RADIUS` and `UNIFORM_BUS_RADIUS`
|
|
213
|
+
* pixels, so a whole country reads as a mesh of dots with its lines showing
|
|
214
|
+
* and a closer view as distinct buses. */
|
|
215
|
+
export function busMarkerRadius(sizing) {
|
|
216
|
+
return sizing === 'uniform'
|
|
217
|
+
? {
|
|
218
|
+
radius: () => UNIFORM_BUS_GROUND_RADIUS,
|
|
219
|
+
radiusUnits: 'meters',
|
|
220
|
+
radiusMinPixels: UNIFORM_BUS_MIN_RADIUS,
|
|
221
|
+
radiusMaxPixels: UNIFORM_BUS_RADIUS
|
|
222
|
+
}
|
|
223
|
+
: {
|
|
224
|
+
radius: busRadius,
|
|
225
|
+
radiusUnits: 'pixels',
|
|
226
|
+
radiusMinPixels: 0,
|
|
227
|
+
radiusMaxPixels: Number.MAX_SAFE_INTEGER
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
let { inline = false }: { inline?: boolean } = $props();
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
|
-
<footer class
|
|
11
|
-
<a href="/credits/">
|
|
10
|
+
<footer class:inline>
|
|
11
|
+
<a href="/credits/">Credits</a>
|
|
12
12
|
<i class="sep"></i>
|
|
13
|
-
<a href="/privacy/">
|
|
13
|
+
<a href="/privacy/">Privacy</a>
|
|
14
14
|
{#if ctrl.showFileDropUi}
|
|
15
15
|
<i class="sep filedrop-ui"></i>
|
|
16
16
|
<span class="drophint filedrop-ui"
|
|
17
|
-
><span class="arrow">⇣</span>
|
|
17
|
+
><span class="arrow">⇣</span> Drop a case or geographic file anywhere</span
|
|
18
18
|
>
|
|
19
19
|
{/if}
|
|
20
20
|
</footer>
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</svg>
|
|
34
34
|
<h1>tellegen</h1>
|
|
35
35
|
</div>
|
|
36
|
-
<nav class="cases" aria-label="
|
|
36
|
+
<nav class="cases" aria-label="Networks">
|
|
37
37
|
{#each app.cases as c (c.id)}
|
|
38
38
|
{@const [cname, cregion] = splitName(c.name)}
|
|
39
39
|
<div class="case-chip" class:active={app.activeCaseId === c.id}>
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
onclick={() => ctrl.activateCase(c.id)}
|
|
45
45
|
>
|
|
46
46
|
<span class="cname"
|
|
47
|
-
>{cname}{#if c.perturbed}<i class="mark" title="
|
|
47
|
+
>{cname}{#if c.perturbed}<i class="mark" title="Demand changed"></i>{/if}</span
|
|
48
48
|
>
|
|
49
49
|
<span class="cregion mono">{c.unavailableReason ? 'Unavailable' : cregion}</span>
|
|
50
50
|
</button>
|
|
51
51
|
<button
|
|
52
52
|
class="case-remove mono"
|
|
53
|
-
aria-label="
|
|
54
|
-
title="
|
|
53
|
+
aria-label="Remove {c.name}"
|
|
54
|
+
title="Remove {c.name}"
|
|
55
55
|
onclick={(e) => ctrl.removeBackendCase(c, e)}>✕</button
|
|
56
56
|
>
|
|
57
57
|
</div>
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
<div class="case-chip local" class:active={app.activeLocalId === c.id}>
|
|
61
61
|
<button class="case-activate" onclick={() => ctrl.activateLocal(c)}>
|
|
62
62
|
<span class="cname">{c.label}</span>
|
|
63
|
-
<span class="cregion mono">
|
|
63
|
+
<span class="cregion mono">Local</span>
|
|
64
64
|
</button>
|
|
65
65
|
<button
|
|
66
66
|
class="case-remove mono"
|
|
67
|
-
aria-label="
|
|
68
|
-
title="
|
|
67
|
+
aria-label="Remove {c.label}"
|
|
68
|
+
title="Remove {c.label}"
|
|
69
69
|
onclick={(e) => ctrl.removeLocalCase(c, e)}>✕</button
|
|
70
70
|
>
|
|
71
71
|
</div>
|
|
@@ -74,33 +74,29 @@
|
|
|
74
74
|
<div class="case-chip local" class:active={app.activeMultiId === c.id}>
|
|
75
75
|
<button class="case-activate" onclick={() => ctrl.activateMulti(c)}>
|
|
76
76
|
<span class="cname">{c.label}</span>
|
|
77
|
-
<span class="cregion mono">
|
|
77
|
+
<span class="cregion mono">Multiconductor</span>
|
|
78
78
|
</button>
|
|
79
79
|
<button
|
|
80
80
|
class="case-remove mono"
|
|
81
|
-
aria-label="
|
|
82
|
-
title="
|
|
81
|
+
aria-label="Remove {c.label}"
|
|
82
|
+
title="Remove {c.label}"
|
|
83
83
|
onclick={(e) => ctrl.removeMultiCase(c, e)}>✕</button
|
|
84
84
|
>
|
|
85
85
|
</div>
|
|
86
86
|
{/each}
|
|
87
|
-
<button
|
|
88
|
-
class="ghost filedrop-ui"
|
|
89
|
-
title="parsed in your browser; the file never uploads"
|
|
90
|
-
onclick={() => fileInput?.click()}
|
|
91
|
-
>
|
|
87
|
+
<button class="ghost filedrop-ui" onclick={() => fileInput?.click()}>
|
|
92
88
|
<span class="cname"
|
|
93
89
|
><span class="arrow">⇣</span>{ctrl.showFileDropUi
|
|
94
|
-
? '
|
|
95
|
-
: '
|
|
90
|
+
? 'Drop a case file'
|
|
91
|
+
: 'Open a case file'}</span
|
|
96
92
|
>
|
|
97
|
-
<span class="cregion mono">
|
|
93
|
+
<span class="cregion mono">Case or geo files</span>
|
|
98
94
|
</button>
|
|
99
95
|
</nav>
|
|
100
96
|
<span class="kicker mono">
|
|
101
97
|
<a href={config.orgHref} target="_blank" rel="noreferrer">{config.orgLabel}</a>
|
|
102
98
|
<i class="sep"></i>
|
|
103
|
-
<a href={config.docsHref} target="_blank" rel="noreferrer">
|
|
99
|
+
<a href={config.docsHref} target="_blank" rel="noreferrer">Docs</a>
|
|
104
100
|
</span>
|
|
105
101
|
<input
|
|
106
102
|
type="file"
|
|
@@ -30,22 +30,17 @@
|
|
|
30
30
|
|
|
31
31
|
{#if bindingLines.length > 0}
|
|
32
32
|
<div class="binding-lines">
|
|
33
|
-
<p
|
|
34
|
-
|
|
35
|
-
title="lines loaded to their thermal rating; select one to see LMP sensitivity to the rating"
|
|
36
|
-
>
|
|
37
|
-
binding lines
|
|
38
|
-
</p>
|
|
39
|
-
<ul class="mono">
|
|
33
|
+
<p class="dim head" title="Lines loaded to their thermal rating">Binding lines</p>
|
|
34
|
+
<ul>
|
|
40
35
|
{#each bindingLines as { branch, loading } (branch.id)}
|
|
41
36
|
<li>
|
|
42
37
|
<button
|
|
43
|
-
class="mono"
|
|
44
38
|
class:selected={app.selectedBranch === branch.id}
|
|
45
39
|
aria-pressed={app.selectedBranch === branch.id}
|
|
46
40
|
onclick={() => select(branch.id)}
|
|
47
41
|
>
|
|
48
|
-
|
|
42
|
+
Line <span class="mono">{branch.from} – {branch.to}</span>
|
|
43
|
+
<span class="mono load">{Math.round(loading * 100)}%</span>
|
|
49
44
|
</button>
|
|
50
45
|
</li>
|
|
51
46
|
{/each}
|
|
@@ -72,6 +67,8 @@
|
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
li button {
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: 4px;
|
|
75
72
|
width: 100%;
|
|
76
73
|
padding: 3px 6px 3px 8px;
|
|
77
74
|
text-align: left;
|
|
@@ -84,6 +81,10 @@
|
|
|
84
81
|
cursor: pointer;
|
|
85
82
|
}
|
|
86
83
|
|
|
84
|
+
.load {
|
|
85
|
+
margin-left: auto;
|
|
86
|
+
}
|
|
87
|
+
|
|
87
88
|
li button:hover {
|
|
88
89
|
background: var(--accent-soft);
|
|
89
90
|
}
|
|
@@ -89,14 +89,14 @@
|
|
|
89
89
|
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) open = false;
|
|
90
90
|
}}
|
|
91
91
|
>
|
|
92
|
-
<label class="
|
|
92
|
+
<label class="dim" for="bus-lookup-input">Bus lookup</label>
|
|
93
93
|
<input
|
|
94
94
|
id="bus-lookup-input"
|
|
95
95
|
class="mono"
|
|
96
96
|
type="text"
|
|
97
97
|
inputmode="numeric"
|
|
98
98
|
autocomplete="off"
|
|
99
|
-
placeholder="
|
|
99
|
+
placeholder="Bus ID"
|
|
100
100
|
role="combobox"
|
|
101
101
|
aria-keyshortcuts="/"
|
|
102
102
|
aria-expanded={open}
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
onkeydown={onKeydown}
|
|
115
115
|
/>
|
|
116
116
|
{#if open && results.length > 0}
|
|
117
|
-
<ul
|
|
117
|
+
<ul id="bus-lookup-listbox" role="listbox" aria-label="Matching buses">
|
|
118
118
|
{#each results as bus, i (bus.id)}
|
|
119
119
|
<li
|
|
120
120
|
id="bus-lookup-opt-{bus.id}"
|
|
@@ -126,12 +126,12 @@
|
|
|
126
126
|
commit(bus.id);
|
|
127
127
|
}}
|
|
128
128
|
>
|
|
129
|
-
|
|
129
|
+
Bus <span class="mono">{bus.id}</span>
|
|
130
130
|
</li>
|
|
131
131
|
{/each}
|
|
132
132
|
</ul>
|
|
133
133
|
{:else if open && query.trim() !== ''}
|
|
134
|
-
<p class="
|
|
134
|
+
<p class="dim empty">No matching bus</p>
|
|
135
135
|
{/if}
|
|
136
136
|
</div>
|
|
137
137
|
{/if}
|
|
@@ -156,7 +156,6 @@
|
|
|
156
156
|
label {
|
|
157
157
|
display: block;
|
|
158
158
|
font-size: 12px;
|
|
159
|
-
text-transform: uppercase;
|
|
160
159
|
margin-bottom: 3px;
|
|
161
160
|
}
|
|
162
161
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { tick, untrack } from 'svelte';
|
|
2
|
+
import { tick, untrack, type Snippet } from 'svelte';
|
|
3
3
|
import { getAppState, getController, getUiConfig } from '../context.svelte.js';
|
|
4
|
+
import type { LocalCase } from '../state.svelte.js';
|
|
4
5
|
import PanelFrame from './PanelFrame.svelte';
|
|
5
6
|
import SavedNetworkDetails from './SavedNetworkDetails.svelte';
|
|
6
7
|
import ModelDetails from './ModelDetails.svelte';
|
|
@@ -20,6 +21,9 @@
|
|
|
20
21
|
import SizeLegend from './SizeLegend.svelte';
|
|
21
22
|
import TopMovers from './TopMovers.svelte';
|
|
22
23
|
|
|
24
|
+
/** Optional host content rendered under a local case's stats. */
|
|
25
|
+
let { caseNote }: { caseNote?: Snippet<[LocalCase]> } = $props();
|
|
26
|
+
|
|
23
27
|
const app = getAppState();
|
|
24
28
|
const ctrl = getController();
|
|
25
29
|
const config = getUiConfig();
|
|
@@ -90,7 +94,7 @@
|
|
|
90
94
|
<SavedNetworkDetails />
|
|
91
95
|
{:else}
|
|
92
96
|
{#if app.parsingFile}
|
|
93
|
-
<p class="dim
|
|
97
|
+
<p class="dim blink">Parsing…</p>
|
|
94
98
|
{/if}
|
|
95
99
|
|
|
96
100
|
<!-- Selected equipment leads on narrow screens. -->
|
|
@@ -113,7 +117,7 @@
|
|
|
113
117
|
>
|
|
114
118
|
</div>
|
|
115
119
|
{/if}
|
|
116
|
-
<LocalCaseDetails />
|
|
120
|
+
<LocalCaseDetails note={caseNote} />
|
|
117
121
|
{/if}
|
|
118
122
|
{#if app.activeMulti}
|
|
119
123
|
<MulticonductorDetails />
|
|
@@ -121,18 +125,17 @@
|
|
|
121
125
|
{#if !ctrl.networkStats}
|
|
122
126
|
{#if !app.activeLocal && !app.activeMulti}
|
|
123
127
|
{#if ctrl.casesLoaded && app.cases.length === 0}
|
|
124
|
-
<p class="dim
|
|
125
|
-
{config.loadDefaultCases ? '
|
|
128
|
+
<p class="dim">
|
|
129
|
+
{config.loadDefaultCases ? 'No default cases loaded' : 'Drop a case file to begin'}
|
|
126
130
|
</p>
|
|
127
131
|
{#if config.loadDefaultCases}
|
|
128
|
-
<button class="reset
|
|
129
|
-
>restore default cases</button
|
|
132
|
+
<button class="reset" onclick={ctrl.restoreDefaultCases}>Restore default cases</button
|
|
130
133
|
>
|
|
131
134
|
{/if}
|
|
132
135
|
{:else if ctrl.loadingBackendCase}
|
|
133
|
-
<p class="dim
|
|
136
|
+
<p class="dim blink">Loading case…</p>
|
|
134
137
|
{:else}
|
|
135
|
-
<p class="dim
|
|
138
|
+
<p class="dim blink">Loading cases…</p>
|
|
136
139
|
{/if}
|
|
137
140
|
{/if}
|
|
138
141
|
{:else}
|
|
@@ -157,9 +160,11 @@
|
|
|
157
160
|
{@render selectionBlock()}
|
|
158
161
|
{/if}
|
|
159
162
|
|
|
160
|
-
|
|
163
|
+
{#if app.busSizing === 'power'}
|
|
164
|
+
<hr />
|
|
161
165
|
|
|
162
|
-
|
|
166
|
+
<SizeLegend />
|
|
167
|
+
{/if}
|
|
163
168
|
{/if}
|
|
164
169
|
{/if}
|
|
165
170
|
{#if app.compactLayout && config.showFooter}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { LocalCase } from '../state.svelte.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
caseNote?: Snippet<[LocalCase]>;
|
|
5
|
+
};
|
|
6
|
+
declare const ControlPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
2
7
|
type ControlPanel = ReturnType<typeof ControlPanel>;
|
|
3
8
|
export default ControlPanel;
|
|
@@ -23,48 +23,45 @@
|
|
|
23
23
|
const neutralPos = $derived(thumbAlignedPos(neutralFrac));
|
|
24
24
|
const fillLo = $derived(valFrac < neutralFrac ? valPos : neutralPos);
|
|
25
25
|
const fillHi = $derived(valFrac < neutralFrac ? neutralPos : valPos);
|
|
26
|
-
const sliderTip =
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const scoreTip =
|
|
30
|
-
'Gradient estimates the declared objective change before the solve finishes. It uses the selected bus LMP times the demand step, plus local curvature when the preview engine is unavailable. Exact is the resolved OPF objective change.';
|
|
26
|
+
const sliderTip = 'Drag to preview, release to solve. The tick marks the committed demand.';
|
|
27
|
+
const disabledTip = 'Display-only bus; demand is fixed';
|
|
28
|
+
const scoreTip = 'Gradient: first-order estimate from the LMP. Exact: solved objective change.';
|
|
31
29
|
</script>
|
|
32
30
|
|
|
33
31
|
{#if ctrl.activeSolvable}
|
|
34
32
|
{@const c = ctrl.activeSolvable}
|
|
35
33
|
<div class="slider-block">
|
|
36
|
-
<div class="slider-head
|
|
37
|
-
<span
|
|
38
|
-
<span class="val">{signed(ctrl.sliderValue)} MW</span>
|
|
34
|
+
<div class="slider-head">
|
|
35
|
+
<span>Demand change</span>
|
|
36
|
+
<span class="val mono">{signed(ctrl.sliderValue)} MW</span>
|
|
39
37
|
</div>
|
|
40
38
|
<div class="range-mode">
|
|
41
|
-
<div class="segment
|
|
39
|
+
<div class="segment" role="group" aria-label="Demand range">
|
|
42
40
|
<button
|
|
43
41
|
type="button"
|
|
44
42
|
class:active={app.demandRangeMode === 'local'}
|
|
45
43
|
aria-pressed={app.demandRangeMode === 'local'}
|
|
46
|
-
aria-label="
|
|
44
|
+
aria-label="Nearby demand range"
|
|
47
45
|
{disabled}
|
|
48
|
-
title="
|
|
49
|
-
onclick={() => ctrl.setDemandRangeMode('local')}>
|
|
46
|
+
title="Range near the committed demand"
|
|
47
|
+
onclick={() => ctrl.setDemandRangeMode('local')}>Nearby</button
|
|
50
48
|
>
|
|
51
49
|
<button
|
|
52
50
|
type="button"
|
|
53
51
|
class:active={app.demandRangeMode === 'full'}
|
|
54
52
|
aria-pressed={app.demandRangeMode === 'full'}
|
|
55
|
-
aria-label="
|
|
53
|
+
aria-label="Full demand range"
|
|
56
54
|
{disabled}
|
|
57
|
-
title="
|
|
58
|
-
onclick={() => ctrl.setDemandRangeMode('full')}>
|
|
55
|
+
title="Zero load to the local physical limit"
|
|
56
|
+
onclick={() => ctrl.setDemandRangeMode('full')}>Full range</button
|
|
59
57
|
>
|
|
60
58
|
</div>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
</span>
|
|
59
|
+
{#if disabled}
|
|
60
|
+
<span class="dim">Not adjustable</span>
|
|
61
|
+
{:else}
|
|
62
|
+
<span class="mono dim">{fmt.format(ctrl.sliderMin)} to {fmt.format(ctrl.sliderMax)} MW</span
|
|
63
|
+
>
|
|
64
|
+
{/if}
|
|
68
65
|
</div>
|
|
69
66
|
<div
|
|
70
67
|
class="slider-track"
|
|
@@ -78,7 +75,7 @@
|
|
|
78
75
|
step="any"
|
|
79
76
|
{disabled}
|
|
80
77
|
bind:value={ctrl.sliderCurrent, ctrl.setSliderPreview}
|
|
81
|
-
aria-label="
|
|
78
|
+
aria-label="Demand delta at selected bus"
|
|
82
79
|
onpointerdown={() => ctrl.setSliderPreview(ctrl.sliderValue)}
|
|
83
80
|
onkeydown={(e) => {
|
|
84
81
|
const next = sliderKeyValue(e.key, ctrl.sliderValue, ctrl.sliderMin, ctrl.sliderMax);
|
|
@@ -95,18 +92,18 @@
|
|
|
95
92
|
/>
|
|
96
93
|
</div>
|
|
97
94
|
<div class="demand-feedback" class:idle={!ctrl.previewing && !ctrl.isPerturbed(c)}>
|
|
98
|
-
<p class="pred
|
|
95
|
+
<p class="pred dim" aria-hidden={!(ctrl.predictedDeltaObj !== null && ctrl.previewing)}>
|
|
99
96
|
{#if ctrl.predictedDeltaObj !== null && ctrl.previewing}
|
|
100
|
-
|
|
97
|
+
Predicted objective change <span class="mono">{signed(ctrl.predictedDeltaObj)}</span>
|
|
101
98
|
{:else}
|
|
102
99
|
|
|
103
100
|
{/if}
|
|
104
101
|
</p>
|
|
105
|
-
<p class="score
|
|
102
|
+
<p class="score" aria-hidden={!(ctrl.gradientScore && ctrl.isPerturbed(c))}>
|
|
106
103
|
{#if ctrl.gradientScore && ctrl.isPerturbed(c)}
|
|
107
104
|
<span title={scoreTip}>
|
|
108
|
-
|
|
109
|
-
units
|
|
105
|
+
Gradient <span class="mono">{signed(ctrl.gradientScore.pred)}</span>, exact
|
|
106
|
+
<span class="mono">{signed(ctrl.gradientScore.exact)}</span> objective units
|
|
110
107
|
</span>
|
|
111
108
|
{:else}
|
|
112
109
|
|
|
@@ -114,7 +111,7 @@
|
|
|
114
111
|
</p>
|
|
115
112
|
<div class="reset-row">
|
|
116
113
|
{#if ctrl.isPerturbed(c)}
|
|
117
|
-
<button class="reset
|
|
114
|
+
<button class="reset" onclick={() => ctrl.resetCase(c)}>Reset demand</button>
|
|
118
115
|
{/if}
|
|
119
116
|
</div>
|
|
120
117
|
</div>
|
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
import type { Network, NetworkBus } from '../api.js';
|
|
5
5
|
import { getAppState, getController } from '../context.svelte.js';
|
|
6
6
|
import { getPanelLayout } from '../panels.svelte.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
branchColor,
|
|
9
|
+
busNeutral,
|
|
10
|
+
busRadiusFor,
|
|
11
|
+
demandColor,
|
|
12
|
+
priceColor,
|
|
13
|
+
scalarDomain
|
|
14
|
+
} from '../colors.js';
|
|
8
15
|
import { rgbaCss } from '../format.js';
|
|
9
16
|
import type { MulticonductorCase } from '../state.svelte.js';
|
|
10
17
|
import { edgeColor, attachmentColor } from '../multiconductor.js';
|
|
@@ -66,6 +73,8 @@
|
|
|
66
73
|
if (multiconductor) return [];
|
|
67
74
|
if (!app.studyView) return ctrl.activeDisplay?.values ?? [];
|
|
68
75
|
const solution = app.studyView.solution;
|
|
76
|
+
if (app.displayMode === 'demand')
|
|
77
|
+
return app.studyView.network.buses.map((bus) => ({ bus: bus.id, value: bus.demand_mw }));
|
|
69
78
|
if (app.displayMode === 'angle') return solution?.va ?? [];
|
|
70
79
|
if (app.displayMode === 'voltage')
|
|
71
80
|
return (
|
|
@@ -107,7 +116,9 @@
|
|
|
107
116
|
return rgbaCss(
|
|
108
117
|
value === undefined
|
|
109
118
|
? busNeutral
|
|
110
|
-
:
|
|
119
|
+
: (displayMode === 'demand' ? demandColor : priceColor)(
|
|
120
|
+
(value - domain.lo) / Math.max(domain.hi - domain.lo, 1e-12)
|
|
121
|
+
)
|
|
111
122
|
);
|
|
112
123
|
}
|
|
113
124
|
function branchLabel(id: number) {
|
|
@@ -348,7 +359,8 @@
|
|
|
348
359
|
<circle
|
|
349
360
|
cx={bus.lon}
|
|
350
361
|
cy={bus.lat}
|
|
351
|
-
r={Math.max(5,
|
|
362
|
+
r={Math.max(5, busRadiusFor(app.busSizing, Math.max(bus.demand_mw, bus.gen_mw))) /
|
|
363
|
+
scale}
|
|
352
364
|
fill={color(bus)}
|
|
353
365
|
style:stroke={selectedBus === bus.id ? '#2f6fbb' : 'var(--diagram-bus-stroke)'}
|
|
354
366
|
stroke-width={selectedBus === bus.id ? 3 : 1}
|
|
@@ -364,7 +376,7 @@
|
|
|
364
376
|
>Bus {multiconductor ? bus.uid : bus.id}{!multiconductor && bus.name
|
|
365
377
|
? `, ${bus.name}`
|
|
366
378
|
: ''}, {(bus.demand_mw * (multiconductor ? 1000 : 1)).toFixed(1)}
|
|
367
|
-
{multiconductor ? 'kW' : 'MW'} demand{values.has(bus.id)
|
|
379
|
+
{multiconductor ? 'kW' : 'MW'} demand{displayMode !== 'demand' && values.has(bus.id)
|
|
368
380
|
? `, ${values.get(bus.id)!.toFixed(4)} ${displayMode === 'price' ? 'objective units/MW LMP' : displayMode === 'voltage' ? 'pu voltage' : 'rad voltage angle'}`
|
|
369
381
|
: ''}</title
|
|
370
382
|
>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<div class="mode display-mode">
|
|
10
|
-
<div class="segment
|
|
10
|
+
<div class="segment" aria-label="Bus color variable">
|
|
11
11
|
{#each ctrl.displayOptions as option (option.mode)}
|
|
12
12
|
<button
|
|
13
13
|
type="button"
|
|
@@ -19,27 +19,16 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
<span class="mono dim">{ctrl.activeDisplay?.unit ?? ''}</span>
|
|
21
21
|
{#if app.sensitivityLoading}
|
|
22
|
-
<span class="
|
|
22
|
+
<span class="dim blink">Loading…</span>
|
|
23
23
|
{/if}
|
|
24
24
|
</div>
|
|
25
25
|
{#if ctrl.activeDisplay && ctrl.displayStats}
|
|
26
|
-
{
|
|
27
|
-
<div class="legend-heading">
|
|
28
|
-
<span class="legend-title">Locational marginal price</span>
|
|
29
|
-
<span
|
|
30
|
-
class="hint-dot mono"
|
|
31
|
-
title={ctrl.activeDisplay.copy}
|
|
32
|
-
aria-label={ctrl.activeDisplay.copy}>i</span
|
|
33
|
-
>
|
|
34
|
-
</div>
|
|
35
|
-
{:else}
|
|
36
|
-
<p class="dim small">{ctrl.activeDisplay.copy}</p>
|
|
37
|
-
{/if}
|
|
26
|
+
<p class="legend-title">{ctrl.activeDisplay.copy}</p>
|
|
38
27
|
<div class="legend" style:background={ctrl.activeDisplay.gradient}></div>
|
|
39
28
|
<div class="legend-labels mono">
|
|
40
29
|
{#if ctrl.displayStats.uniform !== null}
|
|
41
30
|
<span>
|
|
42
|
-
|
|
31
|
+
Uniform {displayFmt(ctrl.activeDisplay.mode, ctrl.displayStats.uniform)}
|
|
43
32
|
{ctrl.activeDisplay.unit}
|
|
44
33
|
</span>
|
|
45
34
|
{:else}
|
|
@@ -70,18 +59,10 @@
|
|
|
70
59
|
{/if}
|
|
71
60
|
|
|
72
61
|
<style>
|
|
73
|
-
.legend-heading {
|
|
74
|
-
display: flex;
|
|
75
|
-
align-items: center;
|
|
76
|
-
gap: 6px;
|
|
77
|
-
margin-top: 10px;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
62
|
.legend-title {
|
|
63
|
+
margin: 10px 0 0;
|
|
81
64
|
font-size: 12px;
|
|
82
65
|
font-weight: 600;
|
|
83
66
|
color: var(--ink);
|
|
84
67
|
}
|
|
85
|
-
|
|
86
|
-
/* .hint-dot lives in the shared stylesheet; SensitivityReadout uses it too. */
|
|
87
68
|
</style>
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import { getAppState } from '../context.svelte.js';
|
|
3
4
|
|
|
5
|
+
/** Optional host content rendered under the drop prompt. */
|
|
6
|
+
let { children }: { children?: Snippet } = $props();
|
|
7
|
+
|
|
4
8
|
const app = getAppState();
|
|
5
9
|
</script>
|
|
6
10
|
|
|
7
11
|
{#if app.dragOver}
|
|
8
12
|
<div class="dropzone" aria-hidden="true">
|
|
9
13
|
<div class="dropframe">
|
|
10
|
-
<p
|
|
11
|
-
<
|
|
14
|
+
<p>Drop case or geographic files</p>
|
|
15
|
+
{#if children}<div class="note">{@render children()}</div>{/if}
|
|
12
16
|
</div>
|
|
13
17
|
</div>
|
|
14
18
|
{/if}
|
|
@@ -39,7 +43,7 @@
|
|
|
39
43
|
font-size: 13px;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
.
|
|
46
|
+
.note {
|
|
43
47
|
font-size: 11px;
|
|
44
48
|
color: var(--text-secondary);
|
|
45
49
|
}
|
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
$$slots?: Slots;
|
|
8
|
-
}): Exports & {
|
|
9
|
-
$set?: any;
|
|
10
|
-
$on?: any;
|
|
11
|
-
};
|
|
12
|
-
z_$$bindings?: Bindings;
|
|
13
|
-
}
|
|
14
|
-
declare const DropZone: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
-
[evt: string]: CustomEvent<any>;
|
|
16
|
-
}, {}, {}, string>;
|
|
17
|
-
type DropZone = InstanceType<typeof DropZone>;
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
children?: Snippet;
|
|
4
|
+
};
|
|
5
|
+
declare const DropZone: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type DropZone = ReturnType<typeof DropZone>;
|
|
18
7
|
export default DropZone;
|