@tellegen/svelte 0.3.0 → 0.4.1

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 (71) hide show
  1. package/README.md +233 -0
  2. package/dist/Sparkline.svelte +2 -2
  3. package/dist/TellegenMap.svelte +239 -66
  4. package/dist/TellegenMap.svelte.d.ts +45 -0
  5. package/dist/TellegenProvider.svelte +36 -4
  6. package/dist/TellegenShell.svelte +14 -42
  7. package/dist/TellegenViewer.svelte +12 -2
  8. package/dist/api.d.ts +10 -2
  9. package/dist/api.js +2 -0
  10. package/dist/basemap.d.ts +192 -0
  11. package/dist/basemap.js +259 -0
  12. package/dist/colors.d.ts +55 -1
  13. package/dist/colors.js +89 -0
  14. package/dist/components/AppFooter.svelte +3 -2
  15. package/dist/components/AppHeader.svelte +21 -8
  16. package/dist/components/BindingLines.svelte +1 -1
  17. package/dist/components/BusPicker.svelte +3 -3
  18. package/dist/components/ControlPanel.svelte +4 -2
  19. package/dist/components/DemandSlider.svelte +16 -7
  20. package/dist/components/DiagramCanvas.svelte +17 -5
  21. package/dist/components/DisplayControls.svelte +5 -1
  22. package/dist/components/DropZone.svelte +1 -1
  23. package/dist/components/FormulationSelector.svelte +1 -1
  24. package/dist/components/LocalCaseDetails.svelte +6 -3
  25. package/dist/components/McResults.svelte +98 -35
  26. package/dist/components/McResults.svelte.d.ts +8 -2
  27. package/dist/components/ModelDetails.svelte +5 -5
  28. package/dist/components/MulticonductorDetails.svelte +9 -9
  29. package/dist/components/NotificationCenter.svelte +4 -2
  30. package/dist/components/PanelCard.svelte +72 -7
  31. package/dist/components/PanelHost.svelte +59 -34
  32. package/dist/components/PanelHost.svelte.d.ts +4 -1
  33. package/dist/components/PlacementCue.svelte +1 -1
  34. package/dist/components/PowerFlowDetails.svelte +1 -1
  35. package/dist/components/RatingSlider.svelte +14 -5
  36. package/dist/components/RestoreDefaultsButton.svelte +1 -1
  37. package/dist/components/SavedNetworkDetails.svelte +3 -3
  38. package/dist/components/SensitivityReadout.svelte +2 -2
  39. package/dist/components/ShellEnvironment.svelte +51 -0
  40. package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
  41. package/dist/components/SizeLegend.svelte +2 -2
  42. package/dist/components/SolveCard.svelte +3 -3
  43. package/dist/components/TopMovers.svelte +1 -1
  44. package/dist/components.d.ts +3 -0
  45. package/dist/components.js +3 -0
  46. package/dist/context.svelte.d.ts +8 -1
  47. package/dist/context.svelte.js +8 -3
  48. package/dist/controller.svelte.d.ts +65 -5
  49. package/dist/controller.svelte.js +266 -78
  50. package/dist/display.d.ts +3 -2
  51. package/dist/display.js +22 -4
  52. package/dist/format.d.ts +9 -0
  53. package/dist/format.js +13 -1
  54. package/dist/index.d.ts +6 -2
  55. package/dist/index.js +2 -0
  56. package/dist/map.d.ts +4 -2
  57. package/dist/map.js +2 -1
  58. package/dist/maplibre-worker.d.ts +5 -0
  59. package/dist/maplibre-worker.dev.d.ts +11 -0
  60. package/dist/maplibre-worker.dev.js +15 -0
  61. package/dist/maplibre-worker.js +8 -0
  62. package/dist/notices.svelte.js +5 -1
  63. package/dist/panels.svelte.d.ts +6 -0
  64. package/dist/panels.svelte.js +31 -1
  65. package/dist/slider.d.ts +12 -0
  66. package/dist/slider.js +31 -0
  67. package/dist/state.svelte.d.ts +25 -3
  68. package/dist/state.svelte.js +13 -0
  69. package/dist/styles.css +59 -6
  70. package/dist/types.d.ts +1 -1
  71. package/package.json +13 -8
