@tellegen/svelte 0.2.0 → 0.3.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.
Files changed (66) hide show
  1. package/README.md +36 -5
  2. package/dist/TellegenMap.svelte +325 -163
  3. package/dist/TellegenProvider.svelte +4 -0
  4. package/dist/TellegenShell.svelte +19 -13
  5. package/dist/api.d.ts +8 -1
  6. package/dist/api.js +6 -2
  7. package/dist/colors.d.ts +7 -7
  8. package/dist/colors.js +13 -13
  9. package/dist/components/AppHeader.svelte +23 -11
  10. package/dist/components/BindingLines.svelte +3 -6
  11. package/dist/components/BusPicker.svelte +8 -2
  12. package/dist/components/ControlPanel.svelte +106 -343
  13. package/dist/components/DemandSlider.svelte +21 -23
  14. package/dist/components/DiagramCanvas.svelte +486 -0
  15. package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
  16. package/dist/components/DisplayControls.svelte +4 -2
  17. package/dist/components/FormulationSelector.svelte +8 -3
  18. package/dist/components/LocalCaseDetails.svelte +55 -45
  19. package/dist/components/McResults.svelte +365 -0
  20. package/dist/components/McResults.svelte.d.ts +11 -0
  21. package/dist/components/ModelDetails.svelte +230 -0
  22. package/dist/components/ModelDetails.svelte.d.ts +7 -0
  23. package/dist/components/MulticonductorDetails.svelte +227 -78
  24. package/dist/components/NetworkStats.svelte +20 -18
  25. package/dist/components/NotificationCenter.svelte +243 -0
  26. package/dist/components/NotificationCenter.svelte.d.ts +3 -0
  27. package/dist/components/PanelCard.svelte +319 -0
  28. package/dist/components/PanelCard.svelte.d.ts +9 -0
  29. package/dist/components/PanelFrame.svelte +46 -0
  30. package/dist/components/PanelFrame.svelte.d.ts +16 -0
  31. package/dist/components/PanelHost.svelte +212 -0
  32. package/dist/components/PanelHost.svelte.d.ts +3 -0
  33. package/dist/components/PowerFlowDetails.svelte +99 -0
  34. package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
  35. package/dist/components/RatingSlider.svelte +5 -9
  36. package/dist/components/SavedNetworkDetails.svelte +214 -0
  37. package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
  38. package/dist/components/SensitivityReadout.svelte +24 -16
  39. package/dist/components/SolveCard.svelte +46 -49
  40. package/dist/components/TopMovers.svelte +8 -3
  41. package/dist/components/TopMovers.svelte.d.ts +2 -17
  42. package/dist/components.d.ts +2 -0
  43. package/dist/components.js +2 -0
  44. package/dist/controller.svelte.d.ts +54 -33
  45. package/dist/controller.svelte.js +927 -233
  46. package/dist/diagram-labels.d.ts +9 -0
  47. package/dist/diagram-labels.js +40 -0
  48. package/dist/display.d.ts +3 -7
  49. package/dist/display.js +31 -27
  50. package/dist/format.js +5 -4
  51. package/dist/index.d.ts +10 -4
  52. package/dist/index.js +7 -2
  53. package/dist/map.d.ts +1 -1
  54. package/dist/map.js +1 -1
  55. package/dist/multiconductor.d.ts +11 -2
  56. package/dist/multiconductor.js +107 -14
  57. package/dist/notices.svelte.d.ts +29 -0
  58. package/dist/notices.svelte.js +82 -0
  59. package/dist/panels.svelte.d.ts +65 -0
  60. package/dist/panels.svelte.js +215 -0
  61. package/dist/state.svelte.d.ts +90 -23
  62. package/dist/state.svelte.js +94 -20
  63. package/dist/synthetic-layout.js +1 -0
  64. package/package.json +22 -9
  65. package/dist/study-package.d.ts +0 -8
  66. package/dist/study-package.js +0 -17
