@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.
Files changed (45) hide show
  1. package/README.md +38 -10
  2. package/dist/Sparkline.svelte +4 -4
  3. package/dist/TellegenMap.svelte +80 -66
  4. package/dist/TellegenProvider.svelte +29 -13
  5. package/dist/colors.d.ts +37 -1
  6. package/dist/colors.js +63 -0
  7. package/dist/components/AppFooter.svelte +4 -4
  8. package/dist/components/AppHeader.svelte +15 -19
  9. package/dist/components/BindingLines.svelte +10 -9
  10. package/dist/components/BusPicker.svelte +5 -6
  11. package/dist/components/ControlPanel.svelte +16 -11
  12. package/dist/components/ControlPanel.svelte.d.ts +6 -1
  13. package/dist/components/DemandSlider.svelte +26 -29
  14. package/dist/components/DiagramCanvas.svelte +16 -4
  15. package/dist/components/DisplayControls.svelte +5 -24
  16. package/dist/components/DropZone.svelte +7 -3
  17. package/dist/components/DropZone.svelte.d.ts +6 -17
  18. package/dist/components/FormulationSelector.svelte +2 -7
  19. package/dist/components/LocalCaseDetails.svelte +126 -126
  20. package/dist/components/LocalCaseDetails.svelte.d.ts +6 -1
  21. package/dist/components/MulticonductorDetails.svelte +48 -38
  22. package/dist/components/NetworkStats.svelte +14 -16
  23. package/dist/components/PanelCard.svelte +1 -1
  24. package/dist/components/PlacementCue.svelte +1 -1
  25. package/dist/components/RatingSlider.svelte +22 -23
  26. package/dist/components/RestoreDefaultsButton.svelte +2 -3
  27. package/dist/components/SensitivityReadout.svelte +9 -27
  28. package/dist/components/SolveCard.svelte +69 -35
  29. package/dist/components/TopMovers.svelte +5 -5
  30. package/dist/context.svelte.d.ts +6 -2
  31. package/dist/context.svelte.js +4 -2
  32. package/dist/controller.svelte.d.ts +7 -0
  33. package/dist/controller.svelte.js +37 -8
  34. package/dist/display.d.ts +3 -2
  35. package/dist/display.js +28 -11
  36. package/dist/format.d.ts +1 -9
  37. package/dist/format.js +1 -14
  38. package/dist/index.d.ts +1 -1
  39. package/dist/map.d.ts +1 -1
  40. package/dist/map.js +1 -1
  41. package/dist/state.svelte.d.ts +6 -1
  42. package/dist/state.svelte.js +1 -0
  43. package/dist/styles.css +3 -0
  44. package/dist/types.d.ts +1 -1
  45. 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="mono" class:inline>
11
- <a href="/credits/">credits</a>
10
+ <footer class:inline>
11
+ <a href="/credits/">Credits</a>
12
12
  <i class="sep"></i>
13
- <a href="/privacy/">privacy</a>
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">&#8675;</span> drop a case or geographic file anywhere</span
17
+ ><span class="arrow">&#8675;</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="networks">
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="demand perturbed"></i>{/if}</span
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="remove {c.name} from this browser"
54
- title="remove {c.name} from this browser"
53
+ aria-label="Remove {c.name}"
54
+ title="Remove {c.name}"
55
55
  onclick={(e) => ctrl.removeBackendCase(c, e)}>&#10005;</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">local</span>
63
+ <span class="cregion mono">Local</span>
64
64
  </button>
65
65
  <button
66
66
  class="case-remove mono"
67
- aria-label="remove {c.label}"
68
- title="remove {c.label}"
67
+ aria-label="Remove {c.label}"
68
+ title="Remove {c.label}"
69
69
  onclick={(e) => ctrl.removeLocalCase(c, e)}>&#10005;</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">multiconductor</span>
77
+ <span class="cregion mono">Multiconductor</span>
78
78
  </button>
79
79
  <button
80
80
  class="case-remove mono"
81
- aria-label="remove {c.label}"
82
- title="remove {c.label}"
81
+ aria-label="Remove {c.label}"
82
+ title="Remove {c.label}"
83
83
  onclick={(e) => ctrl.removeMultiCase(c, e)}>&#10005;</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">&#8675;</span>{ctrl.showFileDropUi
94
- ? 'drop a case file'
95
- : 'open a case file'}</span
90
+ ? 'Drop a case file'
91
+ : 'Open a case file'}</span
96
92
  >