package/dist/colors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DisplayMode } from './state.svelte.js';
1
+ import type { BusSizing, DisplayMode } from './state.svelte.js';
2
2
  export type RGBA = [number, number, number, number];
3
3
  export interface SensitivityDomain {
4
4
  min: number;
@@ -10,6 +10,13 @@ export interface SensitivityDomain {
10
10
  }
11
11
  export declare const priceColor: (t: number) => RGBA;
12
12
  export declare const priceGradient: string;
13
+ /** Demand colour for a fraction `t` of the demand domain, on a square-root
14
+ * scale: bus demand is heavy tailed (a few large cities, many small towns),
15
+ * and a linear scale would paint everything but the largest buses one hue. */
16
+ export declare const demandColor: (t: number) => RGBA;
17
+ /** The demand ramp as a CSS gradient whose stops sit where the square-root
18
+ * scale puts them, so a legend bar reads linearly from its low to high label. */
19
+ export declare const demandGradient: string;
13
20
  /** Trimmed nodal value color domain: 5th to 95th percentile, with a one-unit
14
21
  * minimum span. Nodal values are heavy tailed when one binding line pins a
15
22
  * handful of buses far from the pack) so min-max scaling compresses the pack
@@ -20,6 +27,14 @@ export declare function priceDomain(values: number[]): {
20
27
  lo: number;
21
28
  hi: number;
22
29
  };
30
+ /** Demand colour domain: zero to the 98th percentile, at least 1 MW. Zero
31
+ * anchors the scale so a bus without load reads as none; the percentile keeps
32
+ * one metropolis from washing out every other bus, and the few above it clamp
33
+ * to the top of the ramp. */
34
+ export declare function demandDomain(values: number[]): {
35
+ lo: number;
36
+ hi: number;
37
+ };
23
38
  /** Center a scalar display variable (bus voltage angle, |V|) on a symmetric domain
24
39
  * with a per-mode minimum span; nodal values keep their own robust quantile domain. Shared by
25
40
  * the control panel readout and the map color scale. */
@@ -32,6 +47,24 @@ export declare const sensColor: (t: number) => RGBA;
32
47
  export declare const sensGradient: string;
33
48
  export declare const sensNeutral: RGBA;
34
49
  export declare const busNeutral: RGBA;
50
+ /** Overlay colours that depend on the basemap's lightness: bus outlines,
51
+ * hover highlights, the selected outline, the "not yet solved" grey, and
52
+ * the `.pwd` substation marker. The selection blue is the same in both. */
53
+ export interface MapPalette {
54
+ outline: RGBA;
55
+ /** The multiconductor view's bus outline, a shade stronger. */
56
+ outlineStrong: RGBA;
57
+ highlight: RGBA;
58
+ highlightSoft: RGBA;
59
+ selected: RGBA;
60
+ grey: RGBA;
61
+ greyLine: RGBA;
62
+ substation: RGBA;
63
+ substationLine: RGBA;
64
+ /** Fill of a neutral (non-phase) terminal in the multiconductor detail. */
65
+ neutral: RGBA;
66
+ }
67
+ export declare const MAP_PALETTE: Record<'light' | 'dark', MapPalette>;
35
68
  export declare function sensFlatColor(domain: Pick<SensitivityDomain, 'mean'>): RGBA;
36
69
  /** The fixed normalization for a slider drag: predicted nodal value change divides by
37
70
  * scale × maxAbsStep, so color intensity is linear in the step and saturates at
@@ -48,3 +81,24 @@ export declare function branchWidth(loading: number): number;
48
81
  /** Bus radius in px; area tracks max(load, gen) so big plants and big loads
49
82
  * both read at a glance. Shared by the map layers and the size legend. */
50
83
  export declare function busRadius(maxMw: number): number;
84
+ /** Radius in px of every bus marker under `uniform` sizing, and the most a
85
+ * uniform marker grows to on the map. */
86
+ export declare const UNIFORM_BUS_RADIUS = 4.5;
87
+ /** The least a uniform marker shrinks to on the map, in px. */
88
+ export declare const UNIFORM_BUS_MIN_RADIUS = 2;
89
+ /** The ground radius of a uniform marker on the map, in metres. */
90
+ export declare const UNIFORM_BUS_GROUND_RADIUS = 9000;
91
+ /** Bus radius in px under a sizing choice: `power` scales with max(load, gen)
92
+ * through `busRadius`, `uniform` draws every bus alike. */
93
+ export declare function busRadiusFor(sizing: BusSizing, maxMw: number): number;
94
+ /** Radius settings for the map's bus markers under a sizing choice. `power`
95
+ * sizes in pixels through `busRadius`. `uniform` gives every bus one ground
96
+ * radius, drawn between `UNIFORM_BUS_MIN_RADIUS` and `UNIFORM_BUS_RADIUS`
97
+ * pixels, so a whole country reads as a mesh of dots with its lines showing
98
+ * and a closer view as distinct buses. */
99
+ export declare function busMarkerRadius(sizing: BusSizing): {
100
+ radius: (maxMw: number) => number;
101
+ radiusUnits: 'meters' | 'pixels';
102
+ radiusMinPixels: number;
103
+ radiusMaxPixels: number;
104
+ };
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);
@@ -78,6 +109,32 @@ export const sensColor = (t) => ramp(SENS_STOPS, (t + 1) / 2);
78
109
  export const sensGradient = cssGradient(SENS_STOPS);
