@tellegen/svelte 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +70 -0
- package/dist/Sparkline.svelte +100 -0
- package/dist/Sparkline.svelte.d.ts +8 -0
- package/dist/Sparkline.svelte.d.ts.map +1 -0
- package/dist/TellegenMap.svelte +1430 -0
- package/dist/TellegenMap.svelte.d.ts +14 -0
- package/dist/TellegenMap.svelte.d.ts.map +1 -0
- package/dist/TellegenProvider.svelte +38 -0
- package/dist/TellegenProvider.svelte.d.ts +9 -0
- package/dist/TellegenProvider.svelte.d.ts.map +1 -0
- package/dist/TellegenShell.svelte +126 -0
- package/dist/TellegenShell.svelte.d.ts +4 -0
- package/dist/TellegenShell.svelte.d.ts.map +1 -0
- package/dist/TellegenViewer.svelte +19 -0
- package/dist/TellegenViewer.svelte.d.ts +4 -0
- package/dist/TellegenViewer.svelte.d.ts.map +1 -0
- package/dist/api.d.ts +44 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +137 -0
- package/dist/colors.d.ts +51 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +140 -0
- package/dist/components/AppFooter.svelte +73 -0
- package/dist/components/AppFooter.svelte.d.ts +19 -0
- package/dist/components/AppFooter.svelte.d.ts.map +1 -0
- package/dist/components/AppHeader.svelte +405 -0
- package/dist/components/AppHeader.svelte.d.ts +4 -0
- package/dist/components/AppHeader.svelte.d.ts.map +1 -0
- package/dist/components/BindingLines.svelte +101 -0
- package/dist/components/BindingLines.svelte.d.ts +4 -0
- package/dist/components/BindingLines.svelte.d.ts.map +1 -0
- package/dist/components/BusPicker.svelte +227 -0
- package/dist/components/BusPicker.svelte.d.ts +4 -0
- package/dist/components/BusPicker.svelte.d.ts.map +1 -0
- package/dist/components/ControlPanel.svelte +154 -0
- package/dist/components/ControlPanel.svelte.d.ts +19 -0
- package/dist/components/ControlPanel.svelte.d.ts.map +1 -0
- package/dist/components/DemandSlider.svelte +271 -0
- package/dist/components/DemandSlider.svelte.d.ts +4 -0
- package/dist/components/DemandSlider.svelte.d.ts.map +1 -0
- package/dist/components/DisplayControls.svelte +81 -0
- package/dist/components/DisplayControls.svelte.d.ts +19 -0
- package/dist/components/DisplayControls.svelte.d.ts.map +1 -0
- package/dist/components/DropZone.svelte +46 -0
- package/dist/components/DropZone.svelte.d.ts +19 -0
- package/dist/components/DropZone.svelte.d.ts.map +1 -0
- package/dist/components/FormulationSelector.svelte +82 -0
- package/dist/components/FormulationSelector.svelte.d.ts +19 -0
- package/dist/components/FormulationSelector.svelte.d.ts.map +1 -0
- package/dist/components/LocalCaseDetails.svelte +253 -0
- package/dist/components/LocalCaseDetails.svelte.d.ts +4 -0
- package/dist/components/LocalCaseDetails.svelte.d.ts.map +1 -0
- package/dist/components/MulticonductorDetails.svelte +280 -0
- package/dist/components/MulticonductorDetails.svelte.d.ts +19 -0
- package/dist/components/MulticonductorDetails.svelte.d.ts.map +1 -0
- package/dist/components/NetworkStats.svelte +75 -0
- package/dist/components/NetworkStats.svelte.d.ts +19 -0
- package/dist/components/NetworkStats.svelte.d.ts.map +1 -0
- package/dist/components/PlacementCue.svelte +35 -0
- package/dist/components/PlacementCue.svelte.d.ts +19 -0
- package/dist/components/PlacementCue.svelte.d.ts.map +1 -0
- package/dist/components/RatingSlider.svelte +273 -0
- package/dist/components/RatingSlider.svelte.d.ts +4 -0
- package/dist/components/RatingSlider.svelte.d.ts.map +1 -0
- package/dist/components/RestoreDefaultsButton.svelte +34 -0
- package/dist/components/RestoreDefaultsButton.svelte.d.ts +19 -0
- package/dist/components/RestoreDefaultsButton.svelte.d.ts.map +1 -0
- package/dist/components/SensitivityReadout.svelte +126 -0
- package/dist/components/SensitivityReadout.svelte.d.ts +4 -0
- package/dist/components/SensitivityReadout.svelte.d.ts.map +1 -0
- package/dist/components/SizeLegend.svelte +55 -0
- package/dist/components/SizeLegend.svelte.d.ts +19 -0
- package/dist/components/SizeLegend.svelte.d.ts.map +1 -0
- package/dist/components/SolveCard.svelte +84 -0
- package/dist/components/SolveCard.svelte.d.ts +4 -0
- package/dist/components/SolveCard.svelte.d.ts.map +1 -0
- package/dist/components/TopMovers.svelte +68 -0
- package/dist/components/TopMovers.svelte.d.ts +19 -0
- package/dist/components/TopMovers.svelte.d.ts.map +1 -0
- package/dist/components.d.ts +19 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +18 -0
- package/dist/context.svelte.d.ts +20 -0
- package/dist/context.svelte.d.ts.map +1 -0
- package/dist/context.svelte.js +43 -0
- package/dist/controller.svelte.d.ts +334 -0
- package/dist/controller.svelte.d.ts.map +1 -0
- package/dist/controller.svelte.js +2026 -0
- package/dist/display.d.ts +31 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +63 -0
- package/dist/drop-classify.d.ts +48 -0
- package/dist/drop-classify.d.ts.map +1 -0
- package/dist/drop-classify.js +92 -0
- package/dist/format.d.ts +30 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +49 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/map.d.ts +9 -0
- package/dist/map.d.ts.map +1 -0
- package/dist/map.js +4 -0
- package/dist/multiconductor.d.ts +63 -0
- package/dist/multiconductor.d.ts.map +1 -0
- package/dist/multiconductor.js +255 -0
- package/dist/state.svelte.d.ts +258 -0
- package/dist/state.svelte.d.ts.map +1 -0
- package/dist/state.svelte.js +323 -0
- package/dist/study-package.d.ts +9 -0
- package/dist/study-package.d.ts.map +1 -0
- package/dist/study-package.js +17 -0
- package/dist/styles.css +393 -0
- package/dist/synthetic-layout.d.ts +12 -0
- package/dist/synthetic-layout.d.ts.map +1 -0
- package/dist/synthetic-layout.js +271 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
onbusclick: (caseId: string, busId: number) => void;
|
|
4
|
+
onlocalbusclick: (caseId: string, busId: number) => void;
|
|
5
|
+
onbranchclick?: (caseId: string, branchId: number) => void;
|
|
6
|
+
onlocalbranchclick?: (caseId: string, branchId: number) => void;
|
|
7
|
+
onmultibusclick: (caseId: string, busId: string) => void;
|
|
8
|
+
onplacecase: (lon: number, lat: number) => void;
|
|
9
|
+
onmapclick: () => void;
|
|
10
|
+
};
|
|
11
|
+
declare const TellegenMap: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type TellegenMap = ReturnType<typeof TellegenMap>;
|
|
13
|
+
export default TellegenMap;
|
|
14
|
+
//# sourceMappingURL=TellegenMap.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TellegenMap.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenMap.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,kCAAkC,CAAC;AAgEzC,KAAK,gBAAgB,GAAI;IACxB,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAkmCH,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack, type Snippet } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
resolveTellegenUiConfig,
|
|
5
|
+
setAppState,
|
|
6
|
+
setController,
|
|
7
|
+
setUiConfig,
|
|
8
|
+
type TellegenUiOptions
|
|
9
|
+
} from './context.svelte.js';
|
|
10
|
+
import { createController } from './controller.svelte.js';
|
|
11
|
+
import { createAppState } from './state.svelte.js';
|
|
12
|
+
|
|
13
|
+
interface Props extends TellegenUiOptions {
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { apiBase, loadDefaultCases, docsHref, orgHref, orgLabel, showFooter, children }: Props =
|
|
18
|
+
$props();
|
|
19
|
+
|
|
20
|
+
const config = untrack(() =>
|
|
21
|
+
resolveTellegenUiConfig({
|
|
22
|
+
apiBase,
|
|
23
|
+
loadDefaultCases,
|
|
24
|
+
docsHref,
|
|
25
|
+
orgHref,
|
|
26
|
+
orgLabel,
|
|
27
|
+
showFooter
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
const app = createAppState();
|
|
31
|
+
const ctrl = createController(app, { apiBase: config.apiBase });
|
|
32
|
+
|
|
33
|
+
setUiConfig(config);
|
|
34
|
+
setAppState(app);
|
|
35
|
+
setController(ctrl);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
{@render children?.()}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import { type TellegenUiOptions } from './context.svelte.js';
|
|
3
|
+
interface Props extends TellegenUiOptions {
|
|
4
|
+
children?: Snippet;
|
|
5
|
+
}
|
|
6
|
+
declare const TellegenProvider: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type TellegenProvider = ReturnType<typeof TellegenProvider>;
|
|
8
|
+
export default TellegenProvider;
|
|
9
|
+
//# sourceMappingURL=TellegenProvider.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TellegenProvider.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenProvider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAKL,KAAK,iBAAiB,EACtB,MAAM,qBAAqB,CAAC;AAK7B,UAAU,KAAM,SAAQ,iBAAiB;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAiCF,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import AppFooter from './components/AppFooter.svelte';
|
|
4
|
+
import AppHeader from './components/AppHeader.svelte';
|
|
5
|
+
import BusPicker from './components/BusPicker.svelte';
|
|
6
|
+
import ControlPanel from './components/ControlPanel.svelte';
|
|
7
|
+
import DropZone from './components/DropZone.svelte';
|
|
8
|
+
import PlacementCue from './components/PlacementCue.svelte';
|
|
9
|
+
import RestoreDefaultsButton from './components/RestoreDefaultsButton.svelte';
|
|
10
|
+
import SolveCard from './components/SolveCard.svelte';
|
|
11
|
+
import { getAppState, getController, getUiConfig } from './context.svelte.js';
|
|
12
|
+
import TellegenMap from './TellegenMap.svelte';
|
|
13
|
+
|
|
14
|
+
const FILE_DROP_QUERY = '(hover: hover) and (pointer: fine) and (min-width: 761px)';
|
|
15
|
+
|
|
16
|
+
const app = getAppState();
|
|
17
|
+
const ctrl = getController();
|
|
18
|
+
const config = getUiConfig();
|
|
19
|
+
|
|
20
|
+
let dragDepth = 0;
|
|
21
|
+
|
|
22
|
+
$effect(() => {
|
|
23
|
+
let hasDisplayMode = false;
|
|
24
|
+
for (const option of ctrl.displayOptions) {
|
|
25
|
+
if (option.mode === app.displayMode) {
|
|
26
|
+
hasDisplayMode = true;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (ctrl.displayOptions.length > 0 && !hasDisplayMode) {
|
|
31
|
+
app.displayMode = 'lmp';
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
onMount(() => {
|
|
36
|
+
if (config.loadDefaultCases) {
|
|
37
|
+
if (!ctrl.casesLoaded) void ctrl.load();
|
|
38
|
+
} else {
|
|
39
|
+
ctrl.casesLoaded = true;
|
|
40
|
+
app.requestFrame('all');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const query = window.matchMedia(FILE_DROP_QUERY);
|
|
44
|
+
const syncFileDropUi = () => {
|
|
45
|
+
ctrl.showFileDropUi = query.matches;
|
|
46
|
+
if (!ctrl.showFileDropUi) {
|
|
47
|
+
dragDepth = 0;
|
|
48
|
+
app.dragOver = false;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
syncFileDropUi();
|
|
52
|
+
query.addEventListener('change', syncFileDropUi);
|
|
53
|
+
return () => query.removeEventListener('change', syncFileDropUi);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function dragHasFiles(e: DragEvent): boolean {
|
|
57
|
+
return ctrl.showFileDropUi && (e.dataTransfer?.types.includes('Files') ?? false);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onDragEnter(e: DragEvent) {
|
|
61
|
+
if (!dragHasFiles(e)) return;
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
dragDepth++;
|
|
64
|
+
app.dragOver = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onDragLeave(e: DragEvent) {
|
|
68
|
+
if (!dragHasFiles(e)) return;
|
|
69
|
+
dragDepth = Math.max(0, dragDepth - 1);
|
|
70
|
+
if (dragDepth === 0) app.dragOver = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onDragOver(e: DragEvent) {
|
|
74
|
+
if (!dragHasFiles(e)) return;
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onDrop(e: DragEvent) {
|
|
79
|
+
if (!dragHasFiles(e)) return;
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
dragDepth = 0;
|
|
82
|
+
app.dragOver = false;
|
|
83
|
+
if (e.dataTransfer) ctrl.ingestFiles(e.dataTransfer.files);
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<svelte:window
|
|
88
|
+
onkeydown={(e) => {
|
|
89
|
+
if (e.key === 'Escape') ctrl.clearSelection();
|
|
90
|
+
}}
|
|
91
|
+
ondragenter={onDragEnter}
|
|
92
|
+
ondragleave={onDragLeave}
|
|
93
|
+
ondragover={onDragOver}
|
|
94
|
+
ondrop={onDrop}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<main>
|
|
98
|
+
<TellegenMap
|
|
99
|
+
onbusclick={ctrl.selectBus}
|
|
100
|
+
onlocalbusclick={ctrl.selectLocalBus}
|
|
101
|
+
onbranchclick={ctrl.selectBranch}
|
|
102
|
+
onlocalbranchclick={ctrl.selectLocalBranch}
|
|
103
|
+
onmultibusclick={ctrl.selectMultiBus}
|
|
104
|
+
onplacecase={ctrl.placeCase}
|
|
105
|
+
onmapclick={ctrl.clearSelection}
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
<AppHeader />
|
|
109
|
+
<ControlPanel />
|
|
110
|
+
<SolveCard />
|
|
111
|
+
<BusPicker />
|
|
112
|
+
<DropZone />
|
|
113
|
+
<PlacementCue />
|
|
114
|
+
<RestoreDefaultsButton />
|
|
115
|
+
{#if config.showFooter}
|
|
116
|
+
<AppFooter />
|
|
117
|
+
{/if}
|
|
118
|
+
</main>
|
|
119
|
+
|
|
120
|
+
<style>
|
|
121
|
+
main {
|
|
122
|
+
position: fixed;
|
|
123
|
+
inset: 0;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TellegenShell.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenShell.svelte.ts"],"names":[],"mappings":"AA4HA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import TellegenProvider from './TellegenProvider.svelte';
|
|
3
|
+
import TellegenShell from './TellegenShell.svelte';
|
|
4
|
+
import type { TellegenUiOptions } from './context.svelte.js';
|
|
5
|
+
|
|
6
|
+
let { apiBase, loadDefaultCases, docsHref, orgHref, orgLabel, showFooter }: TellegenUiOptions =
|
|
7
|
+
$props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<TellegenProvider
|
|
11
|
+
{apiBase}
|
|
12
|
+
{loadDefaultCases}
|
|
13
|
+
{docsHref}
|
|
14
|
+
{orgHref}
|
|
15
|
+
{orgLabel}
|
|
16
|
+
{showFooter}
|
|
17
|
+
>
|
|
18
|
+
<TellegenShell />
|
|
19
|
+
</TellegenProvider>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TellegenViewer.svelte.d.ts","sourceRoot":"","sources":["../src/lib/TellegenViewer.svelte.ts"],"names":[],"mappings":"AAsBA,QAAA,MAAM,cAAc,6FAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { CaseSummary, DemandDeltas, Network, SensitivityColumn, Solution, SolveIteration } from '@tellegen/engine';
|
|
2
|
+
export type { BranchRatingDeltas, CaseSummary, DemandDeltas, Network, NetworkBranch, NetworkBus, SensitivityColumn, Solution, SolveIteration } from '@tellegen/engine';
|
|
3
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
4
|
+
export interface TellegenApiClientOptions {
|
|
5
|
+
apiBase?: string;
|
|
6
|
+
fetch?: FetchLike;
|
|
7
|
+
EventSource?: typeof EventSource;
|
|
8
|
+
}
|
|
9
|
+
export interface TellegenApiClient {
|
|
10
|
+
getCases(): Promise<CaseSummary[]>;
|
|
11
|
+
getNetwork(caseId: string): Promise<Network>;
|
|
12
|
+
getCaseNetworkJson(caseId: string, signal?: AbortSignal): Promise<string>;
|
|
13
|
+
getSolution(caseId: string): Promise<Solution>;
|
|
14
|
+
getSensitivity(caseId: string, bus: number, deltas?: DemandDeltas, signal?: AbortSignal): Promise<SensitivityColumn>;
|
|
15
|
+
getBranchSensitivity(caseId: string, branch: number, deltas?: DemandDeltas, signal?: AbortSignal): Promise<SensitivityColumn>;
|
|
16
|
+
/** Whether the server's compute endpoints (solve stream, sensitivities) are
|
|
17
|
+
* enabled on this deploy, so the frontend can pick honest fallback copy and
|
|
18
|
+
* skip doomed requests. */
|
|
19
|
+
getComputeStatus(signal?: AbortSignal): Promise<{
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
openSolveStream(caseId: string, deltas: DemandDeltas, sensBus: number | null, handlers: SolveStreamHandlers): () => void;
|
|
23
|
+
}
|
|
24
|
+
/** A failed API request. `status` is the HTTP status, or 0 for a network
|
|
25
|
+
* failure (the request never completed). `serverMessage` is the backend's
|
|
26
|
+
* own `{"error": ...}` copy when the response carried one; edge-proxy
|
|
27
|
+
* rejections (e.g. Caddy 429s) have none. */
|
|
28
|
+
export declare class ApiError extends Error {
|
|
29
|
+
status: number;
|
|
30
|
+
serverMessage: string | null;
|
|
31
|
+
constructor(message: string, status: number, serverMessage: string | null);
|
|
32
|
+
}
|
|
33
|
+
export interface SolveStreamHandlers {
|
|
34
|
+
onsolution?: (sol: Solution & {
|
|
35
|
+
case: string;
|
|
36
|
+
solve_ms: number;
|
|
37
|
+
iterations: SolveIteration[];
|
|
38
|
+
}) => void;
|
|
39
|
+
onsensitivity?: (col: SensitivityColumn) => void;
|
|
40
|
+
onfail?: (message: string) => void;
|
|
41
|
+
ondone?: () => void;
|
|
42
|
+
}
|
|
43
|
+
export declare function createApiClient(options?: TellegenApiClientOptions): TellegenApiClient;
|
|
44
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/lib/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,WAAW,EACX,YAAY,EACZ,OAAO,EACP,iBAAiB,EACjB,QAAQ,EACR,cAAc,EACd,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EACX,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,OAAO,EACP,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,QAAQ,EACR,cAAc,EACd,MAAM,kBAAkB,CAAC;AAE1B,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE1E,MAAM,WAAW,wBAAwB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,WAAW,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACnC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1E,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/C,cAAc,CACb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,YAAY,EACrB,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B,oBAAoB,CACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,EACrB,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B;;+BAE2B;IAC3B,gBAAgB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtE,eAAe,CACd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,QAAQ,EAAE,mBAAmB,GAC3B,MAAM,IAAI,CAAC;CACd;AAwBD;;;6CAG6C;AAC7C,qBAAa,QAAS,SAAQ,KAAK;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;CAMzE;AA6CD,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,CACZ,GAAG,EAAE,QAAQ,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,cAAc,EAAE,CAAA;KAAE,KAC5E,IAAI,CAAC;IACV,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,wBAA6B,GAAG,iBAAiB,CAyEzF"}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
function encodeDeltas(deltas) {
|
|
2
|
+
return Object.entries(deltas)
|
|
3
|
+
.filter(([, mw]) => mw !== 0)
|
|
4
|
+
.map(([bus, mw]) => `${bus}:${mw.toFixed(2)}`)
|
|
5
|
+
.join(',');
|
|
6
|
+
}
|
|
7
|
+
function apiPath(apiBase, path) {
|
|
8
|
+
const base = apiBase.replace(/\/+$/, '');
|
|
9
|
+
return `${base}${path}`;
|
|
10
|
+
}
|
|
11
|
+
function defaultFetch() {
|
|
12
|
+
if (typeof fetch === 'undefined')
|
|
13
|
+
throw new Error('fetch is not available');
|
|
14
|
+
return fetch.bind(globalThis);
|
|
15
|
+
}
|
|
16
|
+
function defaultEventSource() {
|
|
17
|
+
if (typeof EventSource === 'undefined')
|
|
18
|
+
throw new Error('EventSource is not available');
|
|
19
|
+
return EventSource;
|
|
20
|
+
}
|
|
21
|
+
/** A failed API request. `status` is the HTTP status, or 0 for a network
|
|
22
|
+
* failure (the request never completed). `serverMessage` is the backend's
|
|
23
|
+
* own `{"error": ...}` copy when the response carried one; edge-proxy
|
|
24
|
+
* rejections (e.g. Caddy 429s) have none. */
|
|
25
|
+
export class ApiError extends Error {
|
|
26
|
+
status;
|
|
27
|
+
serverMessage;
|
|
28
|
+
constructor(message, status, serverMessage) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'ApiError';
|
|
31
|
+
this.status = status;
|
|
32
|
+
this.serverMessage = serverMessage;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function userMessage(status, serverMessage) {
|
|
36
|
+
if (status === 0)
|
|
37
|
+
return 'server unreachable, check your connection';
|
|
38
|
+
if (status === 429)
|
|
39
|
+
return 'rate limited; wait a few seconds and try again';
|
|
40
|
+
if (status >= 500)
|
|
41
|
+
return 'server error, try again';
|
|
42
|
+
return serverMessage ?? `request failed (HTTP ${status})`;
|
|
43
|
+
}
|
|
44
|
+
async function checkedFetch(fetchImpl, url, signal) {
|
|
45
|
+
let res;
|
|
46
|
+
try {
|
|
47
|
+
res = await fetchImpl(url, { signal });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
// Aborts must pass through unchanged: the controller filters them by
|
|
51
|
+
// DOMException to distinguish cancellation from failure.
|
|
52
|
+
if (e instanceof DOMException)
|
|
53
|
+
throw e;
|
|
54
|
+
throw new ApiError(userMessage(0, null), 0, null);
|
|
55
|
+
}
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
let serverMessage = null;
|
|
58
|
+
try {
|
|
59
|
+
const body = (await res.json());
|
|
60
|
+
if (typeof body.error === 'string')
|
|
61
|
+
serverMessage = body.error;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// non-JSON body (edge-proxy rejections); keep serverMessage null
|
|
65
|
+
}
|
|
66
|
+
throw new ApiError(userMessage(res.status, serverMessage), res.status, serverMessage);
|
|
67
|
+
}
|
|
68
|
+
return res;
|
|
69
|
+
}
|
|
70
|
+
async function getJson(fetchImpl, url, signal) {
|
|
71
|
+
const res = await checkedFetch(fetchImpl, url, signal);
|
|
72
|
+
return res.json();
|
|
73
|
+
}
|
|
74
|
+
export function createApiClient(options = {}) {
|
|
75
|
+
const apiBase = options.apiBase ?? '/api';
|
|
76
|
+
const fetchImpl = options.fetch ?? defaultFetch();
|
|
77
|
+
return {
|
|
78
|
+
getCases: () => getJson(fetchImpl, apiPath(apiBase, '/cases')),
|
|
79
|
+
getNetwork: (caseId) => getJson(fetchImpl, apiPath(apiBase, `/cases/${caseId}/network`)),
|
|
80
|
+
async getCaseNetworkJson(caseId, signal) {
|
|
81
|
+
const url = apiPath(apiBase, `/cases/${caseId}/case`);
|
|
82
|
+
const res = await checkedFetch(fetchImpl, url, signal);
|
|
83
|
+
return res.text();
|
|
84
|
+
},
|
|
85
|
+
getSolution: (caseId) => getJson(fetchImpl, apiPath(apiBase, `/cases/${caseId}/solution`)),
|
|
86
|
+
getSensitivity(caseId, bus, deltas = {}, signal) {
|
|
87
|
+
const d = encodeDeltas(deltas);
|
|
88
|
+
const query = d ? `?d=${encodeURIComponent(d)}` : '';
|
|
89
|
+
return getJson(fetchImpl, apiPath(apiBase, `/cases/${caseId}/sensitivity/lmp/d/${bus}${query}`), signal);
|
|
90
|
+
},
|
|
91
|
+
getBranchSensitivity(caseId, branch, deltas = {}, signal) {
|
|
92
|
+
const d = encodeDeltas(deltas);
|
|
93
|
+
const query = d ? `?d=${encodeURIComponent(d)}` : '';
|
|
94
|
+
return getJson(fetchImpl, apiPath(apiBase, `/cases/${caseId}/sensitivity/lmp/fmax/${branch}${query}`), signal);
|
|
95
|
+
},
|
|
96
|
+
getComputeStatus(signal) {
|
|
97
|
+
return getJson(fetchImpl, apiPath(apiBase, `/compute`), signal);
|
|
98
|
+
},
|
|
99
|
+
openSolveStream(caseId, deltas, sensBus, handlers) {
|
|
100
|
+
const EventSourceImpl = options.EventSource ?? defaultEventSource();
|
|
101
|
+
const params = new URLSearchParams();
|
|
102
|
+
const d = encodeDeltas(deltas);
|
|
103
|
+
if (d)
|
|
104
|
+
params.set('d', d);
|
|
105
|
+
if (sensBus !== null)
|
|
106
|
+
params.set('sens', String(sensBus));
|
|
107
|
+
const qs = params.toString();
|
|
108
|
+
const es = new EventSourceImpl(apiPath(apiBase, `/cases/${caseId}/solve${qs ? `?${qs}` : ''}`));
|
|
109
|
+
let finished = false;
|
|
110
|
+
const finish = () => {
|
|
111
|
+
finished = true;
|
|
112
|
+
es.close();
|
|
113
|
+
};
|
|
114
|
+
es.addEventListener('solution', (e) => handlers.onsolution?.(JSON.parse(e.data)));
|
|
115
|
+
es.addEventListener('sensitivity', (e) => handlers.onsensitivity?.(JSON.parse(e.data)));
|
|
116
|
+
es.addEventListener('fail', (e) => {
|
|
117
|
+
finish();
|
|
118
|
+
handlers.onfail?.(JSON.parse(e.data).error ?? 'solve failed');
|
|
119
|
+
});
|
|
120
|
+
es.addEventListener('done', () => {
|
|
121
|
+
finish();
|
|
122
|
+
handlers.ondone?.();
|
|
123
|
+
});
|
|
124
|
+
es.onerror = () => {
|
|
125
|
+
// EventSource auto-reconnects; a closed-by-server stream after "done"
|
|
126
|
+
// never reaches here because finish() already ran.
|
|
127
|
+
if (!finished) {
|
|
128
|
+
finish();
|
|
129
|
+
// EventSource exposes no status code, so this covers both a lost
|
|
130
|
+
// connection and a rate-limited stream open.
|
|
131
|
+
handlers.onfail?.('solve stream interrupted; server busy or rate limited, try again in a few seconds');
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return finish;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
package/dist/colors.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { DisplayMode } from './state.svelte.js';
|
|
2
|
+
export type RGBA = [number, number, number, number];
|
|
3
|
+
export interface SensitivityDomain {
|
|
4
|
+
min: number;
|
|
5
|
+
max: number;
|
|
6
|
+
mean: number;
|
|
7
|
+
absMax: number;
|
|
8
|
+
scale: number;
|
|
9
|
+
flat: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const lmpColor: (t: number) => RGBA;
|
|
12
|
+
export declare const lmpGradient: string;
|
|
13
|
+
/** Trimmed LMP color domain: 5th to 95th percentile, with a $1/MWh
|
|
14
|
+
* minimum span. LMP distributions are heavy tailed (one binding line pins a
|
|
15
|
+
* handful of buses far from the pack) so min-max scaling compresses the pack
|
|
16
|
+
* into a single hue, and a congestion-free case is one price plus solver
|
|
17
|
+
* noise that an exact scale would stretch into fake structure. Values beyond
|
|
18
|
+
* the domain clamp to the ramp ends. */
|
|
19
|
+
export declare function lmpDomain(values: number[]): {
|
|
20
|
+
lo: number;
|
|
21
|
+
hi: number;
|
|
22
|
+
};
|
|
23
|
+
/** Center a scalar display variable (bus voltage angle, |V|) on a symmetric domain
|
|
24
|
+
* with a per-mode minimum span; LMP keeps its own robust quantile domain. Shared by
|
|
25
|
+
* the control panel readout and the map color scale. */
|
|
26
|
+
export declare function scalarDomain(mode: DisplayMode, values: number[]): {
|
|
27
|
+
lo: number;
|
|
28
|
+
hi: number;
|
|
29
|
+
};
|
|
30
|
+
/** t in [-1, 1] */
|
|
31
|
+
export declare const sensColor: (t: number) => RGBA;
|
|
32
|
+
export declare const sensGradient: string;
|
|
33
|
+
export declare const sensNeutral: RGBA;
|
|
34
|
+
export declare const busNeutral: RGBA;
|
|
35
|
+
export declare function sensFlatColor(domain: Pick<SensitivityDomain, 'mean'>): RGBA;
|
|
36
|
+
/** The fixed normalization for a slider drag: predicted ΔLMP divides by
|
|
37
|
+
* scale × maxAbsStep, so color intensity is linear in the step and saturates at
|
|
38
|
+
* full deflection for the column's robust-max buses. A per-frame domain over
|
|
39
|
+
* column × step would cancel the step exactly (sensitivityDomain is degree-1
|
|
40
|
+
* homogeneous), freezing the intensity. Null for flat columns, whose scale is
|
|
41
|
+
* meaningless. */
|
|
42
|
+
export declare function previewScaleFor(domain: Pick<SensitivityDomain, 'scale' | 'flat'>, maxAbsStep: number): number | null;
|
|
43
|
+
/** Zero anchored sensitivity domain with a flat column guard. */
|
|
44
|
+
export declare function sensitivityDomain(values: number[]): SensitivityDomain;
|
|
45
|
+
/** Branch color by loading fraction: warm gray, amber past 0.6, red past 0.9. */
|
|
46
|
+
export declare function branchColor(loading: number, inService: boolean): RGBA;
|
|
47
|
+
export declare function branchWidth(loading: number): number;
|
|
48
|
+
/** Bus radius in px; area tracks max(load, gen) so big plants and big loads
|
|
49
|
+
* both read at a glance. Shared by the map layers and the size legend. */
|
|
50
|
+
export declare function busRadius(maxMw: number): number;
|
|
51
|
+
//# sourceMappingURL=colors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../src/lib/colors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACd;AAqCD,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,KAAG,IAA0B,CAAC;AAChE,eAAO,MAAM,WAAW,QAAyB,CAAC;AAElD;;;;;wCAKwC;AACxC,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAStE;AAED;;wDAEwD;AACxD,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAQ5F;AAcD,mBAAmB;AACnB,eAAO,MAAM,SAAS,GAAI,GAAG,MAAM,KAAG,IAAqC,CAAC;AAC5E,eAAO,MAAM,YAAY,QAA0B,CAAC;AACpD,eAAO,MAAM,WAAW,EAAE,IAAoB,CAAC;AAC/C,eAAO,MAAM,UAAU,EAAE,IAA2B,CAAC;AAIrD,wBAAgB,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG,IAAI,CAG3E;AAED;;;;;kBAKkB;AAClB,wBAAgB,eAAe,CAC9B,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,MAAM,CAAC,EACjD,UAAU,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAGf;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAuBrE;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAMrE;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;0EAC0E;AAC1E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
|
package/dist/colors.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { extent } from './format.js';
|
|
2
|
+
function lerp(a, b, t) {
|
|
3
|
+
return a + (b - a) * t;
|
|
4
|
+
}
|
|
5
|
+
function lerpColor(a, b, t) {
|
|
6
|
+
return [
|
|
7
|
+
Math.round(lerp(a[0], b[0], t)),
|
|
8
|
+
Math.round(lerp(a[1], b[1], t)),
|
|
9
|
+
Math.round(lerp(a[2], b[2], t)),
|
|
10
|
+
Math.round(lerp(a[3], b[3], t))
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
function ramp(stops, t) {
|
|
14
|
+
const x = Math.min(1, Math.max(0, t)) * (stops.length - 1);
|
|
15
|
+
const i = Math.min(stops.length - 2, Math.floor(x));
|
|
16
|
+
return lerpColor(stops[i], stops[i + 1], x - i);
|
|
17
|
+
}
|
|
18
|
+
function cssGradient(stops) {
|
|
19
|
+
const parts = stops.map((s) => `rgb(${s[0]} ${s[1]} ${s[2]})`);
|
|
20
|
+
return `linear-gradient(90deg, ${parts.join(', ')})`;
|
|
21
|
+
}
|
|
22
|
+
/** Sequential LMP ramp, cheap to expensive: pale straw darkening through
|
|
23
|
+
* amber and rust to near-black maroon. Lightness falls monotonically, so the
|
|
24
|
+
* ordering survives every kind of color vision deficiency. */
|
|
25
|
+
const LMP_STOPS = [
|
|
26
|
+
[246, 227, 180, 235],
|
|
27
|
+
[236, 178, 66, 235],
|
|
28
|
+
[212, 116, 34, 240],
|
|
29
|
+
[160, 56, 21, 245],
|
|
30
|
+
[84, 18, 7, 250]
|
|
31
|
+
];
|
|
32
|
+
export const lmpColor = (t) => ramp(LMP_STOPS, t);
|
|
33
|
+
export const lmpGradient = cssGradient(LMP_STOPS);
|
|
34
|
+
/** Trimmed LMP color domain: 5th to 95th percentile, with a $1/MWh
|
|
35
|
+
* minimum span. LMP distributions are heavy tailed (one binding line pins a
|
|
36
|
+
* handful of buses far from the pack) so min-max scaling compresses the pack
|
|
37
|
+
* into a single hue, and a congestion-free case is one price plus solver
|
|
38
|
+
* noise that an exact scale would stretch into fake structure. Values beyond
|
|
39
|
+
* the domain clamp to the ramp ends. */
|
|
40
|
+
export function lmpDomain(values) {
|
|
41
|
+
if (values.length === 0)
|
|
42
|
+
return { lo: 0, hi: 1 };
|
|
43
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
44
|
+
const q = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
|
|
45
|
+
const lo = q(0.05);
|
|
46
|
+
const hi = q(0.95);
|
|
47
|
+
const mid = (lo + hi) / 2;
|
|
48
|
+
const span = Math.max(hi - lo, 1);
|
|
49
|
+
return { lo: mid - span / 2, hi: mid + span / 2 };
|
|
50
|
+
}
|
|
51
|
+
/** Center a scalar display variable (bus voltage angle, |V|) on a symmetric domain
|
|
52
|
+
* with a per-mode minimum span; LMP keeps its own robust quantile domain. Shared by
|
|
53
|
+
* the control panel readout and the map color scale. */
|
|
54
|
+
export function scalarDomain(mode, values) {
|
|
55
|
+
if (mode === 'lmp')
|
|
56
|
+
return lmpDomain(values);
|
|
57
|
+
if (values.length === 0)
|
|
58
|
+
return { lo: 0, hi: 1 };
|
|
59
|
+
const { min: rawLo, max: rawHi } = extent(values);
|
|
60
|
+
const minSpan = mode === 'voltage' ? 0.02 : 0.04;
|
|
61
|
+
const span = Math.max(rawHi - rawLo, minSpan);
|
|
62
|
+
const mid = (rawLo + rawHi) / 2;
|
|
63
|
+
return { lo: mid - span / 2, hi: mid + span / 2 };
|
|
64
|
+
}
|
|
65
|
+
/** Diverging sensitivity ramp: green (price falls per MW of demand) through
|
|
66
|
+
* a paper-toned neutral to purple (price rises). The green/purple pair stays
|
|
67
|
+
* legible under CVD and shares no hue with the warm LMP ramp, so the two
|
|
68
|
+
* modes cannot be confused. */
|
|
69
|
+
const SENS_STOPS = [
|
|
70
|
+
[27, 120, 55, 240],
|
|
71
|
+
[127, 191, 123, 230],
|
|
72
|
+
[223, 219, 208, 160],
|
|
73
|
+
[194, 165, 207, 230],
|
|
74
|
+
[118, 42, 131, 240]
|
|
75
|
+
];
|
|
76
|
+
/** t in [-1, 1] */
|
|
77
|
+
export const sensColor = (t) => ramp(SENS_STOPS, (t + 1) / 2);
|
|
78
|
+
export const sensGradient = cssGradient(SENS_STOPS);
|
|
79
|
+
export const sensNeutral = SENS_STOPS[2];
|
|
80
|
+
export const busNeutral = [180, 175, 165, 200];
|
|
81
|
+
const FLAT_SENSITIVITY_TINT = 0.55;
|
|
82
|
+
export function sensFlatColor(domain) {
|
|
83
|
+
if (domain.mean === 0)
|
|
84
|
+
return sensNeutral;
|
|
85
|
+
return sensColor(domain.mean > 0 ? FLAT_SENSITIVITY_TINT : -FLAT_SENSITIVITY_TINT);
|
|
86
|
+
}
|
|
87
|
+
/** The fixed normalization for a slider drag: predicted ΔLMP divides by
|
|
88
|
+
* scale × maxAbsStep, so color intensity is linear in the step and saturates at
|
|
89
|
+
* full deflection for the column's robust-max buses. A per-frame domain over
|
|
90
|
+
* column × step would cancel the step exactly (sensitivityDomain is degree-1
|
|
91
|
+
* homogeneous), freezing the intensity. Null for flat columns, whose scale is
|
|
92
|
+
* meaningless. */
|
|
93
|
+
export function previewScaleFor(domain, maxAbsStep) {
|
|
94
|
+
if (domain.flat)
|
|
95
|
+
return null;
|
|
96
|
+
return domain.scale * Math.max(maxAbsStep, 1e-9);
|
|
97
|
+
}
|
|
98
|
+
/** Zero anchored sensitivity domain with a flat column guard. */
|
|
99
|
+
export function sensitivityDomain(values) {
|
|
100
|
+
if (values.length === 0) {
|
|
101
|
+
return { min: 0, max: 0, mean: 0, absMax: 0, scale: 1, flat: true };
|
|
102
|
+
}
|
|
103
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
104
|
+
const q = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
|
|
105
|
+
const min = sorted[0];
|
|
106
|
+
const max = sorted[sorted.length - 1];
|
|
107
|
+
const mean = values.reduce((acc, v) => acc + v, 0) / values.length;
|
|
108
|
+
// The largest absolute value sits at one sorted extreme; computing it from the
|
|
109
|
+
// endpoints avoids spreading one argument per bus into Math.max (a RangeError on
|
|
110
|
+
// very large cases).
|
|
111
|
+
const absMax = Math.max(Math.abs(min), Math.abs(max));
|
|
112
|
+
const robust = Math.max(Math.abs(q(0.01)), Math.abs(q(0.99)));
|
|
113
|
+
const flat = max - min <= Math.max(1e-7, 0.01 * absMax);
|
|
114
|
+
return {
|
|
115
|
+
min,
|
|
116
|
+
max,
|
|
117
|
+
mean,
|
|
118
|
+
absMax,
|
|
119
|
+
scale: Math.max(robust, 1e-12),
|
|
120
|
+
flat
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Branch color by loading fraction: warm gray, amber past 0.6, red past 0.9. */
|
|
124
|
+
export function branchColor(loading, inService) {
|
|
125
|
+
if (!inService)
|
|
126
|
+
return [197, 191, 178, 110];
|
|
127
|
+
if (loading < 0.6)
|
|
128
|
+
return [138, 131, 117, 200];
|
|
129
|
+
if (loading < 0.9)
|
|
130
|
+
return lerpColor([138, 131, 117, 210], [212, 116, 34, 240], (loading - 0.6) / 0.3);
|
|
131
|
+
return [179, 38, 30, 250];
|
|
132
|
+
}
|
|
133
|
+
export function branchWidth(loading) {
|
|
134
|
+
return 1.6 + 3.4 * Math.min(1, loading);
|
|
135
|
+
}
|
|
136
|
+
/** Bus radius in px; area tracks max(load, gen) so big plants and big loads
|
|
137
|
+
* both read at a glance. Shared by the map layers and the size legend. */
|
|
138
|
+
export function busRadius(maxMw) {
|
|
139
|
+
return 3.2 + 0.45 * Math.sqrt(Math.max(maxMw, 1));
|
|
140
|
+
}
|