97
- <span class="cregion mono">case + geo files</span>
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">docs</a>
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
- class="mono dim head"
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
- line {branch.from}&#8201;&ndash;&#8201;{branch.to}, {Math.round(loading * 100)}%
42
+ Line <span class="mono">{branch.from}&#8201;&ndash;&#8201;{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="mono dim" for="bus-lookup-input">bus lookup</label>
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="bus id"
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 class="mono" id="bus-lookup-listbox" role="listbox" aria-label="matching buses">
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
- bus {bus.id}
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="mono dim empty">no bus matches "{query.trim()}"</p>
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 mono blink">parsing&hellip;</p>
97
+ <p class="dim blink">Parsing&hellip;</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 mono">
125
- {config.loadDefaultCases ? 'no default cases loaded' : 'drop a case file to begin'}
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 mono" onclick={ctrl.restoreDefaultCases}
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 mono blink">loading selected case&hellip;</p>
136
+ <p class="dim blink">Loading case&hellip;</p>
134
137
  {:else}
135
- <p class="dim mono blink">loading cases&hellip;</p>
138
+ <p class="dim blink">Loading cases&hellip;</p>
136
139
  {/if}
137
140
  {/if}
138
141
  {:else}
@@ -157,9 +160,11 @@
157
160
  {@render selectionBlock()}
158
161
  {/if}
159
162
 
160
- <hr />
163
+ {#if app.busSizing === 'power'}
164
+ <hr />
161
165
 
162
- <SizeLegend />
166
+ <SizeLegend />
167
+ {/if}
163
168
  {/if}
164
169
  {/if}
165
170
  {#if app.compactLayout && config.showFooter}
@@ -1,3 +1,8 @@
1
- declare const ControlPanel: import("svelte").Component<Record<string, never>, {}, "">;
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
- 'The black tick marks the last committed demand. Drag the knob to preview a new demand; release to solve and move the tick to that point.';
28
- const disabledTip = 'this analysis bus is display-only; demand is not adjustable';
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 mono">
37
- <span>&Delta; demand</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 mono" role="group" aria-label="demand range">
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="nearby demand range"
44
+ aria-label="Nearby demand range"
47
45
  {disabled}
48
- title="range near the selected demand setting"
49
- onclick={() => ctrl.setDemandRangeMode('local')}>nearby</button
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="full range of demand"
53
+ aria-label="Full demand range"
56
54
  {disabled}
57
- title="range from zero load to the local physical limit"
58
- onclick={() => ctrl.setDemandRangeMode('full')}>full range</button
55
+ title="Zero load to the local physical limit"
56
+ onclick={() => ctrl.setDemandRangeMode('full')}>Full range</button
59
57
  >
60
58
  </div>
61
- <span class="mono dim">
62
- {#if disabled}
63
- not adjustable
64
- {:else}
65
- {fmt.format(ctrl.sliderMin)} to {fmt.format(ctrl.sliderMax)} MW
66
- {/if}
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="demand delta at selected bus"
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 mono dim" aria-hidden={!(ctrl.predictedDeltaObj !== null && ctrl.previewing)}>
95
+ <p class="pred dim" aria-hidden={!(ctrl.predictedDeltaObj !== null && ctrl.previewing)}>
99
96
  {#if ctrl.predictedDeltaObj !== null && ctrl.previewing}
100
- predicted &Delta;objective {signed(ctrl.predictedDeltaObj)}
97
+ Predicted objective change <span class="mono">{signed(ctrl.predictedDeltaObj)}</span>
101
98
  {:else}
102
99
  &nbsp;
103
100
  {/if}
104
101
  </p>
105
- <p class="score mono" aria-hidden={!(ctrl.gradientScore && ctrl.isPerturbed(c))}>
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
- gradient {signed(ctrl.gradientScore.pred)}, exact {signed(ctrl.gradientScore.exact)} objective
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
  &nbsp;
@@ -114,7 +111,7 @@
114
111
  </p>
115
112
  <div class="reset-row">
116
113
  {#if ctrl.isPerturbed(c)}
117
- <button class="reset mono" onclick={() => ctrl.resetCase(c)}>reset demand</button>
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 { branchColor, busNeutral, busRadius, priceColor, scalarDomain } from '../colors.js';
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
- : priceColor((value - domain.lo) / Math.max(domain.hi - domain.lo, 1e-12))
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, busRadius(Math.max(bus.demand_mw, bus.gen_mw))) / scale}
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 mono" aria-label="bus color variable">
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="mono dim blink">&part; loading&hellip;</span>
22
+ <span class="dim blink">Loading&hellip;</span>
23
23
  {/if}
24
24
  </div>
25
25
  {#if ctrl.activeDisplay && ctrl.displayStats}
26
- {#if ctrl.activeDisplay.mode === 'price'}
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
- uniform {displayFmt(ctrl.activeDisplay.mode, ctrl.displayStats.uniform)}
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 class="mono">drop to parse: case files or geographic files</p>
11
- <p class="mono hint">parsed in your browser; the file never uploads</p>
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
- .dropframe .hint {
46
+ .note {
43
47
  font-size: 11px;
44
48
  color: var(--text-secondary);
45
49
  }
@@ -1,18 +1,7 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
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;