@@ -0,0 +1,212 @@
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
+ const app = getAppState();
8
+ const layout = getPanelLayout();
9
+ const notices = getNoticeCenter();
10
+ const bottomReserve = $derived(notices.entries.length ? 120 : 88);
11
+ const left = $derived(layout.ordered('left').filter((p) => p.open));
12
+ const right = $derived(layout.ordered('right').filter((p) => p.open));
13
+ const floating = $derived(
14
+ layout.ordered().filter((p) => p.open && layout.position(p) === 'floating')
15
+ );
16
+ const drawer = $derived(layout.panels.find((p) => p.id === layout.drawer));
17
+ const toolbarTop = $derived(app.headerInset + 6);
18
+ const panelTop = $derived(toolbarTop + 42);
19
+ const drawerHeight = $derived(
20
+ Math.max(
21
+ 1,
22
+ Math.min(
23
+ layout.viewportHeight * 0.6,
24
+ (layout.viewportHeight - panelTop - bottomReserve - 8) * 0.75
25
+ )
26
+ )
27
+ );
28
+ function resize() {
29
+ layout.resizeViewport(window.innerWidth, window.innerHeight, panelTop);
30
+ }
31
+ onMount(() => {
32
+ resize();
33
+ try {
34
+ layout.initialize(window.localStorage);
35
+ } catch {
36
+ layout.initialize();
37
+ }
38
+ });
39
+ $effect(() => {
40
+ const top = panelTop;
41
+ const bottom = bottomReserve;
42
+ untrack(() => {
43
+ layout.bottom = bottom;
44
+ layout.resizeViewport(window.innerWidth, window.innerHeight, top);
45
+ });
46
+ });
47
+ $effect(() => {
48
+ app.sheetInset = layout.compact && drawer ? drawerHeight + bottomReserve : 0;
49
+ });
50
+ </script>
51
+
52
+ <svelte:window
53
+ onresize={resize}
54
+ onpointermove={(event) => layout.moveGesture(event.clientX, event.clientY)}
55
+ onpointerup={() => layout.finishGesture()}
56
+ onpointercancel={() => layout.finishGesture()}
57
+ />
58
+ <div class="panel-toolbar" style:top="{toolbarTop}px" aria-label="Panels">
59
+ <div class="panel-buttons">
60
+ {#each layout.ordered().filter((p) => p.side === 'left') as panel (panel.id)}
61
+ <button
62
+ aria-label={panel.title}
63
+ aria-pressed={layout.compact ? layout.drawer === panel.id : panel.open}
64
+ onclick={() => layout.toggle(panel)}>{panel.title}</button
65
+ >
66
+ {/each}
67
+ </div>
68
+ <div class="panel-buttons right">
69
+ {#each layout.ordered().filter((p) => p.side === 'right') as panel (panel.id)}
70
+ <button
71
+ aria-label={panel.title}
72
+ aria-pressed={layout.compact ? layout.drawer === panel.id : panel.open}
73
+ onclick={() => layout.toggle(panel)}>{panel.title}</button
74
+ >
75
+ {/each}
76
+ <button
77
+ class="reset-layout"
78
+ onclick={() => layout.reset()}
79
+ title="Restore the default panel positions">Reset layout</button
80
+ >
81
+ </div>
82
+ </div>
83
+ {#if layout.storageError}<p class="layout-error" role="status" style:top="{panelTop}px">
84
+ {layout.storageError}
85
+ </p>{/if}
86
+ {#if layout.compact}
87
+ {#if drawer}<div class="drawer" style:bottom="{bottomReserve}px" style:height="{drawerHeight}px">
88
+ <PanelCard panel={drawer} mobile />
89
+ </div>{/if}
90
+ {:else}
91
+ <div
92
+ class="dock left"
93
+ style:bottom="{Math.max(100, bottomReserve)}px"
94
+ data-panel-dock="left"
95
+ style:top="{panelTop}px"
96
+ style:width="{layout.dockWidth('left')}px"
97
+ >
98
+ {#each left as panel (panel.id)}<PanelCard {panel} />{/each}
99
+ </div>
100
+ <div
101
+ class="dock right"
102
+ style:bottom="{Math.max(100, bottomReserve)}px"
103
+ data-panel-dock="right"
104
+ style:top="{panelTop}px"
105
+ style:width="{layout.dockWidth('right')}px"
106
+ >
107
+ {#each right as panel (panel.id)}<PanelCard {panel} />{/each}
108
+ </div>
109
+ {#each floating as panel (panel.id)}<PanelCard {panel} floating />{/each}
110
+ {/if}
111
+
112
+ <style>
113
+ .panel-toolbar {
114
+ position: fixed;
115
+ z-index: 30;
116
+ left: 20px;
117
+ right: 20px;
118
+ display: flex;
119
+ justify-content: space-between;
120
+ align-items: center;
121
+ gap: 12px;
122
+ pointer-events: none;
123
+ height: 34px;
124
+ }
125
+ .panel-buttons {
126
+ display: flex;
127
+ gap: 6px;
128
+ align-items: center;
129
+ pointer-events: auto;
130
+ }
131
+ button {
132
+ border: 1px solid var(--line);
133
+ border-radius: 4px;
134
+ background: var(--paper);
135
+ color: var(--ink);
136
+ font: 12px/1.4 var(--font-display);
137
+ min-height: 32px;
138
+ padding: 6px 12px;
139
+ cursor: pointer;
140
+ box-shadow: var(--elev-1);
141
+ }
142
+ button[aria-pressed='true'] {
143
+ border-color: var(--accent);
144
+ background: var(--accent-soft);
145
+ }
146
+ button:focus-visible {
147
+ outline: 2px solid var(--focus-ring);
148
+ outline-offset: 2px;
149
+ }
150
+ button:hover {
151
+ border-color: var(--accent);
152
+ }
153
+ .reset-layout {
154
+ color: var(--text-secondary);
155
+ font-size: 11px;
156
+ padding: 6px 8px;
157
+ background: var(--panel);
158
+ }
159
+ .dock {
160
+ position: fixed;
161
+ z-index: 25;
162
+ bottom: 100px;
163
+ display: flex;
164
+ flex-direction: column;
165
+ align-items: stretch;
166
+ gap: 12px;
167
+ pointer-events: none;
168
+ }
169
+ .dock.left {
170
+ left: 20px;
171
+ }
172
+ .dock.right {
173
+ right: 20px;
174
+ }
175
+ .drawer {
176
+ position: fixed;
177
+ bottom: 88px;
178
+ left: 12px;
179
+ right: 12px;
180
+ z-index: 25;
181
+ }
182
+ .layout-error {
183
+ position: fixed;
184
+ left: 50%;
185
+ transform: translateX(-50%);
186
+ z-index: 42;
187
+ max-width: 400px;
188
+ margin: 0;
189
+ padding: 8px 12px;
190
+ font: 12px/1.5 var(--font-display);
191
+ color: var(--ink);
192
+ background: var(--paper);
193
+ border: 1px solid var(--line);
194
+ }
195
+ @media (max-width: 960px) {
196
+ .panel-toolbar {
197
+ left: 12px;
198
+ right: 12px;
199
+ gap: 6px;
200
+ }
201
+ .panel-buttons {
202
+ gap: 4px;
203
+ }
204
+ button {
205
+ padding: 6px 8px;
206
+ font-size: 11px;
207
+ }
208
+ .reset-layout {
209
+ display: none;
210
+ }
211
+ }
212
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const PanelHost: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type PanelHost = ReturnType<typeof PanelHost>;
3
+ export default PanelHost;
@@ -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: 11px;
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>
@@ -0,0 +1,3 @@
1
+ declare const PowerFlowDetails: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type PowerFlowDetails = ReturnType<typeof PowerFlowDetails>;
3
+ export default PowerFlowDetails;
@@ -25,7 +25,7 @@
25
25
  '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
26
  const disabledTip = "this line's rating is synthesized; not adjustable";
27
27
  const scoreTip =
28
- 'Gradient is the estimate of total cost change versus base before the solve finishes, from the engine preview at the committed point. Exact is the resolved OPF objective change.';
28
+ '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
29
  </script>
30
30
 
31
31
  {#if ctrl.activeSolvable}
@@ -39,9 +39,7 @@
39
39
  <div class="line-row mono">
40
40
  {#if branch}
41
41
  <span class="dim"
42
- >line {branch.from}&#8201;&ndash;&#8201;{branch.to} &middot; {fmt.format(
43
- branch.rate_mw
44
- )} MW</span
42
+ >line {branch.from}&#8201;&ndash;&#8201;{branch.to}, {fmt.format(branch.rate_mw)} MW</span
45
43
  >
46
44
  {/if}
47
45
  <span class="dim">
@@ -75,7 +73,7 @@
75
73
  <div class="rating-feedback" class:idle={!ctrl.previewing && !ctrl.isPerturbed(c)}>
76
74
  <p class="pred mono dim" aria-hidden={!(ctrl.predictedDeltaObj !== null && ctrl.previewing)}>
77
75
  {#if ctrl.predictedDeltaObj !== null && ctrl.previewing}
78
- predicted &Delta;cost {signed(ctrl.predictedDeltaObj)} $/h
76
+ predicted &Delta;objective {signed(ctrl.predictedDeltaObj)}
79
77
  {:else}
80
78
  &nbsp;
81
79
  {/if}
@@ -83,10 +81,8 @@
83
81
  <p class="score mono" aria-hidden={!(ctrl.gradientScore && ctrl.isPerturbed(c))}>
84
82
  {#if ctrl.gradientScore && ctrl.isPerturbed(c)}
85
83
  <span title={scoreTip}>
86
- gradient {signed(ctrl.gradientScore.pred)} &middot; exact {signed(
87
- ctrl.gradientScore.exact
88
- )}
89
- $/h
84
+ gradient {signed(ctrl.gradientScore.pred)}, exact {signed(ctrl.gradientScore.exact)} objective
85
+ units
90
86
  </span>
91
87
  {:else}
92
88
  &nbsp;
@@ -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: 11px;
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: 10px;
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: 11px;
212
+ font-weight: 400;
213
+ }
214
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const SavedNetworkDetails: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SavedNetworkDetails = ReturnType<typeof SavedNetworkDetails>;
3
+ export default SavedNetworkDetails;
@@ -5,19 +5,24 @@
5
5
 
6
6
  const app = getAppState();
7
7
  const ctrl = getController();
8
- const busUnitTitle =
9
- 'LMP is measured in $/MWh and demand is perturbed in MW, so dLMP/dd has units ($/MWh)/MW. ' +
10
- 'DC LMPs move only when a binding constraint changes, so most nudges in this slider range ' +
11
- 'shift prices very little unless the active set changes.';
12
- const branchUnitTitle =
13
- 'LMP is measured in $/MWh and the rating is perturbed in MVA, so dLMP/dfmax has units ' +
14
- "($/MWh)/MVA. The column is zero unless the line's limit is binding.";
8
+ const reportedUnits = $derived(
9
+ (ctrl.selectedSensitivity?.units ?? '(objective units/MW)/MW').replaceAll(
10
+ 'objective_unit',
11
+ 'objective units'
12
+ )
13
+ );
14
+ const busUnitTitle = $derived(
15
+ `LMP response to active demand, in ${reportedUnits}. The response is small while the active set stays unchanged.`
16
+ );
17
+ const branchUnitTitle = $derived(
18
+ `LMP response to the thermal rating, in ${reportedUnits}. The column is zero when the limit is inactive.`
19
+ );
15
20
 
16
- // Branch mode: the selection is a line and the column is ∂LMP/∂rating; the
21
+ // Branch mode: the selection is a line and the column is price/rating; the
17
22
  // legend and preview pipeline are identical (both columns are bus-keyed).
18
23
  const branchMode = $derived(app.selectedBranch !== null);
19
24
  const unitTitle = $derived(branchMode ? branchUnitTitle : busUnitTitle);
20
- const units = $derived(branchMode ? '($/MWh)/MVA' : '($/MWh)/MW');
25
+ const previewUnits = $derived(app.previewPrices?.units ?? 'objective units/MW');
21
26
 
22
27
  const previewStep = $derived(
23
28
  branchMode
@@ -54,33 +59,36 @@
54
59
  {#if ctrl.sensSummary?.flat}
55
60
  <div class="legend flat" style:background={ctrl.flatSensBackground}></div>
56
61
  <div class="legend-labels mono single">
57
- <span>uniform {signedExp(ctrl.sensSummary.mean * previewStep)} $/MWh</span>
62
+ <span
63
+ >uniform {signedExp(ctrl.sensSummary.mean * previewStep)}
64
+ {previewUnits}</span
65
+ >
58
66
  </div>
59
67
  {:else if ctrl.previewScale}
60
68
  <!-- The bounds are fixed for the whole drag (column scale × full slider
61
69
  deflection), so the ramp ends label the colors: full green/purple is
62
- the predicted ΔLMP at full deflection, and intensity grows with the
70
+ the predicted LMP change at full deflection, and intensity grows with the
63
71
  step instead of renormalizing every frame. -->
64
72
  <div class="legend" style:background={sensGradient}></div>
65
73
  <div class="legend-labels mono">
66
74
  <span>&minus;{ctrl.previewScale.toExponential(1)}</span>
67
- <span>&Delta;LMP $/MWh</span>
75
+ <span>&Delta; LMP {previewUnits}</span>
68
76
  <span>+{ctrl.previewScale.toExponential(1)}</span>
69
77
  </div>
70
78
  {/if}
71
79
  {:else}
72
80
  <p class="dim small sensitivity-copy" title={unitTitle}>
73
81
  {#if branchMode}
74
- LMP response per MVA of rating on line {branchLabel}.
82
+ LMP response to the rating on line {branchLabel}.
75
83
  {:else}
76
- LMP response per MW of demand at bus {app.selectedBus}.
84
+ LMP response to demand at bus {app.selectedBus}.
77
85
  {/if}
78
86
  <span class="hint-dot mono" title={unitTitle} aria-label={unitTitle}>i</span>
79
87
  </p>
80
88
  {#if ctrl.sensSummary?.flat}
81
89
  <div class="legend flat" style:background={ctrl.flatSensBackground}></div>
82
90
  <div class="legend-labels mono single">
83
- <span>uniform {signedExp(ctrl.sensSummary.mean)} {units}</span>
91
+ <span>uniform {signedExp(ctrl.sensSummary.mean)} {reportedUnits}</span>
84
92
  </div>
85
93
  {:else if ctrl.sensSummary}
86
94
  <div class="legend" style:background={sensGradient}></div>
@@ -93,7 +101,7 @@
93
101
  <div class="legend" style:background="var(--line)" style:opacity="0.4"></div>
94
102
  <div class="legend-labels mono single">
95
103
  <span class="blink">
96
- computing {#if branchMode}&part;LMP/&part;fmax{:else}&part;LMP/&part;d{/if}&hellip;
104
+ computing {#if branchMode}&part;value/&part;fmax{:else}&part;value/&part;d{/if}&hellip;
97
105
  </span>
98
106
  </div>
99
107
  {/if}