79
110
  export const sensNeutral = SENS_STOPS[2];
80
111
  export const busNeutral = [180, 175, 165, 200];
112
+ export const MAP_PALETTE = {
113
+ light: {
114
+ outline: [46, 42, 34, 110],
115
+ outlineStrong: [46, 42, 34, 120],
116
+ highlight: [32, 36, 43, 90],
117
+ highlightSoft: [32, 36, 43, 70],
118
+ selected: [32, 36, 43, 255],
119
+ grey: [110, 115, 120, 200],
120
+ greyLine: [138, 131, 117, 150],
121
+ substation: [70, 92, 124, 190],
122
+ substationLine: [38, 52, 78, 220],
123
+ neutral: [120, 114, 102, 255]
124
+ },
125
+ dark: {
126
+ outline: [232, 238, 244, 120],
127
+ outlineStrong: [232, 238, 244, 130],
128
+ highlight: [232, 238, 244, 90],
129
+ highlightSoft: [232, 238, 244, 70],
130
+ selected: [255, 255, 255, 255],
131
+ grey: [150, 160, 170, 200],
132
+ greyLine: [150, 160, 170, 150],
133
+ substation: [120, 150, 200, 190],
134
+ substationLine: [180, 205, 235, 220],
135
+ neutral: [150, 160, 170, 255]
136
+ }
137
+ };
81
138
  const FLAT_SENSITIVITY_TINT = 0.55;
82
139
  export function sensFlatColor(domain) {
83
140
  if (domain.mean === 0)
@@ -138,3 +195,35 @@ export function branchWidth(loading) {
138
195
  export function busRadius(maxMw) {
139
196
  return 3.2 + 0.45 * Math.sqrt(Math.max(maxMw, 1));
140
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
+ }
@@ -31,7 +31,7 @@
31
31
  padding: 8px 20px;
32
32
  font-size: 10.5px;
33
33
  color: var(--text-secondary);
34
- background: linear-gradient(rgba(236, 233, 226, 0), rgba(236, 233, 226, 0.9));
34
+ background: linear-gradient(rgb(var(--bg-rgb) / 0), rgb(var(--bg-rgb) / 0.9));
35
35
  animation: rise 0.5s 0.24s ease-out both;
36
36
  pointer-events: none;
37
37
  }
@@ -53,10 +53,12 @@
53
53
  color: var(--text-accent);
54
54
  }
55
55
 
56
+ /* Inside the Network panel on narrow screens, panel text size applies. */
56
57
  footer.inline {
57
58
  position: static;
58
59
  margin-top: 14px;
59
60
  padding: 0;
61
+ font-size: 12px;
60
62
  background: none;
61
63
  animation: none;
62
64
  pointer-events: auto;
@@ -71,7 +73,6 @@
71
73
 
72
74
  footer.inline {
73
75
  margin-top: 4px;
74
- font-size: 11px;
75
76
  }
76
77
  }
77
78
 
@@ -26,10 +26,10 @@
26
26
  <header bind:this={headerEl}>
27
27
  <div class="brand">
28
28
  <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
29
- <path d="M4 18 L12 6 L20 18" stroke="#b25e00" stroke-width="1.6" fill="none" />
30
- <circle cx="4" cy="18" r="2.4" fill="#b25e00" />
31
- <circle cx="12" cy="6" r="2.4" fill="#20242b" />
32
- <circle cx="20" cy="18" r="2.4" fill="#b25e00" />
29
+ <path class="brand-accent-stroke" d="M4 18 L12 6 L20 18" stroke-width="1.6" fill="none" />
30
+ <circle class="brand-accent" cx="4" cy="18" r="2.4" />
31
+ <circle class="brand-ink" cx="12" cy="6" r="2.4" />
32
+ <circle class="brand-accent" cx="20" cy="18" r="2.4" />
33
33
  </svg>
34
34
  <h1>tellegen</h1>
35
35
  </div>
@@ -130,7 +130,7 @@
130
130
  justify-content: space-between;
131
131
  gap: 16px;
132
132
  padding: 10px 20px;
133
- background: linear-gradient(rgba(236, 233, 226, 0.95), rgba(236, 233, 226, 0));
133
+ background: linear-gradient(rgb(var(--bg-rgb) / 0.95), rgb(var(--bg-rgb) / 0));
134
134
  animation: drop 0.5s ease-out both;
135
135
  }
136
136
 
@@ -141,6 +141,19 @@
141
141
  gap: 10px;
142
142
  }
143
143
 
144
+ /* The brand mark follows the theme: accent legs, ink apex. */
145
+ .brand-accent {
146
+ fill: var(--accent);
147
+ }
148
+
149
+ .brand-accent-stroke {
150
+ stroke: var(--accent);
151
+ }
152
+
153
+ .brand-ink {
154
+ fill: var(--ink);
155
+ }
156
+
144
157
  h1 {
145
158
  margin: 0;
146
159
  font-size: 22px;
@@ -305,8 +318,8 @@
305
318
 
306
319
  /* Ghost chip: standing invitation to drop or pick a case file. */
307
320
  .cases > button.ghost {
308
- background: rgba(252, 251, 247, 0.36);
309
- border: 1px dashed rgba(178, 94, 0, 0.55);
321
+ background: rgb(var(--paper-rgb) / 0.36);
322
+ border: 1px dashed rgb(var(--accent-rgb) / 0.55);
310
323
  color: var(--text-secondary);
311
324
  box-shadow: inset 0 0 0 1px rgba(212, 116, 34, 0.08);
312
325
  }
@@ -351,7 +364,7 @@
351
364
  flex-wrap: wrap;
352
365
  gap: 8px;
353
366
  padding: 8px 10px 12px;
354
- background: linear-gradient(rgba(236, 233, 226, 0.97), rgba(236, 233, 226, 0.72));
367
+ background: linear-gradient(rgb(var(--bg-rgb) / 0.97), rgb(var(--bg-rgb) / 0.72));
355
368
  }
356
369
 
357
370
  .brand {
@@ -59,7 +59,7 @@
59
59
  }
60
60
 
61
61
  .head {
62
- font-size: 10.5px;
62
+ font-size: 12.5px;
63
63
  margin: 0 0 4px;
64
64
  }
65
65
 
@@ -155,7 +155,7 @@
155
155
 
156
156
  label {
157
157
  display: block;
158
- font-size: 10px;
158
+ font-size: 12px;
159
159
  text-transform: uppercase;
160
160
  margin-bottom: 3px;
161
161
  }
@@ -220,7 +220,7 @@
220
220
  background: var(--panel);
221
221
  border: 1px solid var(--line);
222
222
  border-radius: var(--radius-xs, 2px);
223
- box-shadow: 0 4px 18px rgba(32, 36, 43, 0.14);
223
+ box-shadow: var(--elev-menu);
224
224
  }
225
225
 
226
226
  li {
@@ -244,7 +244,7 @@
244
244
  border: 1px solid var(--line);
245
245
  border-radius: var(--radius-xs, 2px);
246
246
  box-shadow: var(--elev-1);
247
- font-size: 10.5px;
247
+ font-size: 12.5px;
248
248
  margin: 4px 0 0;
249
249
  }
250
250
 
@@ -157,9 +157,11 @@
157
157
  {@render selectionBlock()}
158
158
  {/if}
159
159
 
160
- <hr />
160
+ {#if app.busSizing === 'power'}
161
+ <hr />
161
162
 
162
- <SizeLegend />
163
+ <SizeLegend />
164
+ {/if}
163
165
  {/if}
164
166
  {/if}
165
167
  {#if app.compactLayout && config.showFooter}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { getAppState, getController } from '../context.svelte.js';
3
3
  import { fmt, signed } from '../format.js';
4
+ import { sliderKeyValue } from '../slider.js';
4
5
 
5
6
  const app = getAppState();
6
7
  const ctrl = getController();
@@ -37,7 +38,7 @@
37
38
  <span class="val">{signed(ctrl.sliderValue)} MW</span>
38
39
  </div>
39
40
  <div class="range-mode">
40
- <div class="segment mono" aria-label="demand range">
41
+ <div class="segment mono" role="group" aria-label="demand range">
41
42
  <button
42
43
  type="button"
43
44
  class:active={app.demandRangeMode === 'local'}
@@ -51,7 +52,7 @@
51
52
  type="button"
52
53
  class:active={app.demandRangeMode === 'full'}
53
54
  aria-pressed={app.demandRangeMode === 'full'}
54
- aria-label="full demand range"
55
+ aria-label="full range of demand"
55
56
  {disabled}
56
57
  title="range from zero load to the local physical limit"
57
58
  onclick={() => ctrl.setDemandRangeMode('full')}>full range</button
@@ -79,7 +80,15 @@
79
80
  bind:value={ctrl.sliderCurrent, ctrl.setSliderPreview}
80
81
  aria-label="demand delta at selected bus"
81
82
  onpointerdown={() => ctrl.setSliderPreview(ctrl.sliderValue)}
82
- onkeydown={() => ctrl.setSliderPreview(ctrl.sliderValue)}
83
+ onkeydown={(e) => {
84
+ const next = sliderKeyValue(e.key, ctrl.sliderValue, ctrl.sliderMin, ctrl.sliderMax);
85
+ if (next === null) {
86
+ ctrl.setSliderPreview(ctrl.sliderValue);
87
+ return;
88
+ }
89
+ e.preventDefault();
90
+ ctrl.setSliderPreview(next);
91
+ }}
83
92
  onpointerup={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
84
93
  onkeyup={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
85
94
  onchange={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
@@ -120,7 +129,7 @@
120
129
  .slider-head {
121
130
  display: flex;
122
131
  justify-content: space-between;
123
- font-size: 11.5px;
132
+ font-size: 13.5px;
124
133
  color: var(--text-secondary);
125
134
  margin-bottom: 4px;
126
135
  }
@@ -135,7 +144,7 @@
135
144
  justify-content: space-between;
136
145
  gap: 8px;
137
146
  margin: 6px 0 7px;
138
- font-size: 10.5px;
147
+ font-size: 12.5px;
139
148
  }
140
149
 
141
150
  .slider-track {
@@ -256,13 +265,13 @@
256
265
 
257
266
  .pred {
258
267
  margin: 2px 0 0;
259
- font-size: 11px;
268
+ font-size: 13px;
260
269
  min-height: 16px;
261
270
  }
262
271
 
263
272
  .score {
264
273
  margin: 8px 0 0;
265
- font-size: 11px;
274
+ font-size: 13px;
266
275
  color: var(--ink);
267
276
  min-height: 16px;
268
277
  }
@@ -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,9 +359,10 @@
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
- stroke={selectedBus === bus.id ? '#2f6fbb' : '#756a5b'}
365
+ style:stroke={selectedBus === bus.id ? '#2f6fbb' : 'var(--diagram-bus-stroke)'}
354
366
  stroke-width={selectedBus === bus.id ? 3 : 1}
355
367
  vector-effect="non-scaling-stroke"
356
368
  />
@@ -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
  >
@@ -59,10 +59,14 @@
59
59
  </span>
60
60
  {/if}
61
61
  </div>
62
- {:else}
62
+ {:else if ctrl.activeSolvable?.solving}
63
63
  <p class="dim small blink">
64
64
  Solving {formulationLabel(ctrl.activeFormulation)}&hellip;
65
65
  </p>
66
+ {:else}
67
+ <p class="dim small">
68
+ No {formulationLabel(ctrl.activeFormulation)} results available.
69
+ </p>
66
70
  {/if}
67
71
 
68
72
  <style>
@@ -19,7 +19,7 @@
19
19
  inset: 0;
20
20
  z-index: 20;
21
21
  pointer-events: none;
22
- background: rgba(236, 233, 226, 0.75);
22
+ background: rgb(var(--bg-rgb) / 0.75);
23
23
  }
24
24
 
25
25
  .dropframe {
@@ -53,7 +53,7 @@
53
53
 
54
54
  .formulation-row select {
55
55
  font-family: var(--font-mono);
56
- font-size: 11px;
56
+ font-size: 13px;
57
57
  padding: 3px 22px 3px 8px;
58
58
  border: 1px solid var(--line);
59
59
  border-radius: 2px;
@@ -11,6 +11,7 @@
11
11
  const EXPORT_FORMATS = [
12
12
  { token: 'matpower', label: 'MATPOWER (.m)' },
13
13
  { token: 'psse', label: 'PSS/E (.raw)' },
14
+ { token: 'psse35', label: 'PSS/E v35 (.raw)' },
14
15
  { token: 'powerworld', label: 'PowerWorld (.aux)' },
15
16
  { token: 'pslf', label: 'PSLF (.epc)' },
16
17
  { token: 'powermodels-json', label: 'PowerModels (.json)' },
@@ -117,6 +118,8 @@
117
118
  <p class="footnote mono">
118
119
  no coordinates in this file: click the map or drop a geographic file
119
120
  </p>
121
+ {:else if lc.coordsKind === 'synthetic' && lc.summary.coords_method === 'generated'}
122
+ <p class="footnote mono">coordinates: synthetic grid sited on real geography</p>
120
123
  {:else if lc.coordsKind === 'synthetic' || lc.coordsKind === 'manual'}
121
124
  <p class="footnote mono">
122
125
  coordinates: {lc.coordsKind} topology layout centered where it was placed
@@ -222,7 +225,7 @@
222
225
 
223
226
  .footnote {
224
227
  margin: 8px 0 0;
225
- font-size: 10px;
228
+ font-size: 12px;
226
229
  color: var(--text-tertiary);
227
230
  letter-spacing: 0;
228
231
  }
@@ -231,7 +234,7 @@
231
234
  margin: 8px 0 0;
232
235
  padding: 0;
233
236
  list-style: none;
234
- font-size: 10.5px;
237
+ font-size: 12.5px;
235
238
  line-height: 1.5;
236
239
  color: var(--text-accent);
237
240
  }
@@ -254,7 +257,7 @@
254
257
  padding: 4px;
255
258
  list-style: none;
256
259
  min-width: 100%;
257
- background: var(--surface, #fff);
260
+ background: var(--surface, var(--surface-menu));
258
261
  border: 1px solid var(--border, rgba(0, 0, 0, 0.15));
259
262
  border-radius: 6px;
260
263
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);