@elaraai/east-ui 1.0.68 → 1.0.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/src/charts/chart/index.d.ts +39 -4
- package/dist/src/charts/chart/index.d.ts.map +1 -1
- package/dist/src/charts/chart/index.js +3 -3
- package/dist/src/charts/chart/index.js.map +1 -1
- package/dist/src/charts/spec/index.d.ts +61 -3
- package/dist/src/charts/spec/index.d.ts.map +1 -1
- package/dist/src/charts/spec/types.d.ts +1 -1
- package/dist/src/charts/spec/types.js +1 -1
- package/dist/src/collections/blend/index.d.ts +1 -1
- package/dist/src/collections/blend/index.d.ts.map +1 -1
- package/dist/src/collections/blend/index.js +1 -1
- package/dist/src/collections/blend/index.js.map +1 -1
- package/dist/src/collections/blend/types.js +1 -1
- package/dist/src/collections/blend/types.js.map +1 -1
- package/dist/src/collections/board/index.d.ts +4 -4
- package/dist/src/collections/board/index.d.ts.map +1 -1
- package/dist/src/collections/board/index.js +1 -1
- package/dist/src/collections/board/index.js.map +1 -1
- package/dist/src/collections/board/types.js +1 -1
- package/dist/src/collections/board/types.js.map +1 -1
- package/dist/src/collections/calendar/index.d.ts +1 -1
- package/dist/src/collections/calendar/index.d.ts.map +1 -1
- package/dist/src/collections/deck/index.d.ts +58 -0
- package/dist/src/collections/deck/index.d.ts.map +1 -1
- package/dist/src/collections/index.d.ts +1 -2
- package/dist/src/collections/index.d.ts.map +1 -1
- package/dist/src/collections/index.js +1 -2
- package/dist/src/collections/index.js.map +1 -1
- package/dist/src/collections/library/index.d.ts +29 -0
- package/dist/src/collections/library/index.d.ts.map +1 -1
- package/dist/src/collections/library/types.d.ts +29 -0
- package/dist/src/collections/library/types.d.ts.map +1 -1
- package/dist/src/collections/matrix/index.d.ts +1 -1
- package/dist/src/collections/matrix/index.d.ts.map +1 -1
- package/dist/src/collections/plan/assemble.d.ts +3732 -0
- package/dist/src/collections/plan/assemble.d.ts.map +1 -0
- package/dist/src/collections/plan/assemble.js +187 -0
- package/dist/src/collections/plan/assemble.js.map +1 -0
- package/dist/src/collections/plan/builders.d.ts +612 -0
- package/dist/src/collections/plan/builders.d.ts.map +1 -0
- package/dist/src/collections/plan/builders.js +543 -0
- package/dist/src/collections/plan/builders.js.map +1 -0
- package/dist/src/collections/plan/data-forms.d.ts +332 -0
- package/dist/src/collections/plan/data-forms.d.ts.map +1 -0
- package/dist/src/collections/plan/data-forms.js +212 -0
- package/dist/src/collections/plan/data-forms.js.map +1 -0
- package/dist/src/collections/plan/factories.d.ts +338 -0
- package/dist/src/collections/plan/factories.d.ts.map +1 -0
- package/dist/src/collections/plan/factories.js +414 -0
- package/dist/src/collections/plan/factories.js.map +1 -0
- package/dist/src/collections/plan/index.d.ts +271 -0
- package/dist/src/collections/plan/index.d.ts.map +1 -0
- package/dist/src/collections/plan/index.js +162 -0
- package/dist/src/collections/plan/index.js.map +1 -0
- package/dist/src/collections/plan/ir.d.ts +3496 -0
- package/dist/src/collections/plan/ir.d.ts.map +1 -0
- package/dist/src/collections/plan/ir.js +83 -0
- package/dist/src/collections/plan/ir.js.map +1 -0
- package/dist/src/collections/plan/pick.d.ts +115 -0
- package/dist/src/collections/plan/pick.d.ts.map +1 -0
- package/dist/src/collections/plan/pick.js +193 -0
- package/dist/src/collections/plan/pick.js.map +1 -0
- package/dist/src/collections/plan/root.d.ts +210 -0
- package/dist/src/collections/plan/root.d.ts.map +1 -0
- package/dist/src/collections/plan/root.js +146 -0
- package/dist/src/collections/plan/root.js.map +1 -0
- package/dist/src/collections/plan/series.d.ts +11890 -0
- package/dist/src/collections/plan/series.d.ts.map +1 -0
- package/dist/src/collections/plan/series.js +412 -0
- package/dist/src/collections/plan/series.js.map +1 -0
- package/dist/src/collections/plan/types.d.ts +9291 -0
- package/dist/src/collections/plan/types.d.ts.map +1 -0
- package/dist/src/collections/plan/types.js +940 -0
- package/dist/src/collections/plan/types.js.map +1 -0
- package/dist/src/collections/roster/index.d.ts +4 -4
- package/dist/src/collections/roster/index.d.ts.map +1 -1
- package/dist/src/collections/roster/index.js +1 -1
- package/dist/src/collections/roster/index.js.map +1 -1
- package/dist/src/collections/roster/types.d.ts +1 -1
- package/dist/src/collections/roster/types.d.ts.map +1 -1
- package/dist/src/collections/roster/types.js +4 -3
- package/dist/src/collections/roster/types.js.map +1 -1
- package/dist/src/collections/table/index.d.ts +70 -2
- package/dist/src/collections/table/index.d.ts.map +1 -1
- package/dist/src/collections/table/index.js +55 -11
- package/dist/src/collections/table/index.js.map +1 -1
- package/dist/src/collections/table/types.d.ts +1 -1
- package/dist/src/collections/table/types.d.ts.map +1 -1
- package/dist/src/component.d.ts +3898 -1339
- package/dist/src/component.d.ts.map +1 -1
- package/dist/src/component.js +83 -146
- package/dist/src/component.js.map +1 -1
- package/dist/src/container/card/types.d.ts +1 -1
- package/dist/src/container/card/types.d.ts.map +1 -1
- package/dist/src/contracts/approval.d.ts +117 -0
- package/dist/src/contracts/approval.d.ts.map +1 -0
- package/dist/src/contracts/approval.js +105 -0
- package/dist/src/contracts/approval.js.map +1 -0
- package/dist/src/contracts/drag.d.ts +3 -3
- package/dist/src/contracts/drag.d.ts.map +1 -1
- package/dist/src/contracts/drag.js +23 -10
- package/dist/src/contracts/drag.js.map +1 -1
- package/dist/src/contracts/index.d.ts +4 -1
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/index.js +4 -1
- package/dist/src/contracts/index.js.map +1 -1
- package/dist/src/contracts/pick.d.ts +705 -0
- package/dist/src/contracts/pick.d.ts.map +1 -0
- package/dist/src/contracts/pick.js +360 -0
- package/dist/src/contracts/pick.js.map +1 -0
- package/dist/src/contracts/review.d.ts +7 -99
- package/dist/src/contracts/review.d.ts.map +1 -1
- package/dist/src/contracts/review.js +11 -85
- package/dist/src/contracts/review.js.map +1 -1
- package/dist/src/contracts/slice-affordances.d.ts +13 -4
- package/dist/src/contracts/slice-affordances.d.ts.map +1 -1
- package/dist/src/contracts/slice-affordances.js +13 -3
- package/dist/src/contracts/slice-affordances.js.map +1 -1
- package/dist/src/contracts/source.d.ts +432 -0
- package/dist/src/contracts/source.d.ts.map +1 -0
- package/dist/src/contracts/source.js +505 -0
- package/dist/src/contracts/source.js.map +1 -0
- package/dist/src/contracts/states.d.ts +130 -0
- package/dist/src/contracts/states.d.ts.map +1 -1
- package/dist/src/contracts/states.js +122 -0
- package/dist/src/contracts/states.js.map +1 -1
- package/dist/src/contracts/time.d.ts +76 -0
- package/dist/src/contracts/time.d.ts.map +1 -0
- package/dist/src/contracts/time.js +78 -0
- package/dist/src/contracts/time.js.map +1 -0
- package/dist/src/display/trace/types.d.ts +1 -1
- package/dist/src/display/trace/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +10 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +22 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/internal.d.ts +5 -3
- package/dist/src/internal.d.ts.map +1 -1
- package/dist/src/internal.js +4 -3
- package/dist/src/internal.js.map +1 -1
- package/dist/src/layout/dock/index.d.ts +1 -1
- package/dist/src/layout/dock/index.js +1 -1
- package/dist/src/layout/index.d.ts +0 -1
- package/dist/src/layout/index.d.ts.map +1 -1
- package/dist/src/layout/index.js +0 -1
- package/dist/src/layout/index.js.map +1 -1
- package/dist/src/overlays/drawer/types.d.ts +1 -1
- package/dist/src/overlays/drawer/types.d.ts.map +1 -1
- package/dist/src/pick/index.d.ts +443 -0
- package/dist/src/pick/index.d.ts.map +1 -0
- package/dist/src/pick/index.js +64 -0
- package/dist/src/pick/index.js.map +1 -0
- package/dist/src/pick/panel/index.d.ts +64 -0
- package/dist/src/pick/panel/index.d.ts.map +1 -0
- package/dist/src/pick/panel/index.js +61 -0
- package/dist/src/pick/panel/index.js.map +1 -0
- package/dist/src/platform/slice/index.d.ts +291 -0
- package/dist/src/platform/slice/index.d.ts.map +1 -1
- package/dist/src/platform/slice/index.js +16 -0
- package/dist/src/platform/slice/index.js.map +1 -1
- package/dist/src/runtime/collections/deck.d.ts +29 -0
- package/dist/src/runtime/collections/deck.d.ts.map +1 -1
- package/dist/src/runtime/collections/index.d.ts +1 -2
- package/dist/src/runtime/collections/index.d.ts.map +1 -1
- package/dist/src/runtime/collections/index.js +1 -2
- package/dist/src/runtime/collections/index.js.map +1 -1
- package/dist/src/runtime/collections/library.d.ts +1 -1
- package/dist/src/runtime/collections/library.js +1 -1
- package/dist/src/runtime/collections/plan.d.ts +98 -0
- package/dist/src/runtime/collections/plan.d.ts.map +1 -0
- package/dist/src/runtime/collections/plan.js +98 -0
- package/dist/src/runtime/collections/plan.js.map +1 -0
- package/dist/src/runtime/collections/schematic.d.ts +1 -1
- package/dist/src/runtime/collections/schematic.js +1 -1
- package/dist/src/runtime/collections/table.d.ts +9 -1
- package/dist/src/runtime/collections/table.d.ts.map +1 -1
- package/dist/src/runtime/collections/table.js +1 -7
- package/dist/src/runtime/collections/table.js.map +1 -1
- package/dist/src/runtime/layout/dock.d.ts +2 -2
- package/dist/src/runtime/layout/dock.js +2 -2
- package/dist/src/runtime/layout/index.d.ts +0 -1
- package/dist/src/runtime/layout/index.d.ts.map +1 -1
- package/dist/src/runtime/layout/index.js +0 -1
- package/dist/src/runtime/layout/index.js.map +1 -1
- package/dist/src/shared/plot-gutter.d.ts +6 -23
- package/dist/src/shared/plot-gutter.d.ts.map +1 -1
- package/dist/src/shared/plot-gutter.js +6 -19
- package/dist/src/shared/plot-gutter.js.map +1 -1
- package/dist/src/shared/reify.d.ts +47 -4
- package/dist/src/shared/reify.d.ts.map +1 -1
- package/dist/src/shared/reify.js +62 -4
- package/dist/src/shared/reify.js.map +1 -1
- package/dist/src/slice/breakdown/index.d.ts +27 -0
- package/dist/src/slice/breakdown/index.d.ts.map +1 -1
- package/dist/src/slice/breakdown/types.d.ts +27 -0
- package/dist/src/slice/breakdown/types.d.ts.map +1 -1
- package/dist/src/slice/cohort/index.d.ts +54 -0
- package/dist/src/slice/cohort/index.d.ts.map +1 -1
- package/dist/src/slice/cohort/types.d.ts +27 -0
- package/dist/src/slice/cohort/types.d.ts.map +1 -1
- package/dist/src/slice/filter/index.d.ts +27 -0
- package/dist/src/slice/filter/index.d.ts.map +1 -1
- package/dist/src/slice/filter/types.d.ts +27 -0
- package/dist/src/slice/filter/types.d.ts.map +1 -1
- package/dist/src/slice/index.d.ts +405 -0
- package/dist/src/slice/index.d.ts.map +1 -1
- package/dist/src/slice/legend/index.d.ts +27 -0
- package/dist/src/slice/legend/index.d.ts.map +1 -1
- package/dist/src/slice/legend/types.d.ts +27 -0
- package/dist/src/slice/legend/types.d.ts.map +1 -1
- package/dist/src/slice/range/index.d.ts +60 -18
- package/dist/src/slice/range/index.d.ts.map +1 -1
- package/dist/src/slice/range/index.js +9 -1
- package/dist/src/slice/range/index.js.map +1 -1
- package/dist/src/slice/range/types.d.ts +63 -22
- package/dist/src/slice/range/types.d.ts.map +1 -1
- package/dist/src/slice/range/types.js +12 -5
- package/dist/src/slice/range/types.js.map +1 -1
- package/dist/src/slice/search/index.d.ts +27 -0
- package/dist/src/slice/search/index.d.ts.map +1 -1
- package/dist/src/slice/search/types.d.ts +27 -0
- package/dist/src/slice/search/types.d.ts.map +1 -1
- package/dist/src/slice/summary/index.d.ts +27 -0
- package/dist/src/slice/summary/index.d.ts.map +1 -1
- package/dist/src/slice/summary/types.d.ts +27 -0
- package/dist/src/slice/summary/types.d.ts.map +1 -1
- package/dist/src/style/content.d.ts +4 -6
- package/dist/src/style/content.d.ts.map +1 -1
- package/dist/src/style/content.js +4 -6
- package/dist/src/style/content.js.map +1 -1
- package/dist/src/style/scheme.js +1 -1
- package/dist/src/style/scheme.js.map +1 -1
- package/dist/test/collections/index.d.ts +2 -2
- package/dist/test/collections/index.d.ts.map +1 -1
- package/dist/test/collections/index.js +2 -2
- package/dist/test/collections/index.js.map +1 -1
- package/dist/test/collections/paged-source.examples.d.ts +9 -0
- package/dist/test/collections/paged-source.examples.d.ts.map +1 -0
- package/dist/test/collections/paged-source.examples.js +136 -0
- package/dist/test/collections/paged-source.examples.js.map +1 -0
- package/dist/test/collections/plan.examples.d.ts +125 -0
- package/dist/test/collections/plan.examples.d.ts.map +1 -0
- package/dist/test/collections/plan.examples.js +2939 -0
- package/dist/test/collections/plan.examples.js.map +1 -0
- package/dist/test/contracts/pick.examples.d.ts +6 -0
- package/dist/test/contracts/pick.examples.d.ts.map +1 -0
- package/dist/test/contracts/pick.examples.js +89 -0
- package/dist/test/contracts/pick.examples.js.map +1 -0
- package/dist/test/layout/dock.examples.d.ts +4 -4
- package/dist/test/layout/dock.examples.d.ts.map +1 -1
- package/dist/test/layout/dock.examples.js +34 -16
- package/dist/test/layout/dock.examples.js.map +1 -1
- package/dist/test/slice/slice.examples.d.ts +2 -1
- package/dist/test/slice/slice.examples.d.ts.map +1 -1
- package/dist/test/slice/slice.examples.js +67 -9
- package/dist/test/slice/slice.examples.js.map +1 -1
- package/package.json +3 -3
- package/dist/src/collections/gantt/index.d.ts +0 -377
- package/dist/src/collections/gantt/index.d.ts.map +0 -1
- package/dist/src/collections/gantt/index.js +0 -896
- package/dist/src/collections/gantt/index.js.map +0 -1
- package/dist/src/collections/gantt/types.d.ts +0 -353
- package/dist/src/collections/gantt/types.d.ts.map +0 -1
- package/dist/src/collections/gantt/types.js +0 -207
- package/dist/src/collections/gantt/types.js.map +0 -1
- package/dist/src/collections/planner/index.d.ts +0 -624
- package/dist/src/collections/planner/index.d.ts.map +0 -1
- package/dist/src/collections/planner/index.js +0 -941
- package/dist/src/collections/planner/index.js.map +0 -1
- package/dist/src/collections/planner/types.d.ts +0 -615
- package/dist/src/collections/planner/types.d.ts.map +0 -1
- package/dist/src/collections/planner/types.js +0 -423
- package/dist/src/collections/planner/types.js.map +0 -1
- package/dist/src/layout/aligned-stack/index.d.ts +0 -101
- package/dist/src/layout/aligned-stack/index.d.ts.map +0 -1
- package/dist/src/layout/aligned-stack/index.js +0 -105
- package/dist/src/layout/aligned-stack/index.js.map +0 -1
- package/dist/src/layout/aligned-stack/types.d.ts +0 -50
- package/dist/src/layout/aligned-stack/types.d.ts.map +0 -1
- package/dist/src/layout/aligned-stack/types.js +0 -24
- package/dist/src/layout/aligned-stack/types.js.map +0 -1
- package/dist/src/runtime/collections/gantt.d.ts +0 -67
- package/dist/src/runtime/collections/gantt.d.ts.map +0 -1
- package/dist/src/runtime/collections/gantt.js +0 -58
- package/dist/src/runtime/collections/gantt.js.map +0 -1
- package/dist/src/runtime/collections/planner.d.ts +0 -80
- package/dist/src/runtime/collections/planner.d.ts.map +0 -1
- package/dist/src/runtime/collections/planner.js +0 -76
- package/dist/src/runtime/collections/planner.js.map +0 -1
- package/dist/src/runtime/layout/aligned-stack.d.ts +0 -38
- package/dist/src/runtime/layout/aligned-stack.d.ts.map +0 -1
- package/dist/src/runtime/layout/aligned-stack.js +0 -36
- package/dist/src/runtime/layout/aligned-stack.js.map +0 -1
- package/dist/test/collections/gantt.examples.d.ts +0 -33
- package/dist/test/collections/gantt.examples.d.ts.map +0 -1
- package/dist/test/collections/gantt.examples.js +0 -359
- package/dist/test/collections/gantt.examples.js.map +0 -1
- package/dist/test/collections/planner.examples.d.ts +0 -47
- package/dist/test/collections/planner.examples.d.ts.map +0 -1
- package/dist/test/collections/planner.examples.js +0 -546
- package/dist/test/collections/planner.examples.js.map +0 -1
- package/dist/test/layout/aligned-stack.examples.d.ts +0 -25
- package/dist/test/layout/aligned-stack.examples.d.ts.map +0 -1
- package/dist/test/layout/aligned-stack.examples.js +0 -294
- package/dist/test/layout/aligned-stack.examples.js.map +0 -1
|
@@ -0,0 +1,2939 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@elaraai/east-ui/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
4
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
5
|
+
*/
|
|
6
|
+
/** @jsxImportSource @elaraai/east-ui */
|
|
7
|
+
import { ArrayType, BooleanType, DateTimeType, DictType, East, FloatType, IntegerType, NullType, OptionType, StringType, StructType, VariantType, example, none, some, variant, } from "@elaraai/east";
|
|
8
|
+
import { DragEventType, EventStateType, State, StatusValueType, Style, UIComponentType } from "@elaraai/east-ui";
|
|
9
|
+
import { Badge, Box, Chart, Configurator, Format, Library, Plan, Progress, Reactive, SegmentGroup, Select, Slice, Sparkline, Text, VStack, deriveApproval } from "@elaraai/east-ui";
|
|
10
|
+
// The corpus — every canvas is DEFINED the one way (`Plan Data Interface.md`
|
|
11
|
+
// §3.5): `data` (RAW domain rows — batches, tonnes, lifecycle states; row
|
|
12
|
+
// series discriminated by a field; no factory-built values in the data) +
|
|
13
|
+
// `series` (one `Plan.series.*` value per row series, `$.const`-bound and typed
|
|
14
|
+
// by the `Plan.Types.Series(Row)` constructor — its accessors DERIVE the
|
|
15
|
+
// canvas vocabulary from the raw fields client-side: labels, quantity
|
|
16
|
+
// displays, element values via the `Plan.run`/`chip`/`event` expression
|
|
17
|
+
// builders) + the root RESOLVER functions (`popover` / `hover` /
|
|
18
|
+
// `expandRender`). The kind factories are subtree vocabulary only —
|
|
19
|
+
// library-template `make` bodies and `Plan.series.rows` one-off chrome.
|
|
20
|
+
// Fixtures are defined INLINE in each example's fn body (the automated docs
|
|
21
|
+
// extract only the fn body) and DERIVED with East expressions — a `week(n)`
|
|
22
|
+
// ISO-week function plus `East.Array.generate` — never hand-written per
|
|
23
|
+
// element; only individually-meaningful mock records stay literal, and
|
|
24
|
+
// lifecycle states are plain `EventStateType` variants (the shared contracts
|
|
25
|
+
// vocabulary a plan dataset stores).
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// planTargetState — the §1 flagship (every row kind, ONE source, series)
|
|
28
|
+
// ============================================================================
|
|
29
|
+
export const planTargetState = example({
|
|
30
|
+
keywords: [
|
|
31
|
+
"Plan", "canvas", "data", "series", "match", "axis", "window", "resolution", "now",
|
|
32
|
+
"span", "run", "group", "heat", "buckets", "cards", "chip", "events", "mark", "chart",
|
|
33
|
+
"layers", "rollup", "bands", "review", "footer", "milestone",
|
|
34
|
+
"decision", "exception", "pinned", "port", "hovercard", "popover",
|
|
35
|
+
"slice", "brush", "horizon", "toolbar", "affordances", "expand",
|
|
36
|
+
"expandRender", "resolver", "data-driven", "accessor", "raw", "target state",
|
|
37
|
+
],
|
|
38
|
+
description: "Every row kind on one axis from a single raw ops source, with slice chrome, expand, review and a status footer",
|
|
39
|
+
fn: East.function([], UIComponentType, (_$) => {
|
|
40
|
+
// The horizon fixture — the despatch orders the slice narrows (the
|
|
41
|
+
// canvas rows are the ops source; the slice is chrome over THESE):
|
|
42
|
+
// when, which line, whether the order is at risk of running late,
|
|
43
|
+
// and the tonnage booked so far (0 = nothing yet).
|
|
44
|
+
const HorizonRow = StructType({ key: StringType, at: DateTimeType, line: StringType, risk: StringType, tonnes: FloatType });
|
|
45
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
46
|
+
// The RAW job record — what an ops dataset stores: phase + optional
|
|
47
|
+
// batch, the window, optional tonnage, the lifecycle state, an alert.
|
|
48
|
+
const JobRow = StructType({
|
|
49
|
+
key: StringType, phase: StringType, batch: OptionType(StringType),
|
|
50
|
+
start: DateTimeType, end: DateTimeType,
|
|
51
|
+
tonnes: OptionType(FloatType), state: EventStateType,
|
|
52
|
+
alert: OptionType(StatusValueType),
|
|
53
|
+
});
|
|
54
|
+
const ShiftRow = StructType({
|
|
55
|
+
key: StringType, from: DateTimeType, to: DateTimeType,
|
|
56
|
+
hours: FloatType, state: EventStateType,
|
|
57
|
+
});
|
|
58
|
+
const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
|
|
59
|
+
// ONE source — row series discriminated by the kind variant (the
|
|
60
|
+
// natural ops-dataset shape; the same rows page from a dataset). The
|
|
61
|
+
// arms carry RAW fields; presence (status / expand declaration) is
|
|
62
|
+
// per-row Option DATA the accessors pass through.
|
|
63
|
+
const OpsRow = StructType({
|
|
64
|
+
kind: VariantType({
|
|
65
|
+
kpi: StructType({ name: StringType, headline: StringType, pinned: BooleanType,
|
|
66
|
+
points: ArrayType(MeasureRow) }),
|
|
67
|
+
machine: StructType({ cap: FloatType,
|
|
68
|
+
status: OptionType(StatusValueType),
|
|
69
|
+
detail: OptionType(Plan.Types.Expand),
|
|
70
|
+
jobs: ArrayType(JobRow),
|
|
71
|
+
decisions: ArrayType(Plan.Types.DecisionMark),
|
|
72
|
+
ports: ArrayType(Plan.Types.Port) }),
|
|
73
|
+
load: StructType({ name: StringType, sub: StringType, cells: ArrayType(Plan.Types.HeatCell) }),
|
|
74
|
+
dock: StructType({ name: StringType, allocations: ArrayType(AllocRow),
|
|
75
|
+
markers: ArrayType(Plan.Types.CellMarker) }),
|
|
76
|
+
crew: StructType({ name: StringType, hours: StringType, shifts: ArrayType(ShiftRow) }),
|
|
77
|
+
stream: StructType({ name: StringType, marks: ArrayType(Plan.Types.EventMark) }),
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
const cfg = Slice.config(HorizonRow, {
|
|
81
|
+
fields: {
|
|
82
|
+
at: { label: "Despatched", format: { date: "MMM D" } },
|
|
83
|
+
line: { label: "Line" },
|
|
84
|
+
risk: { label: "Risk", hints: ["late", "on-time"] },
|
|
85
|
+
tonnes: { label: "Tonnes" },
|
|
86
|
+
},
|
|
87
|
+
rangeFieldId: "at",
|
|
88
|
+
searchFieldIds: ["line"],
|
|
89
|
+
});
|
|
90
|
+
return (_jsx(Reactive, { children: $ => {
|
|
91
|
+
// Monday of ISO week n, 2026 (W1 Monday = 2025-12-29). The §1
|
|
92
|
+
// window is W27–W38 (half-open at W39); now = W31.
|
|
93
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
94
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
95
|
+
return w1.addWeeks(n.subtract(1n));
|
|
96
|
+
}));
|
|
97
|
+
const covPcts = $.const([96.1, 96.4, 96.8, 97.0, 96.2, 95.1, 93.4, 91.0, 88.9, 91.4, 93.8, 94.2], ArrayType(FloatType));
|
|
98
|
+
const loadPcts = $.const([46.0, 52.0, 58.0, 61.0, 66.0, 72.0, 78.0, 84.0, 90.0, 96.0, 98.0, 92.0], ArrayType(FloatType));
|
|
99
|
+
const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: covPcts.get(i) })));
|
|
100
|
+
const loadCells = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
|
|
101
|
+
at: Plan.at.time(week(i.add(27n))), value: some(loadPcts.get(i)),
|
|
102
|
+
label: some(East.Float.printFixed(loadPcts.get(i), 0n)),
|
|
103
|
+
})));
|
|
104
|
+
// 36 despatch orders spread over W21–W47 — the horizon fixture.
|
|
105
|
+
// Every third order is at risk of running late (the §1 `Late
|
|
106
|
+
// risk` cohort counts 12); every seventh has no tonnage booked
|
|
107
|
+
// yet (the `Empty` cohort).
|
|
108
|
+
const horizon = $.let(East.Array.generate(36n, HorizonRow, (_$, i) => ({
|
|
109
|
+
key: East.str `h${East.print(i.add(1n))}`,
|
|
110
|
+
at: week(i.multiply(27n).divide(36n).add(21n)),
|
|
111
|
+
line: i.remainder(2n).equal(0n).ifElse(() => "Line 1", () => "Line 2"),
|
|
112
|
+
risk: i.remainder(3n).equal(0n).ifElse(() => "late", () => "on-time"),
|
|
113
|
+
tonnes: i.remainder(7n).equal(6n).ifElse(() => 0.0, () => i.multiply(11n).remainder(40n).toFloat().add(20.0)),
|
|
114
|
+
})));
|
|
115
|
+
// Raw jobs → runs: ONE bound mapping function — the bar label and
|
|
116
|
+
// the quantity display/number pair derive CLIENT-SIDE from the
|
|
117
|
+
// raw phase/batch/tonnes fields (the series-make application). The
|
|
118
|
+
// run is written as a RECORD, so its instants are spelled with
|
|
119
|
+
// `Plan.at.time` — the `Plan.run` builder would wrap a DateTime
|
|
120
|
+
// field by itself.
|
|
121
|
+
const jobRuns = $.const(East.function([ArrayType(JobRow)], ArrayType(Plan.Types.Run), (_$, jobs) => jobs.map(($, j) => {
|
|
122
|
+
const noQuantity = $.const(none, OptionType(StringType));
|
|
123
|
+
const quantity = $.let(j.tonnes.match({
|
|
124
|
+
some: (_$, t) => East.value(some(East.str `${East.Float.printFixed(t, 0n)} t`), OptionType(StringType)),
|
|
125
|
+
none: (_$) => noQuantity,
|
|
126
|
+
}), OptionType(StringType));
|
|
127
|
+
const label = $.let(j.batch.match({
|
|
128
|
+
some: (_$, b) => East.str `${j.phase} · ${b}`,
|
|
129
|
+
none: (_$) => j.phase,
|
|
130
|
+
}), StringType);
|
|
131
|
+
const run = $.let({
|
|
132
|
+
key: j.key, start: Plan.at.time(j.start), end: Plan.at.time(j.end), label,
|
|
133
|
+
quantity, qty: j.tonnes, state: j.state, status: j.alert,
|
|
134
|
+
moved: none, icon: none,
|
|
135
|
+
}, Plan.Types.Run);
|
|
136
|
+
return run;
|
|
137
|
+
})));
|
|
138
|
+
// Raw shifts → chips: hours print as the chip label, an ADDED
|
|
139
|
+
// proposal wearing the `+` prefix — display derives from the
|
|
140
|
+
// lifecycle, down to the proposal's flavour (a `removed` shift is
|
|
141
|
+
// a proposal too, and `+` would read as its opposite).
|
|
142
|
+
const shiftChips = $.const(East.function([ArrayType(ShiftRow)], ArrayType(Plan.Types.Chip), (_$, shifts) => shifts.map(($, s) => {
|
|
143
|
+
const hrs = $.let(East.Float.printFixed(s.hours, 0n), StringType);
|
|
144
|
+
const label = $.let(s.state.match({
|
|
145
|
+
proposed: (_$, p) => p.hasTag("removed").ifElse(() => East.str `${hrs}h`, () => East.str `+${hrs}h`),
|
|
146
|
+
}, _$ => East.str `${hrs}h`), StringType);
|
|
147
|
+
return Plan.chip({ key: s.key, from: s.from, to: s.to, label, state: s.state });
|
|
148
|
+
})));
|
|
149
|
+
// The ONE ops source — every series' rows in one KEYED collection,
|
|
150
|
+
// RAW: no display strings the accessors can derive, no built
|
|
151
|
+
// elements. The keys are the canvas's order as well as its
|
|
152
|
+
// identity (#568), so the §1 layout is expressed by naming them:
|
|
153
|
+
// the ordering segment places each series, the rest is the row's
|
|
154
|
+
// real id — which is what `links`, `popover` and `onSelect` speak.
|
|
155
|
+
const ops = $.const(new Map([
|
|
156
|
+
["coverage", { kind: variant("kpi", { name: "COVERAGE", headline: "94.2%", pinned: true, points: coverage }) }],
|
|
157
|
+
["L1-M03", { kind: variant("machine", { cap: 120.0, status: some(variant("success", null)), detail: none,
|
|
158
|
+
jobs: [
|
|
159
|
+
{ key: "set", phase: "SET", batch: none, start: week(27n), end: week(28n), tonnes: none, state: variant("actual", null), alert: none },
|
|
160
|
+
{ key: "b214", phase: "RUN", batch: some("B-214"), start: week(28n), end: week(31n), tonnes: some(96.0), state: variant("in-progress", null), alert: none },
|
|
161
|
+
{ key: "cln", phase: "CLN", batch: none, start: week(31n), end: week(32n), tonnes: none, state: variant("confirmed", null), alert: none },
|
|
162
|
+
{ key: "b221", phase: "RUN", batch: some("B-221"), start: week(32n), end: week(35n), tonnes: some(88.0), state: variant("proposed", variant("recommended", null)), alert: none },
|
|
163
|
+
],
|
|
164
|
+
decisions: [{ key: "d1", at: Plan.at.time(week(32n)), applied: false }],
|
|
165
|
+
ports: [{ at: Plan.at.time(week(31n)), label: some("−24 t") }] }) }],
|
|
166
|
+
["L1-M04", { kind: variant("machine", { cap: 120.0, status: none,
|
|
167
|
+
// The expand declaration — a stored plain-data record
|
|
168
|
+
// (§3.2); presence is a per-row fact and the ROOT's
|
|
169
|
+
// expandRender mounts the body.
|
|
170
|
+
detail: some({ height: some("152px"), axis: variant("keep", null) }),
|
|
171
|
+
jobs: [
|
|
172
|
+
{ key: "b208", phase: "RUN", batch: some("B-208"), start: week(27n), end: week(30n), tonnes: some(112.0), state: variant("actual", null), alert: none },
|
|
173
|
+
{ key: "hld", phase: "HLD", batch: none, start: week(30n), end: week(31n), tonnes: none, state: variant("confirmed", null), alert: none },
|
|
174
|
+
{ key: "qc", phase: "QC", batch: none, start: week(31n), end: week(33n), tonnes: none, state: variant("confirmed", null), alert: none },
|
|
175
|
+
{ key: "b231", phase: "RUN", batch: some("B-231"), start: week(33n), end: week(41n), tonnes: some(104.0), state: variant("proposed", variant("recommended", null)), alert: none },
|
|
176
|
+
],
|
|
177
|
+
decisions: [], ports: [] }) }],
|
|
178
|
+
["L1-M07", { kind: variant("machine", { cap: 80.0, status: some(variant("warning", null)), detail: none,
|
|
179
|
+
jobs: [
|
|
180
|
+
{ key: "b197", phase: "HLD", batch: some("B-197"), start: week(27n), end: week(31n), tonnes: none, state: variant("actual", null), alert: some(variant("warning", null)) },
|
|
181
|
+
{ key: "cln", phase: "CLN", batch: none, start: week(34n), end: week(36n), tonnes: none, state: variant("proposed", variant("recommended", null)), alert: none },
|
|
182
|
+
],
|
|
183
|
+
decisions: [], ports: [] }) }],
|
|
184
|
+
["l2-load", { kind: variant("load", { name: "L2 load", sub: "%/wk", cells: loadCells }) }],
|
|
185
|
+
["dock2", { kind: variant("dock", { name: "Dock 2",
|
|
186
|
+
allocations: [
|
|
187
|
+
{ key: "a1", at: week(27n), state: variant("confirmed", null) },
|
|
188
|
+
{ key: "a2", at: week(28n), state: variant("confirmed", null) },
|
|
189
|
+
{ key: "a3", at: week(29n), state: variant("confirmed", null) },
|
|
190
|
+
{ key: "a4", at: week(30n), state: variant("confirmed", null) },
|
|
191
|
+
{ key: "a5", at: week(31n), state: variant("proposed", variant("recommended", null)) },
|
|
192
|
+
{ key: "a6", at: week(33n), state: variant("proposed", variant("recommended", null)) },
|
|
193
|
+
{ key: "a7", at: week(35n), state: variant("confirmed", null) },
|
|
194
|
+
{ key: "a8", at: week(35n), state: variant("proposed", variant("recommended", null)) },
|
|
195
|
+
],
|
|
196
|
+
markers: [{ at: Plan.at.time(week(35n)), lane: none, status: variant("warning", null), message: "capacity breach — 2 allocations" }] }) }],
|
|
197
|
+
["40-crewA", { kind: variant("crew", { name: "Crew A", hours: "152h → 168h", shifts: [
|
|
198
|
+
{ key: "s1", from: week(27n), to: week(29n), hours: 80.0, state: variant("confirmed", null) },
|
|
199
|
+
{ key: "s2", from: week(29n), to: week(31n), hours: 72.0, state: variant("confirmed", null) },
|
|
200
|
+
{ key: "s3", from: week(31n), to: week(33n), hours: 64.0, state: variant("proposed", variant("recommended", null)) },
|
|
201
|
+
{ key: "s4", from: week(34n), to: week(35n), hours: 48.0, state: variant("estimated", null) },
|
|
202
|
+
{ key: "s5", from: week(36n), to: week(38n), hours: 56.0, state: variant("proposed", variant("recommended", null)) },
|
|
203
|
+
] }) }],
|
|
204
|
+
["50-milestones", { kind: variant("stream", { name: "MILESTONES", marks: [
|
|
205
|
+
{ key: "kick", at: Plan.at.time(week(28n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
|
|
206
|
+
{ key: "d1", at: Plan.at.time(week(31n)), kind: variant("decision", { applied: true }), icon: none, label: none },
|
|
207
|
+
{ key: "rel", at: Plan.at.time(week(33n)), kind: variant("milestone", null), icon: none, label: some("REL 2.4") },
|
|
208
|
+
{ key: "audit", at: Plan.at.time(week(35n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
|
|
209
|
+
{ key: "d2", at: Plan.at.time(week(37n)), kind: variant("decision", { applied: false }), icon: none, label: some("×3") },
|
|
210
|
+
] }) }],
|
|
211
|
+
]), DictType(StringType, OpsRow));
|
|
212
|
+
// The series — one entry per row series, canvas order = series
|
|
213
|
+
// order; the whole list is an East value typed by the constructor.
|
|
214
|
+
const series = $.const([
|
|
215
|
+
Plan.series.chart(OpsRow, {
|
|
216
|
+
key: "chart", title: "Chart",
|
|
217
|
+
match: r => r.kind.hasTag("kpi"),
|
|
218
|
+
label: r => r.kind.unwrap("kpi").name, id: true,
|
|
219
|
+
pinned: r => r.kind.unwrap("kpi").pinned,
|
|
220
|
+
value: r => some(r.kind.unwrap("kpi").headline),
|
|
221
|
+
status: _r => some(variant("warning", null)),
|
|
222
|
+
height: "spark", expandable: true,
|
|
223
|
+
layers: r => [
|
|
224
|
+
Plan.layer(Chart.Line(r.kind.unwrap("kpi").points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
|
|
225
|
+
Chart.refLine({ y: 100, label: "TARGET 100" }),
|
|
226
|
+
],
|
|
227
|
+
}),
|
|
228
|
+
Plan.series.group(OpsRow, { key: "10-line1", label: "Line 1", meta: "8 rs · 82%" }, [
|
|
229
|
+
Plan.series.span(OpsRow, {
|
|
230
|
+
key: "span", title: "Span",
|
|
231
|
+
match: r => r.kind.hasTag("machine"),
|
|
232
|
+
label: (_r, k) => k, id: true,
|
|
233
|
+
value: r => some(East.str `${East.Float.printFixed(r.kind.unwrap("machine").cap, 0n)} t`),
|
|
234
|
+
status: r => r.kind.unwrap("machine").status,
|
|
235
|
+
expand: r => r.kind.unwrap("machine").detail,
|
|
236
|
+
runs: r => jobRuns(r.kind.unwrap("machine").jobs),
|
|
237
|
+
decisions: r => r.kind.unwrap("machine").decisions,
|
|
238
|
+
ports: r => r.kind.unwrap("machine").ports,
|
|
239
|
+
}),
|
|
240
|
+
]),
|
|
241
|
+
Plan.series.group(OpsRow, { key: "20-line2", label: "Line 2", value: "98%", status: "warning", collapsed: true, summaryAggregate: "mean" }, [
|
|
242
|
+
Plan.series.heat(OpsRow, {
|
|
243
|
+
key: "heat", title: "Heat",
|
|
244
|
+
match: r => r.kind.hasTag("load"),
|
|
245
|
+
label: r => r.kind.unwrap("load").name,
|
|
246
|
+
sub: r => some(r.kind.unwrap("load").sub),
|
|
247
|
+
cells: r => Plan.heatCells(r.kind.unwrap("load").cells, { min: 0, max: 100, warnAt: 95 }),
|
|
248
|
+
}),
|
|
249
|
+
]),
|
|
250
|
+
Plan.series.group(OpsRow, { key: "30-docks", label: "Docks · In", meta: "3 rs" }, [
|
|
251
|
+
Plan.series.buckets(OpsRow, {
|
|
252
|
+
key: "buckets", title: "Buckets",
|
|
253
|
+
match: r => r.kind.hasTag("dock"),
|
|
254
|
+
label: r => r.kind.unwrap("dock").name,
|
|
255
|
+
value: _r => some("load/wk"),
|
|
256
|
+
events: r => r.kind.unwrap("dock").allocations.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
|
|
257
|
+
markers: r => r.kind.unwrap("dock").markers,
|
|
258
|
+
}),
|
|
259
|
+
]),
|
|
260
|
+
Plan.series.cards(OpsRow, {
|
|
261
|
+
key: "cards", title: "Cards",
|
|
262
|
+
match: r => r.kind.hasTag("crew"),
|
|
263
|
+
label: r => r.kind.unwrap("crew").name, stacked: true,
|
|
264
|
+
sub: r => some(r.kind.unwrap("crew").hours),
|
|
265
|
+
chips: r => shiftChips(r.kind.unwrap("crew").shifts),
|
|
266
|
+
}),
|
|
267
|
+
Plan.series.events(OpsRow, {
|
|
268
|
+
key: "events", title: "Events",
|
|
269
|
+
match: r => r.kind.hasTag("stream"),
|
|
270
|
+
label: r => r.kind.unwrap("stream").name, id: true,
|
|
271
|
+
value: r => some(East.print(r.kind.unwrap("stream").marks.length())),
|
|
272
|
+
marks: r => r.kind.unwrap("stream").marks,
|
|
273
|
+
}),
|
|
274
|
+
], ArrayType(Plan.Types.Series(OpsRow)));
|
|
275
|
+
const axis = $.const(Plan.axis({
|
|
276
|
+
window: { min: week(27n), max: week(39n) },
|
|
277
|
+
resolution: "week", resolutions: ["month", "week", "day"], now: week(31n),
|
|
278
|
+
}));
|
|
279
|
+
// The §1 toolbar is SEEDED slice state: the applied window is the
|
|
280
|
+
// range chip (`JUN 29 → SEP 21 · 84d`), and two saved cohorts sit
|
|
281
|
+
// beside the filter builder with `Late risk` active — their
|
|
282
|
+
// counts are live over the horizon fixture, never printed.
|
|
283
|
+
const slice = $.let(Slice.bind([HorizonRow], "ex.plan.target", cfg, Slice.state({
|
|
284
|
+
range: some(variant("datetime", { from: week(27n), to: week(39n) })),
|
|
285
|
+
cohorts: [
|
|
286
|
+
{ id: "late", name: "Late risk", filters: [variant("string", { fieldId: "risk", op: variant("eq", "late") })] },
|
|
287
|
+
{ id: "empty", name: "Empty", filters: [variant("float", { fieldId: "tonnes", op: variant("lte", 0.0) })] },
|
|
288
|
+
],
|
|
289
|
+
activeCohorts: new Set(["late"]),
|
|
290
|
+
}), horizon, none));
|
|
291
|
+
// The R2 developer render — the ROOT's resolver, called with the
|
|
292
|
+
// focused row's ref; ONE function serves every row whose `expand`
|
|
293
|
+
// accessor returned some(...). The machine series declares it.
|
|
294
|
+
const util = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(17n).remainder(45n).toFloat().add(52.0) })));
|
|
295
|
+
const expandRender = $.const(East.function([Plan.Types.RowRef], UIComponentType, (_$, _ref) => (_jsx(Chart, { layers: [Chart.Line(util, { x: r => r.week, y: r => r.pct })], height: 120, grid: false }))));
|
|
296
|
+
// The generalized element resolvers — ONE stored popover / hover
|
|
297
|
+
// function each over Plan.Types.ElementRef (refs carry the row
|
|
298
|
+
// key on every arm); rich bodies build lazily at interaction
|
|
299
|
+
// time, and a none result opens no surface.
|
|
300
|
+
const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
|
|
301
|
+
const noBody = $.const(none, OptionType(UIComponentType));
|
|
302
|
+
return ref.match({
|
|
303
|
+
run: (_$, ev) => ev.run.equal("b221").ifElse(() => some(_jsx(Text, { children: "Proposed by run 412 \u2014 fills the W32 idle window." })), () => noBody),
|
|
304
|
+
mark: (_$, ev) => ev.row.equal("L1-M03").and(() => ev.mark.equal("d1")).ifElse(() => some(_jsx(Text, { children: "Schedule B-221" })), () => noBody),
|
|
305
|
+
}, _$ => noBody);
|
|
306
|
+
}));
|
|
307
|
+
const hover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
|
|
308
|
+
const noBody = $.const(none, OptionType(UIComponentType));
|
|
309
|
+
return ref.match({
|
|
310
|
+
run: (_$, ev) => ev.run.equal("b197").ifElse(() => some(_jsx(Text, { children: "Waiting on QC gate 4 \u2014 2.6\u00D7 median dwell." })), () => noBody),
|
|
311
|
+
}, _$ => noBody);
|
|
312
|
+
}));
|
|
313
|
+
// Behavior props — bound once so memoized renderers keep identity.
|
|
314
|
+
const onRowRef = $.const(East.function([Plan.Types.RowRef], NullType, (_$, _r) => null));
|
|
315
|
+
const onRunClick = $.const(East.function([Plan.Types.RunClickEvent], NullType, (_$, _e) => null));
|
|
316
|
+
const onGroupToggle = $.const(East.function([Plan.Types.GroupToggleEvent], NullType, (_$, _e) => null));
|
|
317
|
+
const onBatch = $.const(East.function([], NullType, (_$) => null));
|
|
318
|
+
return (_jsx(Plan, { slice: { slice, affordances: ["cohort", "filter", "search", "range", "resolution", "brush", "summary"] }, axis: axis,
|
|
319
|
+
// The link graph (R1) — the W31 −24 t transfer: hover a
|
|
320
|
+
// linked machine for the links-focus control.
|
|
321
|
+
links: [
|
|
322
|
+
Plan.link({ from: "L1-M03", fromRun: "b214", to: "L1-M04", toRun: "qc", quantity: 24, label: "−24 t" }),
|
|
323
|
+
], data: ops, series: series, review: {
|
|
324
|
+
summary: _jsx(Text, { children: "4 JOBS \u00B7 2 FLAGGED NEED A CALL \u00B7 +6H FLOAT" }),
|
|
325
|
+
onApprove: onRowRef,
|
|
326
|
+
onReject: onRowRef,
|
|
327
|
+
onApproveAll: onBatch,
|
|
328
|
+
onRejectAll: onBatch,
|
|
329
|
+
onRerun: onBatch,
|
|
330
|
+
}, expandRender: expandRender, popover: popover, hover: hover, onSelect: onRowRef, onRunClick: onRunClick, onGroupToggle: onGroupToggle, footer: [
|
|
331
|
+
{ text: "512 RESOURCES · 12 GROUPS · 3 IN VIEW" },
|
|
332
|
+
{ text: "318 OBSERVED · 966 PLANNED" },
|
|
333
|
+
{ text: "3 EXCEPTIONS", tone: "warning" },
|
|
334
|
+
{ text: "RUN 412 · W27–W38", end: true },
|
|
335
|
+
] }));
|
|
336
|
+
} }));
|
|
337
|
+
}),
|
|
338
|
+
inputs: [],
|
|
339
|
+
});
|
|
340
|
+
// ============================================================================
|
|
341
|
+
// planVariants — THE Plan configurator (#571): axis presets × style sweeps
|
|
342
|
+
// ============================================================================
|
|
343
|
+
/**
|
|
344
|
+
* THE Plan configurator — the slot-2 variant-space surface the retired Gantt /
|
|
345
|
+
* Planner configurators held (#571). ONE live canvas; every axis feeds it as
|
|
346
|
+
* an expression. The window presets are whole `Plan.axis` VALUES riding a
|
|
347
|
+
* typed preset struct: the ops week window, the year roadmap at month
|
|
348
|
+
* resolution, and the 14-day sprint at day resolution with `ddd DD` labels
|
|
349
|
+
* (the #309 pinned-day-columns contract the Planner's `day` preset and the
|
|
350
|
+
* AlignedStack date-axis panel guarded). The style sweeps ride `style` —
|
|
351
|
+
* density rhythm and gutter width — and every callback (select / run click /
|
|
352
|
+
* group toggle / grain change) logs to the aside, the retired configurators'
|
|
353
|
+
* pattern. Fill sizing stays `planFill`'s; the per-kind visual grammars stay
|
|
354
|
+
* the static per-kind panels.
|
|
355
|
+
*/
|
|
356
|
+
export const planVariants = example({
|
|
357
|
+
keywords: [
|
|
358
|
+
"Plan", "configurator", "Configurator", "variants", "preset", "axis", "window",
|
|
359
|
+
"resolution", "month", "week", "day", "roadmap", "sprint", "ops", "format",
|
|
360
|
+
"ddd", "#309", "density", "condensed", "compact", "comfortable", "gutterWidth",
|
|
361
|
+
"gutter", "style", "onSelect", "onRunClick", "onGroupToggle", "onGrainChange",
|
|
362
|
+
"callback", "aside", "Reactive", "State", "SegmentGroup", "Select", "getTag",
|
|
363
|
+
],
|
|
364
|
+
description: "Plan configurator — axis window presets (ops week / year roadmap / 14-day sprint) with density and gutter-width sweeps on one live canvas; every callback logs to the aside",
|
|
365
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
366
|
+
// Monday of ISO week n, 2026 — the ops window is W27–W38
|
|
367
|
+
// (half-open at W39), now = W31; the roadmap preset walks the
|
|
368
|
+
// whole ISO year and the sprint preset a 14-day slice of it.
|
|
369
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
370
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
371
|
+
return w1.addWeeks(n.subtract(1n));
|
|
372
|
+
}));
|
|
373
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
374
|
+
const JobRow = StructType({
|
|
375
|
+
key: StringType, label: StringType,
|
|
376
|
+
start: DateTimeType, end: DateTimeType, state: EventStateType,
|
|
377
|
+
});
|
|
378
|
+
const OpsRow = StructType({
|
|
379
|
+
series: StringType, label: StringType,
|
|
380
|
+
sub: OptionType(StringType),
|
|
381
|
+
jobs: ArrayType(JobRow),
|
|
382
|
+
points: ArrayType(MeasureRow),
|
|
383
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
384
|
+
});
|
|
385
|
+
const noJobs = $.const([], ArrayType(JobRow));
|
|
386
|
+
const noPoints = $.const([], ArrayType(MeasureRow));
|
|
387
|
+
const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
|
|
388
|
+
const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
|
|
389
|
+
const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
|
|
390
|
+
const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
|
|
391
|
+
at: Plan.at.time(week(i.multiply(2n).add(27n))),
|
|
392
|
+
value: some(pcts.get(i)),
|
|
393
|
+
label: some(East.Float.printFixed(pcts.get(i), 0n)),
|
|
394
|
+
})));
|
|
395
|
+
// Mixed kinds on purpose: the density sweep re-rhythms the span /
|
|
396
|
+
// heat rows and the gutter sweep re-widths every label at once.
|
|
397
|
+
const ops = $.const(new Map([
|
|
398
|
+
["10-util", { series: "util", label: "UTIL %", sub: none, jobs: noJobs, points, cells: noCells }],
|
|
399
|
+
["20-m03", { series: "mach", label: "L1-M03", sub: some("cap 120 t"),
|
|
400
|
+
jobs: [
|
|
401
|
+
{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: variant("in-progress", null) },
|
|
402
|
+
{ key: "b221", label: "RUN · B-221", start: week(32n), end: week(35n), state: variant("proposed", variant("recommended", null)) },
|
|
403
|
+
], points: noPoints, cells: noCells }],
|
|
404
|
+
["20-m04", { series: "mach", label: "L1-M04", sub: none,
|
|
405
|
+
jobs: [
|
|
406
|
+
{ key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: variant("actual", null) },
|
|
407
|
+
], points: noPoints, cells: noCells }],
|
|
408
|
+
["30-load", { series: "load", label: "L2 load", sub: some("%/wk"), jobs: noJobs, points: noPoints, cells }],
|
|
409
|
+
["g1-m11", { series: "gmach", label: "L3-M11", sub: none,
|
|
410
|
+
jobs: [
|
|
411
|
+
{ key: "b301", label: "RUN · B-301", start: week(29n), end: week(33n), state: variant("confirmed", null) },
|
|
412
|
+
], points: noPoints, cells: noCells }],
|
|
413
|
+
]), DictType(StringType, OpsRow));
|
|
414
|
+
const series = $.const([
|
|
415
|
+
Plan.series.chart(OpsRow, {
|
|
416
|
+
key: "util", title: "Utilisation",
|
|
417
|
+
match: r => r.series.equal("util"),
|
|
418
|
+
label: r => r.label, id: true, height: "spark",
|
|
419
|
+
layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
|
|
420
|
+
}),
|
|
421
|
+
Plan.series.span(OpsRow, {
|
|
422
|
+
key: "mach", title: "Machine jobs",
|
|
423
|
+
match: r => r.series.equal("mach"),
|
|
424
|
+
label: r => r.label, id: true, sub: r => r.sub,
|
|
425
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
426
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
427
|
+
})),
|
|
428
|
+
}),
|
|
429
|
+
Plan.series.heat(OpsRow, {
|
|
430
|
+
key: "load", title: "Line load",
|
|
431
|
+
match: r => r.series.equal("load"),
|
|
432
|
+
label: r => r.label, sub: r => r.sub,
|
|
433
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
|
|
434
|
+
}),
|
|
435
|
+
// A strip so the group-toggle callback has something to fire on.
|
|
436
|
+
Plan.series.group(OpsRow, { key: "g1-line3", label: "Line 3", meta: "1 rs" }, [
|
|
437
|
+
Plan.series.span(OpsRow, {
|
|
438
|
+
key: "gmach", title: "Grouped jobs",
|
|
439
|
+
match: r => r.series.equal("gmach"),
|
|
440
|
+
label: r => r.label, id: true,
|
|
441
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
442
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
443
|
+
})),
|
|
444
|
+
}),
|
|
445
|
+
]),
|
|
446
|
+
], ArrayType(Plan.Types.Series(OpsRow)));
|
|
447
|
+
// Every preset is DATA — a whole axis value in a typed struct, so
|
|
448
|
+
// switching presets swaps window, resolution, format and now
|
|
449
|
+
// through ONE expression-fed prop.
|
|
450
|
+
const presets = $.const([
|
|
451
|
+
{ key: "ops", axis: Plan.axis({
|
|
452
|
+
window: { min: week(27n), max: week(39n) },
|
|
453
|
+
resolution: "week", resolutions: ["month", "week", "day"], now: week(31n),
|
|
454
|
+
}) },
|
|
455
|
+
{ key: "roadmap", axis: Plan.axis({
|
|
456
|
+
window: { min: week(1n), max: week(53n) },
|
|
457
|
+
resolution: "month", format: "MMM", now: week(31n),
|
|
458
|
+
}) },
|
|
459
|
+
{ key: "sprint", axis: Plan.axis({
|
|
460
|
+
window: { min: week(30n), max: week(32n) },
|
|
461
|
+
resolution: "day", format: "ddd DD", now: week(31n),
|
|
462
|
+
}) },
|
|
463
|
+
], ArrayType(StructType({ key: StringType, axis: Plan.Types.Axis })));
|
|
464
|
+
const presetKeys = $.const(["ops", "roadmap", "sprint"], ArrayType(StringType));
|
|
465
|
+
const densities = $.const([
|
|
466
|
+
variant("condensed", null), variant("compact", null), variant("comfortable", null),
|
|
467
|
+
], ArrayType(Style.Types.Density));
|
|
468
|
+
const gutters = $.const(["168px", "200px", "240px"], ArrayType(StringType));
|
|
469
|
+
const presetBind = $.let(State.bind([StringType], "plan_variants_preset", "ops"));
|
|
470
|
+
const densityBind = $.let(State.bind([StringType], "plan_variants_density", "compact"));
|
|
471
|
+
const gutterBind = $.let(State.bind([StringType], "plan_variants_gutter", "168px"));
|
|
472
|
+
const lastEventBind = $.let(State.bind([StringType], "plan_variants_last_event", ""));
|
|
473
|
+
const pKey = $.let(presetBind.read());
|
|
474
|
+
const dKey = $.let(densityBind.read());
|
|
475
|
+
const gKey = $.let(gutterBind.read());
|
|
476
|
+
const lastEvent = $.let(lastEventBind.read());
|
|
477
|
+
const onPreset = $.const(East.function([StringType], NullType, ($, next) => { $(presetBind.write(next)); }));
|
|
478
|
+
const onDensity = $.const(East.function([StringType], NullType, ($, next) => { $(densityBind.write(next)); }));
|
|
479
|
+
const onGutter = $.const(East.function([StringType], NullType, ($, next) => { $(gutterBind.write(next)); }));
|
|
480
|
+
// The interactive surface — every callback writes the aside line.
|
|
481
|
+
const onSelect = $.const(East.function([Plan.Types.RowRef], NullType, ($, ref) => {
|
|
482
|
+
$(lastEventBind.write(East.str `onSelect · ${ref.key}`));
|
|
483
|
+
}));
|
|
484
|
+
const onRunClick = $.const(East.function([Plan.Types.RunClickEvent], NullType, ($, ev) => {
|
|
485
|
+
$(lastEventBind.write(East.str `onRunClick · ${ev.row} / ${ev.run}`));
|
|
486
|
+
}));
|
|
487
|
+
const onGroupToggle = $.const(East.function([Plan.Types.GroupToggleEvent], NullType, ($, ev) => {
|
|
488
|
+
const state = $.let(ev.expanded.ifElse(() => "expanded", () => "collapsed"), StringType);
|
|
489
|
+
$(lastEventBind.write(East.str `onGroupToggle · ${ev.row} → ${state}`));
|
|
490
|
+
}));
|
|
491
|
+
const onGrainChange = $.const(East.function([Plan.Types.Grain], NullType, ($, grain) => {
|
|
492
|
+
$(lastEventBind.write(East.str `onGrainChange · ${grain.getTag()}`));
|
|
493
|
+
}));
|
|
494
|
+
const sel = $.let(presets.filter((_$, o) => o.key.equal(pKey)).get(0n, _$ => presets.get(0n)));
|
|
495
|
+
const densitySel = $.let(densities.filter((_$, v) => v.getTag().equal(dKey)).get(0n));
|
|
496
|
+
return (_jsx(Configurator, { controls: [
|
|
497
|
+
Configurator.Control("Preset", pKey, _jsx(Select, { value: pKey, onChange: onPreset, size: "sm", items: presetKeys.map((_$, s) => Select.Item(s, s)) })),
|
|
498
|
+
Configurator.Control("Density", dKey, _jsx(SegmentGroup, { value: dKey, onChange: onDensity, size: "sm", items: densities.map((_$, v) => SegmentGroup.Item(v.getTag(), _jsx(Text, { children: v.getTag().upperCase() }))) })),
|
|
499
|
+
Configurator.Control("Gutter", gKey, _jsx(SegmentGroup, { value: gKey, onChange: onGutter, size: "sm", items: gutters.map((_$, g) => SegmentGroup.Item(g, _jsx(Text, { children: g }))) })),
|
|
500
|
+
], preview: _jsx(Plan, { axis: sel.axis, data: ops, series: series, onSelect: onSelect, onRunClick: onRunClick, onGroupToggle: onGroupToggle, onGrainChange: onGrainChange, style: { density: densitySel, gutterWidth: gKey } }), live: true, aside: {
|
|
501
|
+
label: "Events · Reactive",
|
|
502
|
+
body: (_jsx(Badge, { colorPalette: "brand", variant: "outline", children: East.equal(lastEvent.length(), 0n).ifElse(_$ => "Interact with the canvas", _$ => lastEvent) })),
|
|
503
|
+
}, spec: [
|
|
504
|
+
Configurator.Spec("Resolution", sel.axis.unwrap("time").resolution.getTag()),
|
|
505
|
+
Configurator.Spec("Rows", East.print(ops.size())),
|
|
506
|
+
] }));
|
|
507
|
+
} }))),
|
|
508
|
+
inputs: [],
|
|
509
|
+
});
|
|
510
|
+
// ============================================================================
|
|
511
|
+
// Per-kind examples — one canvas per row kind; the config sweep is DATA
|
|
512
|
+
// ============================================================================
|
|
513
|
+
export const planSpanRows = example({
|
|
514
|
+
keywords: ["Plan", "data", "series", "span", "run", "state", "estimated", "removed", "rejected", "decision", "port", "rollup", "union", "byStatus", "groupBy", "bands", "group", "stacked", "gutter", "links", "link", "focus", "expand", "expandRender", "match", "raw"],
|
|
515
|
+
description: "Span rows over one raw machine source — proposal flavours, decision diamonds and ports, rollup parents, and a link graph",
|
|
516
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
517
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
518
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
519
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
520
|
+
return w1.addWeeks(n.subtract(1n));
|
|
521
|
+
}));
|
|
522
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
523
|
+
// The RAW job record; `series` picks the series, everything else —
|
|
524
|
+
// including the expand declaration — is per-row data.
|
|
525
|
+
const JobRow = StructType({
|
|
526
|
+
key: StringType, phase: StringType, batch: OptionType(StringType),
|
|
527
|
+
start: DateTimeType, end: DateTimeType,
|
|
528
|
+
tonnes: OptionType(FloatType), state: EventStateType,
|
|
529
|
+
});
|
|
530
|
+
const MachineRow = StructType({
|
|
531
|
+
series: StringType, program: StringType,
|
|
532
|
+
sub: OptionType(StringType), value: OptionType(StringType),
|
|
533
|
+
expand: OptionType(Plan.Types.Expand),
|
|
534
|
+
jobs: ArrayType(JobRow),
|
|
535
|
+
decisions: ArrayType(Plan.Types.DecisionMark),
|
|
536
|
+
ports: ArrayType(Plan.Types.Port),
|
|
537
|
+
});
|
|
538
|
+
const machines = $.const(new Map([
|
|
539
|
+
// Proposal flavours: forecast ghost · proposed cut · declined.
|
|
540
|
+
["L1-M07", { series: "flavours", program: "", sub: none, value: some("80 t"), expand: none,
|
|
541
|
+
jobs: [
|
|
542
|
+
{ key: "run", phase: "RUN", batch: some("B-197"), start: week(27n), end: week(30n), tonnes: some(64.0), state: variant("in-progress", null) },
|
|
543
|
+
{ key: "gho", phase: "FORECAST", batch: none, start: week(30n), end: week(32n), tonnes: none, state: variant("estimated", null) },
|
|
544
|
+
{ key: "rem", phase: "CUT", batch: none, start: week(33n), end: week(35n), tonnes: none, state: variant("proposed", variant("removed", null)) },
|
|
545
|
+
{ key: "rej", phase: "DECLINED", batch: none, start: week(36n), end: week(38n), tonnes: none, state: variant("rejected", null) },
|
|
546
|
+
], decisions: [], ports: [] }],
|
|
547
|
+
// Tonnage + an applied decision + a port on a stacked two-line
|
|
548
|
+
// gutter; the EXPAND DECLARATION is row data (R2) — the render is
|
|
549
|
+
// the root's expandRender resolver.
|
|
550
|
+
["L1-M09", { series: "detail", program: "", sub: some("cap 120 t"), value: none,
|
|
551
|
+
expand: some({ height: some("152px"), axis: variant("dim", null) }),
|
|
552
|
+
jobs: [
|
|
553
|
+
{ key: "a", phase: "RUN", batch: some("B-208"), start: week(27n), end: week(31n), tonnes: some(112.0), state: variant("actual", null) },
|
|
554
|
+
{ key: "b", phase: "RUN", batch: some("B-231"), start: week(31n), end: week(34n), tonnes: some(104.0), state: variant("proposed", variant("recommended", null)) },
|
|
555
|
+
],
|
|
556
|
+
decisions: [{ key: "d1", at: Plan.at.time(week(31n)), applied: true }],
|
|
557
|
+
ports: [{ at: Plan.at.time(week(31n)), label: some("−24 t") }] }],
|
|
558
|
+
// The rollup series — one union parent per program (renderer-derived bands).
|
|
559
|
+
["L1-M03", { series: "rollup", program: "Program A", sub: none, value: none, expand: none,
|
|
560
|
+
jobs: [
|
|
561
|
+
{ key: "b214", phase: "RUN", batch: some("B-214"), start: week(28n), end: week(31n), tonnes: some(96.0), state: variant("actual", null) },
|
|
562
|
+
{ key: "b221", phase: "RUN", batch: some("B-221"), start: week(32n), end: week(35n), tonnes: some(88.0), state: variant("proposed", variant("recommended", null)) },
|
|
563
|
+
], decisions: [], ports: [] }],
|
|
564
|
+
["L2-M11", { series: "rollup", program: "Program A", sub: none, value: none, expand: none,
|
|
565
|
+
jobs: [{ key: "b241", phase: "RUN", batch: some("B-241"), start: week(29n), end: week(33n), tonnes: some(92.0), state: variant("confirmed", null) }],
|
|
566
|
+
decisions: [], ports: [] }],
|
|
567
|
+
// Linked despatch whose run starts BEYOND the window — in links
|
|
568
|
+
// focus its landing renders as the edge fade.
|
|
569
|
+
["dsp", { series: "despatch", program: "", sub: none, value: none, expand: none,
|
|
570
|
+
jobs: [{ key: "d1", phase: "DSP", batch: none, start: week(39n), end: week(42n), tonnes: some(91.0), state: variant("proposed", variant("recommended", null)) }],
|
|
571
|
+
decisions: [], ports: [] }],
|
|
572
|
+
// The byStatus series inside the Line 2 strip.
|
|
573
|
+
["L2-M12", { series: "line2", program: "Program B", sub: none, value: none, expand: none,
|
|
574
|
+
jobs: [
|
|
575
|
+
{ key: "r1", phase: "RUN", batch: some("B-198"), start: week(28n), end: week(32n), tonnes: some(64.0), state: variant("actual", null) },
|
|
576
|
+
{ key: "r2", phase: "RUN", batch: some("B-202"), start: week(30n), end: week(34n), tonnes: some(40.0), state: variant("proposed", variant("recommended", null)) },
|
|
577
|
+
], decisions: [], ports: [] }],
|
|
578
|
+
]), DictType(StringType, MachineRow));
|
|
579
|
+
// Raw jobs → runs, once — every span series shares the mapping.
|
|
580
|
+
const jobRuns = $.const(East.function([ArrayType(JobRow)], ArrayType(Plan.Types.Run), (_$, jobs) => jobs.map(($, j) => {
|
|
581
|
+
const noQuantity = $.const(none, OptionType(StringType));
|
|
582
|
+
const quantity = $.let(j.tonnes.match({
|
|
583
|
+
some: (_$, t) => East.value(some(East.str `${East.Float.printFixed(t, 0n)} t`), OptionType(StringType)),
|
|
584
|
+
none: (_$) => noQuantity,
|
|
585
|
+
}), OptionType(StringType));
|
|
586
|
+
const label = $.let(j.batch.match({
|
|
587
|
+
some: (_$, b) => East.str `${j.phase} · ${b}`,
|
|
588
|
+
none: (_$) => j.phase,
|
|
589
|
+
}), StringType);
|
|
590
|
+
const run = $.let({
|
|
591
|
+
key: j.key, start: Plan.at.time(j.start), end: Plan.at.time(j.end), label,
|
|
592
|
+
quantity, qty: j.tonnes, state: j.state,
|
|
593
|
+
status: none, moved: none, icon: none,
|
|
594
|
+
}, Plan.Types.Run);
|
|
595
|
+
return run;
|
|
596
|
+
})));
|
|
597
|
+
const series = $.const([
|
|
598
|
+
Plan.series.span(MachineRow, {
|
|
599
|
+
key: "flavours", title: "Flavours",
|
|
600
|
+
match: r => r.series.equal("flavours"),
|
|
601
|
+
label: (_r, k) => k, id: true,
|
|
602
|
+
value: r => r.value,
|
|
603
|
+
runs: r => jobRuns(r.jobs),
|
|
604
|
+
}),
|
|
605
|
+
Plan.series.span(MachineRow, {
|
|
606
|
+
key: "detail", title: "Detail",
|
|
607
|
+
match: r => r.series.equal("detail"),
|
|
608
|
+
label: (_r, k) => k, id: true, stacked: true,
|
|
609
|
+
sub: r => r.sub, expand: r => r.expand,
|
|
610
|
+
runs: r => jobRuns(r.jobs), decisions: r => r.decisions, ports: r => r.ports,
|
|
611
|
+
}),
|
|
612
|
+
Plan.series.span(MachineRow, {
|
|
613
|
+
key: "rollup", title: "Rollup",
|
|
614
|
+
match: r => r.series.equal("rollup"),
|
|
615
|
+
label: (_r, k) => k, id: true,
|
|
616
|
+
runs: r => jobRuns(r.jobs),
|
|
617
|
+
groupBy: [r => r.program], rollup: "union", unit: "t",
|
|
618
|
+
}),
|
|
619
|
+
Plan.series.span(MachineRow, {
|
|
620
|
+
key: "despatch", title: "Despatch",
|
|
621
|
+
match: r => r.series.equal("despatch"),
|
|
622
|
+
label: (_r, k) => k, id: true,
|
|
623
|
+
runs: r => jobRuns(r.jobs),
|
|
624
|
+
}),
|
|
625
|
+
Plan.series.group(MachineRow, { key: "line2", label: "Line 2", meta: "1 rs" }, [
|
|
626
|
+
Plan.series.span(MachineRow, {
|
|
627
|
+
key: "line2", title: "Line2",
|
|
628
|
+
match: r => r.series.equal("line2"),
|
|
629
|
+
label: (_r, k) => k, id: true,
|
|
630
|
+
runs: r => jobRuns(r.jobs),
|
|
631
|
+
groupBy: [r => r.program], rollup: "byStatus", unit: "t",
|
|
632
|
+
}),
|
|
633
|
+
]),
|
|
634
|
+
], ArrayType(Plan.Types.Series(MachineRow)));
|
|
635
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
636
|
+
// The R2 developer render — the ROOT's resolver, called with the
|
|
637
|
+
// focused row's ref; ONE function serves every declaring row.
|
|
638
|
+
const util = $.let(East.Array.generate(8n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(13n).remainder(40n).toFloat().add(55.0) })));
|
|
639
|
+
const expandRender = $.const(East.function([Plan.Types.RowRef], UIComponentType, (_$, _ref) => (_jsx(Chart, { layers: [Chart.Column(util, { x: r => r.week, y: r => r.pct })], height: 100, grid: false }))));
|
|
640
|
+
// The generalized popover resolver — decision diamonds ride the mark
|
|
641
|
+
// arm of the element ref.
|
|
642
|
+
const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
|
|
643
|
+
const noBody = $.const(none, OptionType(UIComponentType));
|
|
644
|
+
return ref.match({
|
|
645
|
+
mark: (_$, ev) => ev.mark.equal("d1").ifElse(() => some(_jsx(Text, { children: "Approved by run 411." })), () => noBody),
|
|
646
|
+
}, _$ => noBody);
|
|
647
|
+
}));
|
|
648
|
+
return (_jsx(Plan, { expandRender: expandRender, popover: popover,
|
|
649
|
+
// A denser gutter (value + carets) — widen it (the shared
|
|
650
|
+
// CSS-px height/width vocabulary).
|
|
651
|
+
style: { gutterWidth: "200px" }, axis: axis,
|
|
652
|
+
// The link graph (R1) — hover a linked row for the ⌁ control.
|
|
653
|
+
// The edges deliberately cover the routing permutations:
|
|
654
|
+
// forward, a same-row seam feed, loopbacks, a rising loop,
|
|
655
|
+
// and an off-window landing.
|
|
656
|
+
links: [
|
|
657
|
+
Plan.link({ from: "L1-M07", fromRun: "run", to: "L1-M09", toRun: "a", quantity: 24, label: "24 t" }),
|
|
658
|
+
Plan.link({ from: "L1-M09", fromRun: "a", to: "L1-M09", toRun: "b", quantity: 40, label: "40 t" }),
|
|
659
|
+
Plan.link({ from: "L1-M09", fromRun: "b", to: "L1-M03", toRun: "b221", quantity: 88, label: "88 t" }),
|
|
660
|
+
Plan.link({ from: "L1-M03", fromRun: "b214", to: "L2-M11", toRun: "b241", quantity: 32, label: "32 t" }),
|
|
661
|
+
Plan.link({ from: "L2-M11", fromRun: "b241", to: "L1-M09", toRun: "b", quantity: 18, label: "18 t" }),
|
|
662
|
+
Plan.link({ from: "L1-M09", fromRun: "b", to: "dsp", toRun: "d1", quantity: 91, label: "91 t" }),
|
|
663
|
+
], data: machines, series: series }));
|
|
664
|
+
}),
|
|
665
|
+
inputs: [],
|
|
666
|
+
});
|
|
667
|
+
export const planBucketRows = example({
|
|
668
|
+
keywords: ["Plan", "data", "series", "buckets", "Planner", "lane", "lanes", "AM", "PM", "event", "tile", "marker", "tone", "color", "colorPalette", "stretch", "pulse", "icon", "hovercard", "popover", "mixed", "unbucketed", "group", "match", "gutter", "raw"],
|
|
669
|
+
description: "Bucket rows over one dock source — tiles derived in the accessor, and stored tile records with lanes, tones, colours and markers",
|
|
670
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
671
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
672
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
673
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
674
|
+
return w1.addWeeks(n.subtract(1n));
|
|
675
|
+
}));
|
|
676
|
+
const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
|
|
677
|
+
const DockRow = StructType({
|
|
678
|
+
series: StringType, label: StringType,
|
|
679
|
+
sub: OptionType(StringType), value: OptionType(StringType),
|
|
680
|
+
lanes: ArrayType(Plan.Types.Lane),
|
|
681
|
+
allocations: ArrayType(AllocRow),
|
|
682
|
+
tiles: ArrayType(Plan.Types.BucketEvent),
|
|
683
|
+
markers: ArrayType(Plan.Types.CellMarker),
|
|
684
|
+
});
|
|
685
|
+
// Raw weekly allocations; every third one is a proposal.
|
|
686
|
+
const inbound = $.let(East.Array.generate(6n, AllocRow, ($, i) => {
|
|
687
|
+
const confirmed = $.const(variant("confirmed", null), EventStateType);
|
|
688
|
+
const recommended = $.const(variant("proposed", variant("recommended", null)), EventStateType);
|
|
689
|
+
return {
|
|
690
|
+
key: East.str `a${East.print(i)}`,
|
|
691
|
+
at: week(i.add(27n)),
|
|
692
|
+
state: i.remainder(3n).equal(2n).ifElse(() => recommended, () => confirmed),
|
|
693
|
+
};
|
|
694
|
+
}));
|
|
695
|
+
const docks = $.const(new Map([
|
|
696
|
+
["dock2", { series: "inbound", label: "Dock 2", sub: none, value: some("load/wk"),
|
|
697
|
+
lanes: [], allocations: inbound, tiles: [],
|
|
698
|
+
markers: [{ at: Plan.at.time(week(29n)), lane: none, status: variant("warning", null), message: "capacity 90%" }] }],
|
|
699
|
+
// The grammar showcase — tiles stored IN the element vocabulary
|
|
700
|
+
// (plain `PlanBucketEventType` records; no builders in data).
|
|
701
|
+
["dock5", { series: "outbound", label: "Dock 5", sub: some("day · am/pm"), value: none,
|
|
702
|
+
lanes: [{ key: "am", label: some("AM") }, { key: "pm", label: some("PM") }],
|
|
703
|
+
allocations: [],
|
|
704
|
+
tiles: [
|
|
705
|
+
{ key: "m1", at: Plan.at.time(week(27n)), lane: some("am"), label: none, icon: none, state: variant("confirmed", null),
|
|
706
|
+
tone: none, color: none, colorPalette: none, stretch: none, content: none, animation: none },
|
|
707
|
+
{ key: "m2", at: Plan.at.time(week(27n)), lane: some("pm"), label: none, icon: none, state: variant("confirmed", null),
|
|
708
|
+
tone: some(variant("warning", null)), color: none, colorPalette: none, stretch: none, content: none, animation: none },
|
|
709
|
+
{ key: "m3", at: Plan.at.time(week(28n)), lane: some("am"), label: none, icon: none, state: variant("proposed", variant("recommended", null)),
|
|
710
|
+
tone: none, color: none, colorPalette: none, stretch: none, content: none, animation: some(variant("pulse", null)) },
|
|
711
|
+
{ key: "m4", at: Plan.at.time(week(29n)), lane: none, label: some("MIXED"), icon: none, state: variant("confirmed", null),
|
|
712
|
+
tone: none, color: none, colorPalette: none, stretch: some(variant("horizontal", null)),
|
|
713
|
+
content: some({ horizontal: some(variant("center", null)), vertical: none }), animation: none },
|
|
714
|
+
{ key: "m5", at: Plan.at.time(week(30n)), lane: some("pm"), label: none,
|
|
715
|
+
icon: some({ prefix: "fas", name: "truck", label: none, style: none }),
|
|
716
|
+
state: variant("proposed", variant("recommended", null)),
|
|
717
|
+
tone: none, color: none, colorPalette: none, stretch: none, content: none, animation: none },
|
|
718
|
+
{ key: "m6", at: Plan.at.time(week(31n)), lane: some("am"), label: some("QC"), icon: none, state: variant("estimated", null),
|
|
719
|
+
tone: none, color: none, colorPalette: none, stretch: none, content: none, animation: none },
|
|
720
|
+
// The colour channels (#571, from the Planner's colors
|
|
721
|
+
// preset): `color` is a raw token override, `colorPalette`
|
|
722
|
+
// recolours the whole lifecycle treatment.
|
|
723
|
+
{ key: "m7", at: Plan.at.time(week(32n)), lane: some("am"), label: some("S-A"), icon: none, state: variant("confirmed", null),
|
|
724
|
+
tone: none, color: some("teal.solid"), colorPalette: none, stretch: none, content: none, animation: none },
|
|
725
|
+
{ key: "m8", at: Plan.at.time(week(32n)), lane: some("pm"), label: some("S-B"), icon: none, state: variant("confirmed", null),
|
|
726
|
+
tone: none, color: none, colorPalette: some(variant("brand", null)), stretch: none, content: none, animation: none },
|
|
727
|
+
],
|
|
728
|
+
markers: [{ at: Plan.at.time(week(29n)), lane: none, status: variant("danger", null), message: "capacity breach" }] }],
|
|
729
|
+
]), DictType(StringType, DockRow));
|
|
730
|
+
const series = $.const([
|
|
731
|
+
// Raw allocations → resting tiles, in the accessor.
|
|
732
|
+
Plan.series.buckets(DockRow, {
|
|
733
|
+
key: "inbound", title: "Inbound",
|
|
734
|
+
match: r => r.series.equal("inbound"),
|
|
735
|
+
label: r => r.label,
|
|
736
|
+
value: r => r.value,
|
|
737
|
+
events: r => r.allocations.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
|
|
738
|
+
markers: r => r.markers,
|
|
739
|
+
}),
|
|
740
|
+
Plan.series.group(DockRow, { key: "outbound", label: "Docks · Out", meta: "1 rs" }, [
|
|
741
|
+
// Stored vocabulary records pass straight through.
|
|
742
|
+
Plan.series.buckets(DockRow, {
|
|
743
|
+
key: "outbound", title: "Outbound",
|
|
744
|
+
match: r => r.series.equal("outbound"),
|
|
745
|
+
label: r => r.label,
|
|
746
|
+
sub: r => r.sub,
|
|
747
|
+
lanes: r => r.lanes, events: r => r.tiles, markers: r => r.markers,
|
|
748
|
+
}),
|
|
749
|
+
]),
|
|
750
|
+
], ArrayType(Plan.Types.Series(DockRow)));
|
|
751
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
752
|
+
// The generalized resolvers — tiles ride the event arm of the ref.
|
|
753
|
+
const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
|
|
754
|
+
const noBody = $.const(none, OptionType(UIComponentType));
|
|
755
|
+
return ref.match({
|
|
756
|
+
event: (_$, ev) => ev.event.equal("m5").ifElse(() => some(_jsx(Text, { children: "Load 41 \u00B7 8 pallets" })), () => noBody),
|
|
757
|
+
}, _$ => noBody);
|
|
758
|
+
}));
|
|
759
|
+
const hover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
|
|
760
|
+
const noBody = $.const(none, OptionType(UIComponentType));
|
|
761
|
+
return ref.match({
|
|
762
|
+
event: (_$, ev) => ev.event.equal("m3").ifElse(() => some(_jsx(Text, { children: "Urgent \u2014 overtime window" })), () => noBody),
|
|
763
|
+
}, _$ => noBody);
|
|
764
|
+
}));
|
|
765
|
+
return (_jsx(Plan, { popover: popover, hover: hover, axis: axis, data: docks, series: series }));
|
|
766
|
+
}),
|
|
767
|
+
inputs: [],
|
|
768
|
+
});
|
|
769
|
+
export const planChartRows = example({
|
|
770
|
+
keywords: ["Plan", "data", "series", "chart", "layers", "spark", "expanded", "fixed", "refLine", "refBand", "refDot", "breach", "stacked", "dual-axis", "swatches", "Area", "Band", "Scatter", "Column", "Line", "domain", "tickValues", "group", "match", "gutter", "raw"],
|
|
771
|
+
description: "Chart rows over one measure source — one series per mark kind, plus annotations and a fixed dual-axis composition",
|
|
772
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
773
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
774
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
775
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
776
|
+
return w1.addWeeks(n.subtract(1n));
|
|
777
|
+
}));
|
|
778
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
779
|
+
const BandRow = StructType({ week: DateTimeType, lo: FloatType, hi: FloatType });
|
|
780
|
+
// ONE raw source — each series's points ride the row (`extra` carries
|
|
781
|
+
// the second set for stacked / dual compositions).
|
|
782
|
+
const ChartRow = StructType({
|
|
783
|
+
series: StringType, label: StringType,
|
|
784
|
+
sub: OptionType(StringType), value: OptionType(StringType),
|
|
785
|
+
points: ArrayType(MeasureRow),
|
|
786
|
+
extra: ArrayType(MeasureRow),
|
|
787
|
+
band: ArrayType(BandRow),
|
|
788
|
+
});
|
|
789
|
+
const pcts = $.const([96.1, 96.4, 96.8, 97.0, 96.2, 95.1, 93.4, 91.0, 88.9, 91.4, 93.8, 94.2], ArrayType(FloatType));
|
|
790
|
+
const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: pcts.get(i) })));
|
|
791
|
+
const cum = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.toFloat().multiply(14.0).add(40.0) })));
|
|
792
|
+
const out1 = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(23n).remainder(17n).toFloat().add(28.0) })));
|
|
793
|
+
const out2 = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(31n).remainder(13n).toFloat().add(14.0) })));
|
|
794
|
+
const ppm = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: i.multiply(37n).remainder(60n).toFloat().add(120.0) })));
|
|
795
|
+
const band = $.let(East.Array.generate(12n, BandRow, (_$, i) => ({ week: week(i.add(27n)), lo: pcts.get(i).subtract(3.0), hi: pcts.get(i).add(3.0) })));
|
|
796
|
+
const measures = $.const(new Map([
|
|
797
|
+
["spark", { series: "spark", label: "COVERAGE", sub: none, value: some("94.2%"), points: coverage, extra: [], band: [] }],
|
|
798
|
+
["cum", { series: "cum", label: "CUMULATIVE · t", sub: none, value: some("194 t"), points: cum, extra: [], band: [] }],
|
|
799
|
+
["stacked", { series: "stacked", label: "OUTPUT · t", sub: some("t/wk"), value: none, points: out1, extra: out2, band: [] }],
|
|
800
|
+
["ppm", { series: "ppm", label: "DEFECTS · ppm", sub: none, value: some("161"), points: ppm, extra: [], band: [] }],
|
|
801
|
+
["refs", { series: "refs", label: "COVERAGE + REFS", sub: none, value: none, points: coverage, extra: [], band: [] }],
|
|
802
|
+
["dual", { series: "dual", label: "OUT + COVERAGE", sub: none, value: none, points: out1, extra: coverage, band }],
|
|
803
|
+
]), DictType(StringType, ChartRow));
|
|
804
|
+
const series = $.const([
|
|
805
|
+
// Line — the KPI spark with a breach threshold; the caret opens
|
|
806
|
+
// it to a custom 120px (expandedHeight, default 88).
|
|
807
|
+
Plan.series.chart(ChartRow, {
|
|
808
|
+
key: "spark", title: "Spark",
|
|
809
|
+
match: r => r.series.equal("spark"),
|
|
810
|
+
label: r => r.label, id: true,
|
|
811
|
+
value: r => r.value, status: _r => some(variant("warning", null)),
|
|
812
|
+
height: "spark", expandable: true, expandedHeight: "120px",
|
|
813
|
+
layers: r => [Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } })],
|
|
814
|
+
}),
|
|
815
|
+
// Area — the cumulative fill.
|
|
816
|
+
Plan.series.chart(ChartRow, {
|
|
817
|
+
key: "cum", title: "Cumulative",
|
|
818
|
+
match: r => r.series.equal("cum"),
|
|
819
|
+
label: r => r.label, id: true, value: r => r.value,
|
|
820
|
+
layers: r => [Chart.Area(r.points, { x: p => p.week, y: p => p.pct })],
|
|
821
|
+
}),
|
|
822
|
+
// Columns — the row's two point sets stacked by one series id,
|
|
823
|
+
// on a two-line gutter (label over sub).
|
|
824
|
+
Plan.series.chart(ChartRow, {
|
|
825
|
+
key: "stacked", title: "Stacked",
|
|
826
|
+
match: r => r.series.equal("stacked"),
|
|
827
|
+
label: r => r.label, id: true, stacked: true, sub: r => r.sub,
|
|
828
|
+
layers: r => [
|
|
829
|
+
Plan.layer(Chart.Column(r.points, { x: p => p.week, y: p => p.pct }), { series: "L1" }),
|
|
830
|
+
Plan.layer(Chart.Column(r.extra, { x: p => p.week, y: p => p.pct }), { series: "L2" }),
|
|
831
|
+
],
|
|
832
|
+
}),
|
|
833
|
+
// Scatter — the defect cloud.
|
|
834
|
+
Plan.series.chart(ChartRow, {
|
|
835
|
+
key: "ppm", title: "Ppm",
|
|
836
|
+
match: r => r.series.equal("ppm"),
|
|
837
|
+
label: r => r.label, id: true, value: r => r.value,
|
|
838
|
+
layers: r => [Chart.Scatter(r.points, { x: p => p.week, y: p => p.pct })],
|
|
839
|
+
}),
|
|
840
|
+
// Line + every annotation kind, at expanded density.
|
|
841
|
+
Plan.series.chart(ChartRow, {
|
|
842
|
+
key: "refs", title: "Refs",
|
|
843
|
+
match: r => r.series.equal("refs"),
|
|
844
|
+
label: r => r.label, id: true,
|
|
845
|
+
height: "expanded",
|
|
846
|
+
layers: r => [
|
|
847
|
+
Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
|
|
848
|
+
Chart.refLine({ y: 100, label: "TARGET 100" }),
|
|
849
|
+
Chart.refBand({ x: [week(34n), week(36n)], label: "CRUNCH" }),
|
|
850
|
+
Chart.refDot({ x: week(36n), y: 91.4, label: "LOW" }),
|
|
851
|
+
],
|
|
852
|
+
}),
|
|
853
|
+
// The composed dual-axis chart inside a static group strip; axes
|
|
854
|
+
// take Chart.Root's vocabulary — domain / tickValues. Output
|
|
855
|
+
// columns scale left; the coverage line + its band scale right.
|
|
856
|
+
Plan.series.group(ChartRow, { key: "quality", label: "Quality", meta: "1 rs" }, [
|
|
857
|
+
Plan.series.chart(ChartRow, {
|
|
858
|
+
key: "dual", title: "Dual",
|
|
859
|
+
match: r => r.series.equal("dual"),
|
|
860
|
+
label: r => r.label, id: true,
|
|
861
|
+
height: Plan.fixed("120px"),
|
|
862
|
+
left: { domain: [0, 60], tickValues: [0, 25, 50] },
|
|
863
|
+
right: { domain: [80, 105], tickValues: [85, 95, 105] },
|
|
864
|
+
swatches: [{ color: "ink.3", label: "out" }, { color: "brand.d", label: "cov · rh" }],
|
|
865
|
+
layers: r => [
|
|
866
|
+
Chart.Column(r.points, { x: p => p.week, y: p => p.pct }),
|
|
867
|
+
Plan.layer(Chart.Line(r.extra, { x: p => p.week, y: p => p.pct }), { axis: "right" }),
|
|
868
|
+
Plan.layer(Chart.Band(r.band, { x: p => p.week, low: p => p.lo, high: p => p.hi }), { axis: "right" }),
|
|
869
|
+
],
|
|
870
|
+
}),
|
|
871
|
+
]),
|
|
872
|
+
], ArrayType(Plan.Types.Series(ChartRow)));
|
|
873
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
874
|
+
return (_jsx(Plan, { axis: axis, data: measures, series: series }));
|
|
875
|
+
}),
|
|
876
|
+
inputs: [],
|
|
877
|
+
});
|
|
878
|
+
export const planHeatRows = example({
|
|
879
|
+
keywords: ["Plan", "data", "series", "heat", "Matrix", "cells", "depth", "aggregate", "mean", "groupBy", "scale", "warnAt", "weightCells", "segmentCells", "segment", "no-data", "hatch", "group", "match", "gutter", "raw"],
|
|
880
|
+
description: "Heat rows over one line source — colour-depth cells under an aggregate parent, weight bars, and status segments",
|
|
881
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
882
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
883
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
884
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
885
|
+
return w1.addWeeks(n.subtract(1n));
|
|
886
|
+
}));
|
|
887
|
+
const HeatRow = StructType({
|
|
888
|
+
series: StringType, line: StringType, label: StringType,
|
|
889
|
+
sub: OptionType(StringType),
|
|
890
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
891
|
+
weights: ArrayType(Plan.Types.WeightCell),
|
|
892
|
+
segs: ArrayType(Plan.Types.SegmentCell),
|
|
893
|
+
});
|
|
894
|
+
const pcts = $.const([46.0, 52.0, 58.0, 61.0, 66.0, 72.0, 78.0, 84.0, 90.0, 96.0, 98.0, 92.0], ArrayType(FloatType));
|
|
895
|
+
const cells = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
|
|
896
|
+
at: Plan.at.time(week(i.add(27n))),
|
|
897
|
+
value: i.equal(4n).ifElse(() => none, () => some(pcts.get(i))), // W31 = no data
|
|
898
|
+
label: i.equal(4n).ifElse(() => none, () => some(East.Float.printFixed(pcts.get(i), 0n))),
|
|
899
|
+
})));
|
|
900
|
+
// Booked-vs-free fractions; the back half is the planned pale tail.
|
|
901
|
+
const weights = $.let(East.Array.generate(6n, Plan.Types.WeightCell, (_$, i) => ({
|
|
902
|
+
at: Plan.at.time(week(i.multiply(2n).add(27n))),
|
|
903
|
+
fraction: i.toFloat().multiply(-0.11).add(0.9),
|
|
904
|
+
planned: i.greaterEqual(3n),
|
|
905
|
+
})));
|
|
906
|
+
const lines = $.const(new Map([
|
|
907
|
+
["m03h", { series: "depth", line: "Line 1", label: "L1-M03", sub: none, cells, weights: [], segs: [] }],
|
|
908
|
+
["m04h", { series: "depth", line: "Line 1", label: "L1-M04", sub: none, cells, weights: [], segs: [] }],
|
|
909
|
+
["booked", { series: "booked", line: "", label: "Crew A", sub: some("booked h"), cells: [], weights, segs: [] }],
|
|
910
|
+
// Segment compositions — plain `{ fill, weight, label }` records.
|
|
911
|
+
["pack", { series: "segments", line: "", label: "Pack line", sub: some("capacity"), cells: [], weights: [],
|
|
912
|
+
segs: [
|
|
913
|
+
{ at: Plan.at.time(week(27n)), segments: [
|
|
914
|
+
{ fill: variant("success", null), weight: 60.0, label: some("60%") },
|
|
915
|
+
{ fill: variant("warning", null), weight: 25.0, label: some("25%") },
|
|
916
|
+
{ fill: variant("slack", null), weight: 15.0, label: none },
|
|
917
|
+
] },
|
|
918
|
+
{ at: Plan.at.time(week(28n)), segments: [
|
|
919
|
+
{ fill: variant("success", null), weight: 70.0, label: some("70%") },
|
|
920
|
+
{ fill: variant("slack", null), weight: 30.0, label: none },
|
|
921
|
+
] },
|
|
922
|
+
{ at: Plan.at.time(week(29n)), segments: [
|
|
923
|
+
{ fill: variant("danger", null), weight: 40.0, label: some("40%") },
|
|
924
|
+
{ fill: variant("free", null), weight: 60.0, label: none },
|
|
925
|
+
] },
|
|
926
|
+
] }],
|
|
927
|
+
]), DictType(StringType, HeatRow));
|
|
928
|
+
const series = $.const([
|
|
929
|
+
// The aggregate-mean parent derives per discovered line value.
|
|
930
|
+
Plan.series.heat(HeatRow, {
|
|
931
|
+
key: "depth", title: "Depth",
|
|
932
|
+
match: r => r.series.equal("depth"),
|
|
933
|
+
label: r => r.label, id: true,
|
|
934
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 95 }),
|
|
935
|
+
groupBy: [r => r.line], aggregate: "mean", scale: { min: 0, max: 100, warnAt: 95 },
|
|
936
|
+
}),
|
|
937
|
+
Plan.series.heat(HeatRow, {
|
|
938
|
+
key: "booked", title: "Booked",
|
|
939
|
+
match: r => r.series.equal("booked"),
|
|
940
|
+
label: r => r.label,
|
|
941
|
+
sub: r => r.sub,
|
|
942
|
+
cells: r => Plan.weightCells(r.weights),
|
|
943
|
+
}),
|
|
944
|
+
Plan.series.group(HeatRow, { key: "packing", label: "Packing", meta: "1 rs" }, [
|
|
945
|
+
Plan.series.heat(HeatRow, {
|
|
946
|
+
key: "segments", title: "Segments",
|
|
947
|
+
match: r => r.series.equal("segments"),
|
|
948
|
+
label: r => r.label,
|
|
949
|
+
sub: r => r.sub,
|
|
950
|
+
cells: r => Plan.segmentCells(r.segs),
|
|
951
|
+
}),
|
|
952
|
+
]),
|
|
953
|
+
], ArrayType(Plan.Types.Series(HeatRow)));
|
|
954
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
955
|
+
return (_jsx(Plan, { axis: axis, data: lines, series: series }));
|
|
956
|
+
}),
|
|
957
|
+
inputs: [],
|
|
958
|
+
});
|
|
959
|
+
export const planTableRows = example({
|
|
960
|
+
keywords: ["Plan", "data", "series", "table", "cells", "tableCells", "subtotal", "aggregate", "sum", "format", "emphasis", "footer", "groupBy", "nested", "depth", "em-dash", "neg", "match", "gutter", "tableSeries", "split", "horizontal", "vertical", "multi-value", "multi-cell", "stacked", "two-line", "strong", "muted", "rollup", "mirror", "position", "raw"],
|
|
961
|
+
description: "Table rows over one order source — nested groupBy subtotals, footer emphasis, and every split × gutter combination",
|
|
962
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
963
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
964
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
965
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
966
|
+
return w1.addWeeks(n.subtract(1n));
|
|
967
|
+
}));
|
|
968
|
+
const RawCell = StructType({ at: DateTimeType, value: OptionType(FloatType) });
|
|
969
|
+
// The RAW order record — actuals and the plan Δ as per-bucket value
|
|
970
|
+
// arrays; every display decision lives in the series configs.
|
|
971
|
+
const OrderRow = StructType({
|
|
972
|
+
series: StringType, name: StringType, top: StringType, program: StringType,
|
|
973
|
+
sub: OptionType(StringType),
|
|
974
|
+
act: ArrayType(RawCell),
|
|
975
|
+
plan: ArrayType(RawCell),
|
|
976
|
+
});
|
|
977
|
+
const act = $.let(East.Array.generate(12n, RawCell, (_$, i) => ({
|
|
978
|
+
at: week(i.add(27n)),
|
|
979
|
+
value: i.equal(9n).ifElse(() => none, () => some(i.toFloat().multiply(7.0).add(40.0))),
|
|
980
|
+
})));
|
|
981
|
+
const deltas = $.let(East.Array.generate(12n, RawCell, (_$, i) => ({
|
|
982
|
+
at: week(i.add(27n)),
|
|
983
|
+
value: i.remainder(4n).equal(3n).ifElse(() => none, () => some(i.toFloat().multiply(1.5).subtract(8.0))),
|
|
984
|
+
})));
|
|
985
|
+
const outflow = $.let(East.Array.generate(12n, RawCell, (_$, i) => ({
|
|
986
|
+
at: week(i.add(27n)),
|
|
987
|
+
value: some(i.toFloat().multiply(-3.0).subtract(12.0)),
|
|
988
|
+
})));
|
|
989
|
+
const orders = $.const(new Map([
|
|
990
|
+
// The nested series — two groupBy levels derive their subtotals.
|
|
991
|
+
["or-1188", { series: "orders", name: "OR-1188", top: "Despatches", program: "Program A", sub: none, act, plan: [] }],
|
|
992
|
+
["or-1204", { series: "orders", name: "OR-1204", top: "Despatches", program: "Program A", sub: none, act, plan: [] }],
|
|
993
|
+
["or-1219", { series: "orders", name: "OR-1219", top: "Despatches", program: "Program B", sub: none, act, plan: [] }],
|
|
994
|
+
["rt-0031", { series: "orders", name: "RT-0031", top: "Returns", program: "Program B", sub: none, act, plan: [] }],
|
|
995
|
+
// Footer emphasis + negative tone + the muted em-dash.
|
|
996
|
+
["net", { series: "net", name: "Net flow", top: "", program: "", sub: none, plan: [],
|
|
997
|
+
act: [
|
|
998
|
+
{ at: week(27n), value: some(22.0) }, { at: week(28n), value: some(-26.0) },
|
|
999
|
+
{ at: week(29n), value: none },
|
|
1000
|
+
] }],
|
|
1001
|
+
// Multi-value series — raw act + plan arrays per row. The SPLIT
|
|
1002
|
+
// (how the positions sit against each other) and the GUTTER (one
|
|
1003
|
+
// line or two) are independent choices, so all four combinations
|
|
1004
|
+
// are here: the pair that reads well depends on the numbers, not
|
|
1005
|
+
// on the split.
|
|
1006
|
+
["actplan", { series: "actplan", name: "Act · Δ plan", top: "", program: "", sub: some("t/wk"), act, plan: deltas }],
|
|
1007
|
+
["inout", { series: "inout", name: "In / out", top: "", program: "", sub: none, act, plan: outflow }],
|
|
1008
|
+
// Horizontal, on a ONE-line gutter — the pair reads as a single
|
|
1009
|
+
// fact ("booked beside free"), so a sub label would only repeat it.
|
|
1010
|
+
["sidebyside", { series: "sidebyside", name: "Booked · free", top: "", program: "", sub: none, act, plan: outflow }],
|
|
1011
|
+
// Vertical, on a TWO-line gutter — the stack needs the unit spelled
|
|
1012
|
+
// out, because the positions are the same measure at two times.
|
|
1013
|
+
["overunder", { series: "overunder", name: "Act / plan", top: "", program: "", sub: some("t/wk"), act, plan: deltas }],
|
|
1014
|
+
// GROUPED and multi-value: the subtotal parent mirrors its members,
|
|
1015
|
+
// an act subtotal beside a Δ subtotal.
|
|
1016
|
+
["fl-1", { series: "flow", name: "FL-2201", top: "Flows", program: "", sub: none, act, plan: deltas }],
|
|
1017
|
+
["fl-2", { series: "flow", name: "FL-2202", top: "Flows", program: "", sub: none, act, plan: deltas }],
|
|
1018
|
+
// The same, STACKED: members and their subtotal both put the two
|
|
1019
|
+
// positions on their own lines, so the parent has to grow too.
|
|
1020
|
+
["st-1", { series: "stack", name: "ST-3301", top: "Stacks", program: "", sub: none, act, plan: outflow }],
|
|
1021
|
+
["st-2", { series: "stack", name: "ST-3302", top: "Stacks", program: "", sub: none, act, plan: outflow }],
|
|
1022
|
+
]), DictType(StringType, OrderRow));
|
|
1023
|
+
const series = $.const([
|
|
1024
|
+
Plan.series.table(OrderRow, {
|
|
1025
|
+
key: "orders", title: "Orders",
|
|
1026
|
+
match: r => r.series.equal("orders"),
|
|
1027
|
+
label: r => r.name,
|
|
1028
|
+
cells: r => Plan.tableCells(r.act),
|
|
1029
|
+
groupBy: [r => r.top, r => r.program], aggregate: "sum",
|
|
1030
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1031
|
+
}),
|
|
1032
|
+
Plan.series.table(OrderRow, {
|
|
1033
|
+
key: "net", title: "Net",
|
|
1034
|
+
match: r => r.series.equal("net"),
|
|
1035
|
+
label: r => r.name, emphasis: "footer",
|
|
1036
|
+
cells: r => Plan.tableCells(r.act),
|
|
1037
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1038
|
+
}),
|
|
1039
|
+
// Per-POSITION style declared ONCE, in the CONFIG — a strong
|
|
1040
|
+
// rolled-up actual beside its muted, always-signed plan Δ.
|
|
1041
|
+
Plan.series.table(OrderRow, {
|
|
1042
|
+
key: "actplan", title: "Actual vs plan",
|
|
1043
|
+
match: r => r.series.equal("actplan"),
|
|
1044
|
+
label: r => r.name, stacked: true, sub: r => r.sub,
|
|
1045
|
+
series: r => [
|
|
1046
|
+
Plan.tableSeries({ strong: true, rollup: true, cells: Plan.tableCells(r.act) }),
|
|
1047
|
+
Plan.tableSeries({
|
|
1048
|
+
tone: "muted",
|
|
1049
|
+
format: Format.Number({ maximumFractionDigits: 0n, signDisplay: "always" }),
|
|
1050
|
+
cells: Plan.tableCells(r.plan),
|
|
1051
|
+
}),
|
|
1052
|
+
],
|
|
1053
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1054
|
+
}),
|
|
1055
|
+
// The VERTICAL split stacks the positions; the row grows.
|
|
1056
|
+
Plan.series.table(OrderRow, {
|
|
1057
|
+
key: "inout", title: "Inout",
|
|
1058
|
+
match: r => r.series.equal("inout"),
|
|
1059
|
+
label: r => r.name, split: "vertical",
|
|
1060
|
+
series: r => [
|
|
1061
|
+
Plan.tableSeries({ cells: Plan.tableCells(r.act) }),
|
|
1062
|
+
Plan.tableSeries({ tone: "muted", cells: Plan.tableCells(r.plan) }),
|
|
1063
|
+
],
|
|
1064
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1065
|
+
}),
|
|
1066
|
+
// HORIZONTAL on a ONE-line gutter — the other half of the pair
|
|
1067
|
+
// above: the split is a cell-layout choice and the gutter a label
|
|
1068
|
+
// choice, so neither implies the other.
|
|
1069
|
+
Plan.series.table(OrderRow, {
|
|
1070
|
+
key: "sidebyside", title: "Side by side",
|
|
1071
|
+
match: r => r.series.equal("sidebyside"),
|
|
1072
|
+
label: r => r.name, split: "horizontal",
|
|
1073
|
+
series: r => [
|
|
1074
|
+
Plan.tableSeries({ strong: true, cells: Plan.tableCells(r.act) }),
|
|
1075
|
+
Plan.tableSeries({ tone: "muted", cells: Plan.tableCells(r.plan) }),
|
|
1076
|
+
],
|
|
1077
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1078
|
+
}),
|
|
1079
|
+
// A MULTI-VALUE series under a subtotal parent — every position
|
|
1080
|
+
// rolls up, so the parent shows an act subtotal beside a Δ subtotal
|
|
1081
|
+
// instead of collapsing to one number and looking complete. Flag a
|
|
1082
|
+
// position `rollup: true` to narrow it back to that one.
|
|
1083
|
+
Plan.series.table(OrderRow, {
|
|
1084
|
+
key: "flow", title: "Flow",
|
|
1085
|
+
match: r => r.series.equal("flow"),
|
|
1086
|
+
label: r => r.name,
|
|
1087
|
+
series: r => [
|
|
1088
|
+
Plan.tableSeries({ strong: true, cells: Plan.tableCells(r.act) }),
|
|
1089
|
+
Plan.tableSeries({
|
|
1090
|
+
tone: "muted",
|
|
1091
|
+
format: Format.Number({ maximumFractionDigits: 0n, signDisplay: "always" }),
|
|
1092
|
+
cells: Plan.tableCells(r.plan),
|
|
1093
|
+
}),
|
|
1094
|
+
],
|
|
1095
|
+
groupBy: [r => r.top], aggregate: "sum",
|
|
1096
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1097
|
+
}),
|
|
1098
|
+
// GROUPED and VERTICAL — the subtotal stacks its positions the way
|
|
1099
|
+
// its members do. The parent carries no series of its own (they are
|
|
1100
|
+
// derived), so its height comes from the DERIVED count: estimating
|
|
1101
|
+
// from its own empty list would call this a one-line row and render
|
|
1102
|
+
// it as two.
|
|
1103
|
+
Plan.series.table(OrderRow, {
|
|
1104
|
+
key: "stack", title: "Stack",
|
|
1105
|
+
match: r => r.series.equal("stack"),
|
|
1106
|
+
label: r => r.name, split: "vertical",
|
|
1107
|
+
series: r => [
|
|
1108
|
+
Plan.tableSeries({ strong: true, cells: Plan.tableCells(r.act) }),
|
|
1109
|
+
Plan.tableSeries({ tone: "muted", cells: Plan.tableCells(r.plan) }),
|
|
1110
|
+
],
|
|
1111
|
+
groupBy: [r => r.top], aggregate: "sum",
|
|
1112
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1113
|
+
}),
|
|
1114
|
+
// VERTICAL on a TWO-line gutter — the remaining combination, and
|
|
1115
|
+
// the one that grows the row in BOTH directions at once.
|
|
1116
|
+
Plan.series.table(OrderRow, {
|
|
1117
|
+
key: "overunder", title: "Over / under",
|
|
1118
|
+
match: r => r.series.equal("overunder"),
|
|
1119
|
+
label: r => r.name, split: "vertical", stacked: true, sub: r => r.sub,
|
|
1120
|
+
series: r => [
|
|
1121
|
+
Plan.tableSeries({ strong: true, rollup: true, cells: Plan.tableCells(r.act) }),
|
|
1122
|
+
Plan.tableSeries({
|
|
1123
|
+
tone: "muted",
|
|
1124
|
+
format: Format.Number({ maximumFractionDigits: 0n, signDisplay: "always" }),
|
|
1125
|
+
cells: Plan.tableCells(r.plan),
|
|
1126
|
+
}),
|
|
1127
|
+
],
|
|
1128
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1129
|
+
}),
|
|
1130
|
+
], ArrayType(Plan.Types.Series(OrderRow)));
|
|
1131
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
1132
|
+
return (_jsx(Plan, { axis: axis, data: orders, series: series }));
|
|
1133
|
+
}),
|
|
1134
|
+
inputs: [],
|
|
1135
|
+
});
|
|
1136
|
+
export const planCardRows = example({
|
|
1137
|
+
keywords: ["Plan", "data", "series", "cards", "Roster", "chip", "lifecycle", "confirmed", "recommended", "removed", "estimated", "icon", "popover", "stacked", "format", "axis", "group", "match", "gutter", "raw"],
|
|
1138
|
+
description: "Cards rows over one crew source — chip labels derived from hours × lifecycle, plus stored chip records",
|
|
1139
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
1140
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
1141
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
1142
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
1143
|
+
return w1.addWeeks(n.subtract(1n));
|
|
1144
|
+
}));
|
|
1145
|
+
const ShiftRow = StructType({
|
|
1146
|
+
key: StringType, from: DateTimeType, to: DateTimeType,
|
|
1147
|
+
hours: FloatType, state: EventStateType,
|
|
1148
|
+
});
|
|
1149
|
+
const CrewRow = StructType({
|
|
1150
|
+
series: StringType, name: StringType,
|
|
1151
|
+
sub: OptionType(StringType), value: OptionType(StringType),
|
|
1152
|
+
shifts: ArrayType(ShiftRow),
|
|
1153
|
+
chips: ArrayType(Plan.Types.Chip),
|
|
1154
|
+
});
|
|
1155
|
+
const crews = $.const(new Map([
|
|
1156
|
+
// RAW shifts — hours + lifecycle; chip labels derive client-side.
|
|
1157
|
+
["crewA", { series: "main", name: "Crew A", sub: some("152h → 168h"), value: none, chips: [], shifts: [
|
|
1158
|
+
{ key: "s1", from: week(27n), to: week(29n), hours: 80.0, state: variant("confirmed", null) },
|
|
1159
|
+
{ key: "s2", from: week(29n), to: week(31n), hours: 56.0, state: variant("proposed", variant("removed", null)) },
|
|
1160
|
+
{ key: "s3", from: week(31n), to: week(33n), hours: 64.0, state: variant("proposed", variant("recommended", null)) },
|
|
1161
|
+
{ key: "s4", from: week(34n), to: week(35n), hours: 48.0, state: variant("estimated", null) },
|
|
1162
|
+
] }],
|
|
1163
|
+
// STORED vocabulary — plain chip records (the §3.2 element
|
|
1164
|
+
// shapes), here carrying the shift-type icon.
|
|
1165
|
+
["crewB", { series: "pool", name: "Crew B", sub: none, value: some("128h"), shifts: [], chips: [
|
|
1166
|
+
{ key: "b1", from: Plan.at.time(week(28n)), to: Plan.at.time(week(31n)), label: "96h", state: variant("confirmed", null),
|
|
1167
|
+
icon: some({ prefix: "fas", name: "user-group", label: none, style: none }) },
|
|
1168
|
+
{ key: "b2", from: Plan.at.time(week(33n)), to: Plan.at.time(week(36n)), label: "+32h", state: variant("proposed", variant("recommended", null)), icon: none },
|
|
1169
|
+
] }],
|
|
1170
|
+
]), DictType(StringType, CrewRow));
|
|
1171
|
+
const series = $.const([
|
|
1172
|
+
Plan.series.cards(CrewRow, {
|
|
1173
|
+
key: "main", title: "Main",
|
|
1174
|
+
match: r => r.series.equal("main"),
|
|
1175
|
+
label: r => r.name, stacked: true,
|
|
1176
|
+
sub: r => r.sub,
|
|
1177
|
+
chips: r => r.shifts.map(($, s) => {
|
|
1178
|
+
const hrs = $.let(East.Float.printFixed(s.hours, 0n), StringType);
|
|
1179
|
+
// The `+` means ADDED hours, so it rides the proposal's
|
|
1180
|
+
// flavour, not the mere fact of being a proposal — a
|
|
1181
|
+
// `removed` shift is a proposal too, and prefixing it `+`
|
|
1182
|
+
// would read as the opposite of what it does.
|
|
1183
|
+
const label = $.let(s.state.match({
|
|
1184
|
+
proposed: (_$, p) => p.hasTag("removed").ifElse(() => East.str `${hrs}h`, () => East.str `+${hrs}h`),
|
|
1185
|
+
}, _$ => East.str `${hrs}h`), StringType);
|
|
1186
|
+
return Plan.chip({ key: s.key, from: s.from, to: s.to, label, state: s.state });
|
|
1187
|
+
}),
|
|
1188
|
+
}),
|
|
1189
|
+
Plan.series.group(CrewRow, { key: "pool", label: "Relief pool", meta: "1 rs" }, [
|
|
1190
|
+
Plan.series.cards(CrewRow, {
|
|
1191
|
+
key: "pool", title: "Pool",
|
|
1192
|
+
match: r => r.series.equal("pool"),
|
|
1193
|
+
label: r => r.name,
|
|
1194
|
+
value: r => r.value,
|
|
1195
|
+
chips: r => r.chips,
|
|
1196
|
+
}),
|
|
1197
|
+
]),
|
|
1198
|
+
], ArrayType(Plan.Types.Series(CrewRow)));
|
|
1199
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n), format: "D MMM" }));
|
|
1200
|
+
// The generalized popover resolver — chips ride the chip arm.
|
|
1201
|
+
const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
|
|
1202
|
+
const noBody = $.const(none, OptionType(UIComponentType));
|
|
1203
|
+
return ref.match({
|
|
1204
|
+
chip: (_$, ev) => ev.chip.equal("s3").ifElse(() => some(_jsx(Text, { children: "Overtime proposal." })), () => noBody),
|
|
1205
|
+
}, _$ => noBody);
|
|
1206
|
+
}));
|
|
1207
|
+
return (_jsx(Plan, { popover: popover, axis: axis, data: crews, series: series }));
|
|
1208
|
+
}),
|
|
1209
|
+
inputs: [],
|
|
1210
|
+
});
|
|
1211
|
+
export const planEventRows = example({
|
|
1212
|
+
keywords: ["Plan", "data", "series", "events", "mark", "milestone", "decision", "exception", "markKind", "applied", "icon", "label", "popover", "group", "match", "stacked", "gutter", "raw"],
|
|
1213
|
+
description: "Event rows over one stream source — milestone dots, decision diamonds, an exception, and a custom glyph",
|
|
1214
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
1215
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
1216
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
1217
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
1218
|
+
return w1.addWeeks(n.subtract(1n));
|
|
1219
|
+
}));
|
|
1220
|
+
const StreamRow = StructType({
|
|
1221
|
+
series: StringType, name: StringType,
|
|
1222
|
+
sub: OptionType(StringType), value: OptionType(StringType),
|
|
1223
|
+
marks: ArrayType(Plan.Types.EventMark),
|
|
1224
|
+
});
|
|
1225
|
+
const streams = $.const(new Map([
|
|
1226
|
+
["ms", { series: "main", name: "MILESTONES", sub: none, value: some("5"), marks: [
|
|
1227
|
+
{ key: "kick", at: Plan.at.time(week(28n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
|
|
1228
|
+
{ key: "d1", at: Plan.at.time(week(31n)), kind: variant("decision", { applied: true }), icon: none, label: none },
|
|
1229
|
+
{ key: "rel", at: Plan.at.time(week(33n)), kind: variant("milestone", null),
|
|
1230
|
+
icon: some({ prefix: "fas", name: "rocket", label: none, style: none }), label: some("REL 2.4") },
|
|
1231
|
+
{ key: "audit", at: Plan.at.time(week(35n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
|
|
1232
|
+
{ key: "d2", at: Plan.at.time(week(37n)), kind: variant("decision", { applied: false }), icon: none, label: some("×3") },
|
|
1233
|
+
] }],
|
|
1234
|
+
["release", { series: "programs", name: "RELEASES", sub: some("6-wk cadence"), value: none, marks: [
|
|
1235
|
+
{ key: "r1", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("2.3") },
|
|
1236
|
+
{ key: "r2", at: Plan.at.time(week(36n)), kind: variant("milestone", null), icon: none, label: some("2.4") },
|
|
1237
|
+
] }],
|
|
1238
|
+
]), DictType(StringType, StreamRow));
|
|
1239
|
+
const series = $.const([
|
|
1240
|
+
Plan.series.events(StreamRow, {
|
|
1241
|
+
key: "main-2", title: "Main",
|
|
1242
|
+
match: r => r.series.equal("main"),
|
|
1243
|
+
label: r => r.name, id: true,
|
|
1244
|
+
value: r => r.value,
|
|
1245
|
+
marks: r => r.marks,
|
|
1246
|
+
}),
|
|
1247
|
+
Plan.series.group(StreamRow, { key: "programs", label: "Programs", meta: "1 rs" }, [
|
|
1248
|
+
Plan.series.events(StreamRow, {
|
|
1249
|
+
key: "programs", title: "Programs",
|
|
1250
|
+
match: r => r.series.equal("programs"),
|
|
1251
|
+
label: r => r.name, id: true, stacked: true,
|
|
1252
|
+
sub: r => r.sub,
|
|
1253
|
+
marks: r => r.marks,
|
|
1254
|
+
}),
|
|
1255
|
+
]),
|
|
1256
|
+
], ArrayType(Plan.Types.Series(StreamRow)));
|
|
1257
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
1258
|
+
// The generalized popover resolver — event marks ride the mark arm.
|
|
1259
|
+
const popover = $.const(East.function([Plan.Types.ElementRef], OptionType(UIComponentType), ($, ref) => {
|
|
1260
|
+
const noBody = $.const(none, OptionType(UIComponentType));
|
|
1261
|
+
return ref.match({
|
|
1262
|
+
mark: (_$, ev) => ev.mark.equal("rel").ifElse(() => some(_jsx(Text, { children: "Go/no-go review." })), () => noBody),
|
|
1263
|
+
}, _$ => noBody);
|
|
1264
|
+
}));
|
|
1265
|
+
return (_jsx(Plan, { popover: popover, axis: axis, data: streams, series: series }));
|
|
1266
|
+
}),
|
|
1267
|
+
inputs: [],
|
|
1268
|
+
});
|
|
1269
|
+
export const planGroupedRows = example({
|
|
1270
|
+
keywords: ["Plan", "data", "series", "group", "groups", "strip", "by", "discovered", "summary", "summaryAggregate", "collapsed", "groupBy", "heterogeneous", "match", "meta", "nesting", "raw"],
|
|
1271
|
+
description: "Group strips in all three forms — static expanded, static collapsed, and discovered by a `by` accessor",
|
|
1272
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
1273
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
1274
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
1275
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
1276
|
+
return w1.addWeeks(n.subtract(1n));
|
|
1277
|
+
}));
|
|
1278
|
+
const JobRow = StructType({
|
|
1279
|
+
key: StringType, batch: StringType,
|
|
1280
|
+
start: DateTimeType, end: DateTimeType, state: EventStateType,
|
|
1281
|
+
});
|
|
1282
|
+
const LineRow = StructType({
|
|
1283
|
+
series: StringType, line: StringType, label: StringType,
|
|
1284
|
+
jobs: ArrayType(JobRow),
|
|
1285
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
1286
|
+
});
|
|
1287
|
+
const pcts = $.const([46.0, 52.0, 58.0, 61.0, 66.0, 72.0, 78.0, 84.0, 90.0, 96.0, 98.0, 92.0], ArrayType(FloatType));
|
|
1288
|
+
const cells = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
|
|
1289
|
+
at: Plan.at.time(week(i.add(27n))),
|
|
1290
|
+
value: some(pcts.get(i)),
|
|
1291
|
+
label: some(East.Float.printFixed(pcts.get(i), 0n)),
|
|
1292
|
+
})));
|
|
1293
|
+
const lines = $.const(new Map([
|
|
1294
|
+
// Static Line 1 — mixed kinds (a span child + a heat child).
|
|
1295
|
+
["m03", { series: "l1span", line: "", label: "L1-M03",
|
|
1296
|
+
jobs: [{ key: "r", batch: "B-214", start: week(28n), end: week(31n), state: variant("in-progress", null) }],
|
|
1297
|
+
cells: [] }],
|
|
1298
|
+
["m03h", { series: "l1heat", line: "", label: "L1-M03 load", jobs: [], cells }],
|
|
1299
|
+
// Static collapsed Line 2 — rests as its DECLARED mean strip.
|
|
1300
|
+
["l2", { series: "l2", line: "", label: "L2 load", jobs: [], cells }],
|
|
1301
|
+
// The DISCOVERED form — one strip per distinct line value. Named
|
|
1302
|
+
// apart from the static strips above so the panel reads as three
|
|
1303
|
+
// distinct forms rather than two repeated ones.
|
|
1304
|
+
["d-m21", { series: "byline", line: "Line 3", label: "L3-M21", jobs: [], cells }],
|
|
1305
|
+
["d-m22", { series: "byline", line: "Line 3", label: "L3-M22", jobs: [], cells }],
|
|
1306
|
+
["d-m31", { series: "byline", line: "Line 4", label: "L4-M31", jobs: [], cells }],
|
|
1307
|
+
]), DictType(StringType, LineRow));
|
|
1308
|
+
const series = $.const([
|
|
1309
|
+
Plan.series.group(LineRow, { key: "line1", label: "Line 1", meta: "2 rs · 82%" }, [
|
|
1310
|
+
Plan.series.span(LineRow, {
|
|
1311
|
+
key: "l1span", title: "L1span",
|
|
1312
|
+
match: r => r.series.equal("l1span"),
|
|
1313
|
+
label: r => r.label, id: true,
|
|
1314
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
1315
|
+
key: j.key, start: j.start, end: j.end,
|
|
1316
|
+
label: East.str `RUN · ${j.batch}`, state: j.state,
|
|
1317
|
+
})),
|
|
1318
|
+
}),
|
|
1319
|
+
Plan.series.heat(LineRow, {
|
|
1320
|
+
key: "l1heat", title: "L1heat",
|
|
1321
|
+
match: r => r.series.equal("l1heat"),
|
|
1322
|
+
label: r => r.label,
|
|
1323
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
|
|
1324
|
+
}),
|
|
1325
|
+
]),
|
|
1326
|
+
Plan.series.group(LineRow, { key: "line2", label: "Line 2", value: "98%", status: "warning", collapsed: true, summaryAggregate: "mean" }, [
|
|
1327
|
+
Plan.series.heat(LineRow, {
|
|
1328
|
+
key: "l2", title: "L2",
|
|
1329
|
+
match: r => r.series.equal("l2"),
|
|
1330
|
+
label: r => r.label,
|
|
1331
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 95 }),
|
|
1332
|
+
}),
|
|
1333
|
+
]),
|
|
1334
|
+
// DISCOVERED strips — one collapsed group per distinct `by`
|
|
1335
|
+
// value, wearing the member-count meta.
|
|
1336
|
+
Plan.series.group(LineRow, {
|
|
1337
|
+
key: "discovered", title: "Discovered lines",
|
|
1338
|
+
by: r => r.line, match: r => r.series.equal("byline"),
|
|
1339
|
+
collapsed: true, summaryAggregate: "mean",
|
|
1340
|
+
}, [
|
|
1341
|
+
Plan.series.heat(LineRow, {
|
|
1342
|
+
key: "byline", title: "By line",
|
|
1343
|
+
match: r => r.series.equal("byline"),
|
|
1344
|
+
label: r => r.label, id: true,
|
|
1345
|
+
cells: r => Plan.heatCells(r.cells),
|
|
1346
|
+
}),
|
|
1347
|
+
]),
|
|
1348
|
+
], ArrayType(Plan.Types.Series(LineRow)));
|
|
1349
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
1350
|
+
return (_jsx(Plan, { axis: axis, data: lines, series: series }));
|
|
1351
|
+
}),
|
|
1352
|
+
inputs: [],
|
|
1353
|
+
});
|
|
1354
|
+
// ============================================================================
|
|
1355
|
+
// planSeriesData — the minimal data + series introduction
|
|
1356
|
+
// ============================================================================
|
|
1357
|
+
export const planSeriesData = example({
|
|
1358
|
+
keywords: ["Plan", "data", "series", "match", "series", "variant", "span", "cards", "group", "rows", "groupBy", "rollup", "Series", "data-driven", "accessor", "raw", "one source"],
|
|
1359
|
+
description: "The data + series canvas, minimally — one raw source and one `Plan.series.*` entry per row series",
|
|
1360
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
1361
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
1362
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
1363
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
1364
|
+
return w1.addWeeks(n.subtract(1n));
|
|
1365
|
+
}));
|
|
1366
|
+
// The RAW domain shape — series discriminated by a variant field
|
|
1367
|
+
// (the natural ops-dataset form; the same rows page from a dataset).
|
|
1368
|
+
const JobRow = StructType({
|
|
1369
|
+
batch: StringType, start: DateTimeType, end: DateTimeType,
|
|
1370
|
+
tonnes: FloatType, state: EventStateType,
|
|
1371
|
+
});
|
|
1372
|
+
const ShiftRow = StructType({
|
|
1373
|
+
key: StringType, from: DateTimeType, to: DateTimeType,
|
|
1374
|
+
hours: FloatType, state: EventStateType,
|
|
1375
|
+
});
|
|
1376
|
+
const OpsRow = StructType({
|
|
1377
|
+
line: StringType,
|
|
1378
|
+
kind: VariantType({
|
|
1379
|
+
machine: StructType({ jobs: ArrayType(JobRow) }),
|
|
1380
|
+
crew: StructType({ shifts: ArrayType(ShiftRow) }),
|
|
1381
|
+
}),
|
|
1382
|
+
});
|
|
1383
|
+
const ops = $.const(new Map([
|
|
1384
|
+
["L1-M03", { line: "Line 1", kind: variant("machine", { jobs: [
|
|
1385
|
+
{ batch: "B-214", start: week(28n), end: week(31n), tonnes: 96.0, state: variant("in-progress", null) },
|
|
1386
|
+
{ batch: "B-221", start: week(32n), end: week(35n), tonnes: 88.0, state: variant("proposed", variant("recommended", null)) },
|
|
1387
|
+
] }) }],
|
|
1388
|
+
["L1-M04", { line: "Line 1", kind: variant("machine", { jobs: [
|
|
1389
|
+
{ batch: "B-208", start: week(27n), end: week(30n), tonnes: 112.0, state: variant("actual", null) },
|
|
1390
|
+
] }) }],
|
|
1391
|
+
["L2-M11", { line: "Line 2", kind: variant("machine", { jobs: [
|
|
1392
|
+
{ batch: "B-241", start: week(29n), end: week(33n), tonnes: 92.0, state: variant("confirmed", null) },
|
|
1393
|
+
] }) }],
|
|
1394
|
+
["crewA", { line: "Line 1", kind: variant("crew", { shifts: [
|
|
1395
|
+
{ key: "s1", from: week(27n), to: week(29n), hours: 80.0, state: variant("confirmed", null) },
|
|
1396
|
+
{ key: "s2", from: week(31n), to: week(33n), hours: 64.0, state: variant("proposed", variant("recommended", null)) },
|
|
1397
|
+
] }) }],
|
|
1398
|
+
]), DictType(StringType, OpsRow));
|
|
1399
|
+
// The series — real East values bound in the body, typed by the
|
|
1400
|
+
// constructor; canvas order = series order. The accessors are where
|
|
1401
|
+
// raw fields become canvas vocabulary: labels, quantity displays and
|
|
1402
|
+
// chip text all derive CLIENT-SIDE inside each series's stored make.
|
|
1403
|
+
const series = $.const([
|
|
1404
|
+
Plan.series.rows(OpsRow, { key: "chrome", title: "Milestones", subtitle: "one-off chrome" }, [Plan.events({ key: "ms", label: "MILESTONES", id: true, marks: [
|
|
1405
|
+
Plan.mark({ key: "kick", at: week(28n), kind: "milestone", label: "KICKOFF" }),
|
|
1406
|
+
Plan.mark({ key: "rel", at: week(33n), kind: "milestone", label: "REL 2.4" }),
|
|
1407
|
+
] })]),
|
|
1408
|
+
Plan.series.span(OpsRow, {
|
|
1409
|
+
key: "span-2", title: "Span",
|
|
1410
|
+
match: r => r.kind.hasTag("machine"),
|
|
1411
|
+
label: (_r, k) => k, id: true,
|
|
1412
|
+
runs: r => r.kind.unwrap("machine").jobs.map((_$, j) => Plan.run({
|
|
1413
|
+
key: j.batch, start: j.start, end: j.end,
|
|
1414
|
+
label: East.str `RUN · ${j.batch}`,
|
|
1415
|
+
quantity: East.str `${East.Float.printFixed(j.tonnes, 0n)} t`,
|
|
1416
|
+
qty: j.tonnes, state: j.state,
|
|
1417
|
+
})),
|
|
1418
|
+
groupBy: [r => r.line], rollup: "union", unit: "t",
|
|
1419
|
+
}),
|
|
1420
|
+
Plan.series.group(OpsRow, { key: "crews", label: "Crews", meta: "1 rs" }, [
|
|
1421
|
+
Plan.series.cards(OpsRow, {
|
|
1422
|
+
key: "cards-2", title: "Cards",
|
|
1423
|
+
match: r => r.kind.hasTag("crew"),
|
|
1424
|
+
label: (_r, k) => k,
|
|
1425
|
+
chips: r => r.kind.unwrap("crew").shifts.map(($, s) => {
|
|
1426
|
+
const hrs = $.let(East.Float.printFixed(s.hours, 0n), StringType);
|
|
1427
|
+
// `+` marks ADDED hours — a removed proposal keeps the
|
|
1428
|
+
// plain figure (see planCardRows for the full ladder).
|
|
1429
|
+
const label = $.let(s.state.match({
|
|
1430
|
+
proposed: (_$, p) => p.hasTag("removed").ifElse(() => East.str `${hrs}h`, () => East.str `+${hrs}h`),
|
|
1431
|
+
}, _$ => East.str `${hrs}h`), StringType);
|
|
1432
|
+
return Plan.chip({ key: s.key, from: s.from, to: s.to, label, state: s.state });
|
|
1433
|
+
}),
|
|
1434
|
+
}),
|
|
1435
|
+
]),
|
|
1436
|
+
], ArrayType(Plan.Types.Series(OpsRow)));
|
|
1437
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
1438
|
+
return (_jsx(Plan, { axis: axis, data: ops, series: series }));
|
|
1439
|
+
}),
|
|
1440
|
+
inputs: [],
|
|
1441
|
+
});
|
|
1442
|
+
// ============================================================================
|
|
1443
|
+
// planSeriesLibrary — the pickable series library (the DnD behavioral isolate)
|
|
1444
|
+
// ============================================================================
|
|
1445
|
+
export const planLibraryDnd = example({
|
|
1446
|
+
keywords: [
|
|
1447
|
+
"Plan", "data", "series", "library", "Pick", "pick", "Panel", "pickItems", "hidden",
|
|
1448
|
+
"toggle", "eye", "kind icon", "count", "group", "grouped", "discovered", "by", "rows",
|
|
1449
|
+
"chrome", "span", "buckets", "chart", "heat", "table", "cards", "events", "duplicate",
|
|
1450
|
+
"keySuffix", "keyPrefix", "same entity", "multiple views", "adjacent", "seek",
|
|
1451
|
+
"DnD", "drag", "drop", "onDrag", "canDrop", "sources", "add", "Reactive", "State", "#590",
|
|
1452
|
+
],
|
|
1453
|
+
description: "The series library across every row kind — duplicate kinds, groups, and a `keySuffix` pair showing one asset three ways",
|
|
1454
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
1455
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
1456
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
1457
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
1458
|
+
return w1.addWeeks(n.subtract(1n));
|
|
1459
|
+
}));
|
|
1460
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
1461
|
+
const JobRow = StructType({
|
|
1462
|
+
key: StringType, label: StringType,
|
|
1463
|
+
start: DateTimeType, end: DateTimeType, state: EventStateType,
|
|
1464
|
+
});
|
|
1465
|
+
const ShiftRow = StructType({
|
|
1466
|
+
key: StringType, from: DateTimeType, to: DateTimeType, label: StringType, state: EventStateType,
|
|
1467
|
+
});
|
|
1468
|
+
const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
|
|
1469
|
+
// ONE flat source (the `planExpand` shape): `pick` names the series
|
|
1470
|
+
// that claims the row, and every other channel is empty for the
|
|
1471
|
+
// series that do not use it. Each series therefore matches a
|
|
1472
|
+
// DISJOINT slice — two series over the same rows would emit the same
|
|
1473
|
+
// keys and collapse last-wins, which is a different demo.
|
|
1474
|
+
const OpsRow = StructType({
|
|
1475
|
+
pick: StringType, label: StringType, line: StringType,
|
|
1476
|
+
jobs: ArrayType(JobRow),
|
|
1477
|
+
points: ArrayType(MeasureRow),
|
|
1478
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
1479
|
+
allocs: ArrayType(AllocRow),
|
|
1480
|
+
nums: ArrayType(Plan.Types.TableCell),
|
|
1481
|
+
shifts: ArrayType(ShiftRow),
|
|
1482
|
+
marks: ArrayType(Plan.Types.EventMark),
|
|
1483
|
+
});
|
|
1484
|
+
const noJobs = $.const([], ArrayType(JobRow));
|
|
1485
|
+
const noPoints = $.const([], ArrayType(MeasureRow));
|
|
1486
|
+
const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
|
|
1487
|
+
const noAllocs = $.const([], ArrayType(AllocRow));
|
|
1488
|
+
const noNums = $.const([], ArrayType(Plan.Types.TableCell));
|
|
1489
|
+
const noShifts = $.const([], ArrayType(ShiftRow));
|
|
1490
|
+
const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
|
|
1491
|
+
const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
|
|
1492
|
+
const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
|
|
1493
|
+
const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
|
|
1494
|
+
at: Plan.at.time(week(i.multiply(2n).add(27n))),
|
|
1495
|
+
value: some(pcts.get(i)),
|
|
1496
|
+
label: some(East.Float.printFixed(pcts.get(i), 0n)),
|
|
1497
|
+
})));
|
|
1498
|
+
// The lifecycle states as consts, so each tag stays a LITERAL —
|
|
1499
|
+
// `variant(someString, null)` widens to `variant<string, null>` and
|
|
1500
|
+
// stops satisfying `EventStateType`.
|
|
1501
|
+
const ACTUAL = variant("actual", null);
|
|
1502
|
+
const CONFIRMED = variant("confirmed", null);
|
|
1503
|
+
const RUNNING = variant("in-progress", null);
|
|
1504
|
+
const PROPOSED = variant("proposed", variant("recommended", null));
|
|
1505
|
+
const base = {
|
|
1506
|
+
line: "", jobs: noJobs, points: noPoints, cells: noCells,
|
|
1507
|
+
allocs: noAllocs, nums: noNums, shifts: noShifts, marks: noMarks,
|
|
1508
|
+
};
|
|
1509
|
+
// Keys ORDER the canvas (#568), so they are numbered to place each
|
|
1510
|
+
// series' rows; group strips sort among the ROOTS by their own key.
|
|
1511
|
+
const ops = $.const(new Map([
|
|
1512
|
+
["10-util", { ...base, pick: "util", label: "UTIL %", points }],
|
|
1513
|
+
// ONE asset, THREE views — the span the machines series draws,
|
|
1514
|
+
// plus a chart and a table over these same two rows (see the
|
|
1515
|
+
// `keySuffix` pair below). The extra channels are what those
|
|
1516
|
+
// two read; nothing about the row is duplicated.
|
|
1517
|
+
["20-m03", { ...base, pick: "machines", label: "L1-M03",
|
|
1518
|
+
jobs: [{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: RUNNING }],
|
|
1519
|
+
points,
|
|
1520
|
+
nums: [
|
|
1521
|
+
{ at: Plan.at.time(week(28n)), value: some(96.0), text: none, tone: none },
|
|
1522
|
+
{ at: Plan.at.time(week(31n)), value: some(88.0), text: none, tone: none },
|
|
1523
|
+
] }],
|
|
1524
|
+
["20-m04", { ...base, pick: "machines", label: "L1-M04",
|
|
1525
|
+
jobs: [{ key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: ACTUAL }],
|
|
1526
|
+
points,
|
|
1527
|
+
nums: [
|
|
1528
|
+
{ at: Plan.at.time(week(28n)), value: some(112.0), text: none, tone: none },
|
|
1529
|
+
{ at: Plan.at.time(week(31n)), value: some(-24.0), text: none, tone: none },
|
|
1530
|
+
] }],
|
|
1531
|
+
// SAME KIND as the machines series, different entry — a kind is
|
|
1532
|
+
// not an identity, which is why the library keys on `key`.
|
|
1533
|
+
["25-c01", { ...base, pick: "contract", label: "CON-01",
|
|
1534
|
+
jobs: [{ key: "c1", label: "RUN · C-1", start: week(30n), end: week(34n), state: CONFIRMED }] }],
|
|
1535
|
+
["30-load", { ...base, pick: "load", label: "L2 load", cells }],
|
|
1536
|
+
["35-qual", { ...base, pick: "quality", label: "Quality", cells }],
|
|
1537
|
+
["40-dock2", { ...base, pick: "docks", label: "Dock 2",
|
|
1538
|
+
allocs: [
|
|
1539
|
+
{ key: "a1", at: week(28n), state: variant("confirmed", null) },
|
|
1540
|
+
{ key: "a2", at: week(31n), state: variant("proposed", variant("recommended", null)) },
|
|
1541
|
+
] }],
|
|
1542
|
+
["50-desp", { ...base, pick: "table", label: "Despatch t",
|
|
1543
|
+
nums: [
|
|
1544
|
+
{ at: Plan.at.time(week(28n)), value: some(128.0), text: none, tone: none },
|
|
1545
|
+
{ at: Plan.at.time(week(30n)), value: some(-96.0), text: none, tone: none },
|
|
1546
|
+
] }],
|
|
1547
|
+
["60-crewA", { ...base, pick: "cards", label: "Crew A",
|
|
1548
|
+
shifts: [
|
|
1549
|
+
{ key: "s1", from: week(27n), to: week(29n), label: "80h", state: variant("confirmed", null) },
|
|
1550
|
+
{ key: "s2", from: week(31n), to: week(33n), label: "+64h", state: variant("proposed", variant("recommended", null)) },
|
|
1551
|
+
] }],
|
|
1552
|
+
["70-ms", { ...base, pick: "events", label: "MILESTONES",
|
|
1553
|
+
marks: [
|
|
1554
|
+
{ key: "k", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
|
|
1555
|
+
{ key: "a", at: Plan.at.time(week(34n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
|
|
1556
|
+
] }],
|
|
1557
|
+
// Members of the three STATIC groups — they hang under their
|
|
1558
|
+
// strip, so their keys only order within that subtree.
|
|
1559
|
+
["g1-m11", { ...base, pick: "gspan", label: "L3-M11",
|
|
1560
|
+
jobs: [{ key: "b301", label: "RUN · B-301", start: week(29n), end: week(33n), state: CONFIRMED }] }],
|
|
1561
|
+
["g1-m12", { ...base, pick: "gspan", label: "L3-M12",
|
|
1562
|
+
jobs: [{ key: "b302", label: "RUN · B-302", start: week(31n), end: week(36n), state: ACTUAL }] }],
|
|
1563
|
+
["g2-l1", { ...base, pick: "gheat", label: "L4 load", cells }],
|
|
1564
|
+
["g3-d5", { ...base, pick: "gbuckets", label: "Dock 5",
|
|
1565
|
+
allocs: [{ key: "a3", at: week(33n), state: variant("confirmed", null) }] }],
|
|
1566
|
+
// The DISCOVERED group's members — one strip per `line`.
|
|
1567
|
+
["p1-a", { ...base, pick: "programs", label: "PR-A1", line: "Program A",
|
|
1568
|
+
jobs: [{ key: "p1", label: "RUN · P-1", start: week(28n), end: week(32n), state: CONFIRMED }] }],
|
|
1569
|
+
["p2-a", { ...base, pick: "programs", label: "PR-A2", line: "Program A",
|
|
1570
|
+
jobs: [{ key: "p2", label: "RUN · P-2", start: week(33n), end: week(37n), state: PROPOSED }] }],
|
|
1571
|
+
["p3-b", { ...base, pick: "programs", label: "PR-B1", line: "Program B",
|
|
1572
|
+
jobs: [{ key: "p3", label: "RUN · P-3", start: week(30n), end: week(35n), state: ACTUAL }] }],
|
|
1573
|
+
]), DictType(StringType, OpsRow));
|
|
1574
|
+
// The whole library: every kind once, two kinds TWICE, each of three
|
|
1575
|
+
// static groups wrapping a different kind, the discovered form, and
|
|
1576
|
+
// literal chrome. Fourteen entries, nine distinct arms.
|
|
1577
|
+
const all = $.const([
|
|
1578
|
+
// Literal one-off chrome — it names itself, so it can be
|
|
1579
|
+
// switched off like anything else.
|
|
1580
|
+
Plan.series.rows(OpsRow, { key: "chrome", title: "Section header", subtitle: "literal chrome" }, [Plan.events({ key: "00-hdr", label: "PLAN", id: true })]),
|
|
1581
|
+
Plan.series.chart(OpsRow, {
|
|
1582
|
+
key: "util", title: "Utilisation", subtitle: "% per fortnight",
|
|
1583
|
+
match: r => r.pick.equal("util"),
|
|
1584
|
+
label: r => r.label, id: true, height: "spark",
|
|
1585
|
+
layers: r => [Chart.Column(r.points, { x: p => p.week, y: p => p.pct })],
|
|
1586
|
+
}),
|
|
1587
|
+
Plan.series.span(OpsRow, {
|
|
1588
|
+
key: "machines", title: "Machine jobs", subtitle: "one row per machine",
|
|
1589
|
+
match: r => r.pick.equal("machines"),
|
|
1590
|
+
label: r => r.label, id: true,
|
|
1591
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
1592
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
1593
|
+
})),
|
|
1594
|
+
}),
|
|
1595
|
+
// ── THE SAME ASSET, SEEN TWO MORE WAYS ────────────────────
|
|
1596
|
+
// Both of these match the machines slice the span series
|
|
1597
|
+
// already claims, so all three emit a row per machine. Without
|
|
1598
|
+
// an affix they would land on one key and the last would
|
|
1599
|
+
// silently replace the others (#568's union is last-wins).
|
|
1600
|
+
//
|
|
1601
|
+
// `keySuffix` separates them while keeping the DATA key first,
|
|
1602
|
+
// which is the whole point: the canvas is in key order, so an
|
|
1603
|
+
// asset's three views sit ADJACENT (`20-m03`, `20-m03/chart`,
|
|
1604
|
+
// `20-m03/table`) instead of the series banking together — and
|
|
1605
|
+
// seek still lands, because it positions on the first row
|
|
1606
|
+
// at-or-after the sought key. A `keyPrefix` would do neither.
|
|
1607
|
+
Plan.series.chart(OpsRow, {
|
|
1608
|
+
key: "machine-util", title: "Machine · utilisation", subtitle: "same asset, 2nd view",
|
|
1609
|
+
match: r => r.pick.equal("machines"),
|
|
1610
|
+
keySuffix: "/chart",
|
|
1611
|
+
// The label stays the ASSET — all three rows are the same
|
|
1612
|
+
// machine, and pretending otherwise would hide that. What
|
|
1613
|
+
// distinguishes them is the VIEW, which is what the gutter
|
|
1614
|
+
// sub-line is for.
|
|
1615
|
+
label: r => r.label, stacked: true, sub: _r => some("utilisation %"), height: "spark",
|
|
1616
|
+
layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
|
|
1617
|
+
}),
|
|
1618
|
+
Plan.series.table(OpsRow, {
|
|
1619
|
+
key: "machine-tonnes", title: "Machine · tonnes", subtitle: "same asset, 3rd view",
|
|
1620
|
+
match: r => r.pick.equal("machines"),
|
|
1621
|
+
keySuffix: "/table",
|
|
1622
|
+
label: r => r.label, stacked: true, sub: _r => some("tonnes · plan Δ"),
|
|
1623
|
+
cells: r => r.nums,
|
|
1624
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1625
|
+
}),
|
|
1626
|
+
Plan.series.span(OpsRow, {
|
|
1627
|
+
key: "contract", title: "Contractor jobs", subtitle: "same KIND, own entry",
|
|
1628
|
+
match: r => r.pick.equal("contract"),
|
|
1629
|
+
label: r => r.label, id: true,
|
|
1630
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
1631
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
1632
|
+
})),
|
|
1633
|
+
}),
|
|
1634
|
+
Plan.series.heat(OpsRow, {
|
|
1635
|
+
key: "load", title: "Line load", subtitle: "% per fortnight",
|
|
1636
|
+
match: r => r.pick.equal("load"),
|
|
1637
|
+
label: r => r.label,
|
|
1638
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
|
|
1639
|
+
}),
|
|
1640
|
+
Plan.series.heat(OpsRow, {
|
|
1641
|
+
key: "quality", title: "Quality index", subtitle: "same KIND, own entry",
|
|
1642
|
+
match: r => r.pick.equal("quality"),
|
|
1643
|
+
label: r => r.label,
|
|
1644
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
|
|
1645
|
+
}),
|
|
1646
|
+
Plan.series.buckets(OpsRow, {
|
|
1647
|
+
key: "docks", title: "Dock allocations", subtitle: "tiles per bucket",
|
|
1648
|
+
match: r => r.pick.equal("docks"),
|
|
1649
|
+
label: r => r.label,
|
|
1650
|
+
events: r => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
|
|
1651
|
+
}),
|
|
1652
|
+
Plan.series.table(OpsRow, {
|
|
1653
|
+
key: "table", title: "Despatch tonnes", subtitle: "per bucket",
|
|
1654
|
+
match: r => r.pick.equal("table"),
|
|
1655
|
+
label: r => r.label,
|
|
1656
|
+
cells: r => r.nums,
|
|
1657
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1658
|
+
}),
|
|
1659
|
+
Plan.series.cards(OpsRow, {
|
|
1660
|
+
key: "cards", title: "Crew shifts", subtitle: "assignments",
|
|
1661
|
+
match: r => r.pick.equal("cards"),
|
|
1662
|
+
label: r => r.label,
|
|
1663
|
+
chips: r => r.shifts.map((_$, s) => Plan.chip({ key: s.key, from: s.from, to: s.to, label: s.label, state: s.state })),
|
|
1664
|
+
}),
|
|
1665
|
+
Plan.series.events(OpsRow, {
|
|
1666
|
+
key: "events", title: "Milestones", subtitle: "instant marks",
|
|
1667
|
+
match: r => r.pick.equal("events"),
|
|
1668
|
+
label: r => r.label, id: true,
|
|
1669
|
+
marks: r => r.marks,
|
|
1670
|
+
}),
|
|
1671
|
+
// STATIC groups — each wrapping a DIFFERENT kind. The strip's
|
|
1672
|
+
// own key / label / meta ARE the library entry's identity, so
|
|
1673
|
+
// "Line 3" is the thing a person picks and switching it off
|
|
1674
|
+
// takes its whole subtree with it.
|
|
1675
|
+
//
|
|
1676
|
+
// The metas are terse because they are read TWICE: as the
|
|
1677
|
+
// strip's meta line in the gutter (which is 168px and truncates
|
|
1678
|
+
// the label if the meta crowds it) and as the entry's subtitle
|
|
1679
|
+
// in the panel. Borrowing identity means one string serves both
|
|
1680
|
+
// — the cost of the group needing nothing extra written.
|
|
1681
|
+
Plan.series.group(OpsRow, { key: "g1-line3", label: "Line 3", meta: "span" }, [
|
|
1682
|
+
Plan.series.span(OpsRow, {
|
|
1683
|
+
key: "gspan", title: "Grouped span", subtitle: "member",
|
|
1684
|
+
match: r => r.pick.equal("gspan"),
|
|
1685
|
+
label: r => r.label, id: true,
|
|
1686
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
1687
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
1688
|
+
})),
|
|
1689
|
+
}),
|
|
1690
|
+
]),
|
|
1691
|
+
Plan.series.group(OpsRow, { key: "g2-lines", label: "Load", meta: "heat" }, [
|
|
1692
|
+
Plan.series.heat(OpsRow, {
|
|
1693
|
+
key: "gheat", title: "Grouped heat", subtitle: "member",
|
|
1694
|
+
match: r => r.pick.equal("gheat"),
|
|
1695
|
+
label: r => r.label,
|
|
1696
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
|
|
1697
|
+
}),
|
|
1698
|
+
]),
|
|
1699
|
+
Plan.series.group(OpsRow, { key: "g3-docks", label: "Docks", meta: "buckets" }, [
|
|
1700
|
+
Plan.series.buckets(OpsRow, {
|
|
1701
|
+
key: "gbuckets", title: "Grouped buckets", subtitle: "member",
|
|
1702
|
+
match: r => r.pick.equal("gbuckets"),
|
|
1703
|
+
label: r => r.label,
|
|
1704
|
+
events: r => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state })),
|
|
1705
|
+
}),
|
|
1706
|
+
]),
|
|
1707
|
+
// The DISCOVERED form — one strip per distinct `line`, and ONE
|
|
1708
|
+
// library entry for all of them, which is why it declares its
|
|
1709
|
+
// own identity instead of borrowing a strip's.
|
|
1710
|
+
Plan.series.group(OpsRow, {
|
|
1711
|
+
key: "programs", title: "Programs", subtitle: "one strip per line",
|
|
1712
|
+
by: r => r.line, match: r => r.pick.equal("programs"), keyPrefix: "p-",
|
|
1713
|
+
}, [
|
|
1714
|
+
Plan.series.span(OpsRow, {
|
|
1715
|
+
key: "programs", title: "Program runs", subtitle: "member",
|
|
1716
|
+
match: r => r.pick.equal("programs"),
|
|
1717
|
+
label: r => r.label, id: true,
|
|
1718
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
1719
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
1720
|
+
})),
|
|
1721
|
+
}),
|
|
1722
|
+
]),
|
|
1723
|
+
], ArrayType(Plan.Types.Series(OpsRow)));
|
|
1724
|
+
// `data` gives each entry its row count, so a series that selects
|
|
1725
|
+
// nothing says `0` rather than switching on to no visible effect.
|
|
1726
|
+
const shown = $.let(Plan.pick("ex.plan.library", all, {
|
|
1727
|
+
data: ops,
|
|
1728
|
+
hidden: ["quality", "cards", "g3-docks"],
|
|
1729
|
+
}));
|
|
1730
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
1731
|
+
const onDrag = $.const(East.function([DragEventType], NullType, (_$, _e) => null));
|
|
1732
|
+
const canDrop = $.const(East.function([DragEventType], BooleanType, (_$, _e) => true));
|
|
1733
|
+
// `pick` REPLACES `series`: the handle already carries the list, so
|
|
1734
|
+
// the canvas feeds itself the picked ones and mounts the library.
|
|
1735
|
+
// Nothing here wires the panel to the canvas.
|
|
1736
|
+
return (_jsx(Plan, { axis: axis, data: ops, pick: shown, id: "plan", sources: ["row-library"], onDrag: onDrag, canDrop: canDrop, style: { height: "620px" } }));
|
|
1737
|
+
} }))),
|
|
1738
|
+
inputs: [],
|
|
1739
|
+
});
|
|
1740
|
+
// ============================================================================
|
|
1741
|
+
// planRowDrop — the canvas as a drag TARGET, and which rows can receive
|
|
1742
|
+
// ============================================================================
|
|
1743
|
+
/**
|
|
1744
|
+
* A Plan is a drag target, and a heterogeneous one — which is what makes it
|
|
1745
|
+
* different from every other target in the grammar.
|
|
1746
|
+
*
|
|
1747
|
+
* Roster, Board and Blend have ONE kind of cell, so "can you drop here" is a
|
|
1748
|
+
* question about the cell's contents. A Plan's rows are nine different things,
|
|
1749
|
+
* so the question is answered TWICE, at two different levels:
|
|
1750
|
+
*
|
|
1751
|
+
* 1. **Structurally, by kind.** Only rows holding discrete scheduled objects
|
|
1752
|
+
* register a drop cell at all — `span` (runs), `buckets` (tiles), `events`
|
|
1753
|
+
* (marks), `cards` (chips). A `chart` / `heat` / `table` row renders
|
|
1754
|
+
* DERIVED values, so there is nothing for a card to become; it registers
|
|
1755
|
+
* no cell, never lights up during a drag, and is not reachable by any
|
|
1756
|
+
* predicate. Group strips are wayfinding, so they are inert too — their
|
|
1757
|
+
* MEMBERS receive.
|
|
1758
|
+
* 2. **By policy, with `canDrop`.** Of the rows that can receive, this canvas
|
|
1759
|
+
* admits only the matching FAMILY: a job goes on a machine, a delivery on
|
|
1760
|
+
* a dock, a shift on a crew, a milestone on a stream. The `PALLET` card
|
|
1761
|
+
* belongs to no family and is therefore refused everywhere — the ⊘ stage
|
|
1762
|
+
* on every row, which is what a card with nowhere to go should look like.
|
|
1763
|
+
*
|
|
1764
|
+
* The drop is not cosmetic. `onDrag` writes to bound state, the series
|
|
1765
|
+
* accessors read that state back, and the row re-derives — the documented flow
|
|
1766
|
+
* for every target ("no drop writes state directly"). Nothing is painted
|
|
1767
|
+
* optimistically, so what appears on the canvas is only ever what the data
|
|
1768
|
+
* says.
|
|
1769
|
+
*/
|
|
1770
|
+
export const planRowDrop = example({
|
|
1771
|
+
keywords: [
|
|
1772
|
+
"Plan", "Library", "DnD", "drag", "drop", "onDrag", "canDrop", "sources", "id",
|
|
1773
|
+
"add", "target", "surface", "cell", "slot", "row kind", "selective", "veto",
|
|
1774
|
+
"invalid", "span", "buckets", "events", "cards", "chart", "heat", "table", "group",
|
|
1775
|
+
"droppable", "inert", "bucket instant", "Reactive", "State", "commit", "re-derive",
|
|
1776
|
+
],
|
|
1777
|
+
description: "Library + Plan DnD — only the kinds holding discrete objects receive, and `canDrop` admits only the matching family",
|
|
1778
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
1779
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
1780
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
1781
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
1782
|
+
return w1.addWeeks(n.subtract(1n));
|
|
1783
|
+
}));
|
|
1784
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
1785
|
+
const JobRow = StructType({
|
|
1786
|
+
key: StringType, label: StringType,
|
|
1787
|
+
start: DateTimeType, end: DateTimeType, state: EventStateType,
|
|
1788
|
+
});
|
|
1789
|
+
const ShiftRow = StructType({
|
|
1790
|
+
key: StringType, from: DateTimeType, to: DateTimeType, label: StringType, state: EventStateType,
|
|
1791
|
+
});
|
|
1792
|
+
const AllocRow = StructType({ key: StringType, at: DateTimeType, state: EventStateType });
|
|
1793
|
+
// What a completed drop leaves behind: an instant and a name. The
|
|
1794
|
+
// KIND it becomes is the receiving series' business, which is why
|
|
1795
|
+
// one shape serves all four.
|
|
1796
|
+
const DropRow = StructType({ key: StringType, at: DateTimeType, label: StringType });
|
|
1797
|
+
const OpsRow = StructType({
|
|
1798
|
+
series: StringType, label: StringType, line: StringType,
|
|
1799
|
+
jobs: ArrayType(JobRow),
|
|
1800
|
+
points: ArrayType(MeasureRow),
|
|
1801
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
1802
|
+
allocs: ArrayType(AllocRow),
|
|
1803
|
+
nums: ArrayType(Plan.Types.TableCell),
|
|
1804
|
+
shifts: ArrayType(ShiftRow),
|
|
1805
|
+
marks: ArrayType(Plan.Types.EventMark),
|
|
1806
|
+
});
|
|
1807
|
+
const noJobs = $.const([], ArrayType(JobRow));
|
|
1808
|
+
const noPoints = $.const([], ArrayType(MeasureRow));
|
|
1809
|
+
const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
|
|
1810
|
+
const noAllocs = $.const([], ArrayType(AllocRow));
|
|
1811
|
+
const noNums = $.const([], ArrayType(Plan.Types.TableCell));
|
|
1812
|
+
const noShifts = $.const([], ArrayType(ShiftRow));
|
|
1813
|
+
const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
|
|
1814
|
+
const noDrops = $.const([], ArrayType(DropRow));
|
|
1815
|
+
const pcts = $.const([46.0, 58.0, 66.0, 72.0, 84.0, 96.0], ArrayType(FloatType));
|
|
1816
|
+
const points = $.let(East.Array.generate(6n, MeasureRow, (_$, i) => ({ week: week(i.multiply(2n).add(27n)), pct: pcts.get(i) })));
|
|
1817
|
+
const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, (_$, i) => ({
|
|
1818
|
+
at: Plan.at.time(week(i.multiply(2n).add(27n))),
|
|
1819
|
+
value: some(pcts.get(i)),
|
|
1820
|
+
label: some(East.Float.printFixed(pcts.get(i), 0n)),
|
|
1821
|
+
})));
|
|
1822
|
+
const CONFIRMED = variant("confirmed", null);
|
|
1823
|
+
const RUNNING = variant("in-progress", null);
|
|
1824
|
+
// Everything a drop creates is a PROPOSAL — it is a suggestion the
|
|
1825
|
+
// host has not committed, and the lifecycle is how the canvas says so.
|
|
1826
|
+
const ADDED = variant("proposed", variant("added", null));
|
|
1827
|
+
const base = {
|
|
1828
|
+
line: "", jobs: noJobs, points: noPoints, cells: noCells,
|
|
1829
|
+
allocs: noAllocs, nums: noNums, shifts: noShifts, marks: noMarks,
|
|
1830
|
+
};
|
|
1831
|
+
// Keys order the canvas (#568), numbered so the droppable and inert
|
|
1832
|
+
// kinds INTERLEAVE — a solid block of receiving rows would not show
|
|
1833
|
+
// that the line is drawn per kind.
|
|
1834
|
+
const ops = $.const(new Map([
|
|
1835
|
+
["10-util", { ...base, series: "util", label: "UTIL %", points }],
|
|
1836
|
+
["20-m03", { ...base, series: "mach", label: "L1-M03",
|
|
1837
|
+
jobs: [{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(31n), state: RUNNING }] }],
|
|
1838
|
+
["20-m04", { ...base, series: "mach", label: "L1-M04",
|
|
1839
|
+
jobs: [{ key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: CONFIRMED }] }],
|
|
1840
|
+
["30-load", { ...base, series: "load", label: "L2 load", cells }],
|
|
1841
|
+
["40-dock2", { ...base, series: "dock", label: "Dock 2",
|
|
1842
|
+
allocs: [{ key: "a1", at: week(29n), state: CONFIRMED }] }],
|
|
1843
|
+
["50-desp", { ...base, series: "table", label: "Despatch t",
|
|
1844
|
+
nums: [
|
|
1845
|
+
{ at: Plan.at.time(week(28n)), value: some(128.0), text: none, tone: none },
|
|
1846
|
+
{ at: Plan.at.time(week(31n)), value: some(-96.0), text: none, tone: none },
|
|
1847
|
+
] }],
|
|
1848
|
+
["60-crewA", { ...base, series: "crew", label: "Crew A",
|
|
1849
|
+
shifts: [{ key: "s1", from: week(27n), to: week(29n), label: "80h", state: CONFIRMED }] }],
|
|
1850
|
+
["70-ms", { ...base, series: "strm", label: "MILESTONES",
|
|
1851
|
+
marks: [{ key: "k", at: Plan.at.time(week(29n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") }] }],
|
|
1852
|
+
// A group MEMBER — the strip itself takes no drops, but the
|
|
1853
|
+
// span row inside it receives like any other span row.
|
|
1854
|
+
["g1-m11", { ...base, series: "gmach", label: "L3-M11", line: "Line 3",
|
|
1855
|
+
jobs: [{ key: "b301", label: "RUN · B-301", start: week(30n), end: week(34n), state: CONFIRMED }] }],
|
|
1856
|
+
]), DictType(StringType, OpsRow));
|
|
1857
|
+
// ── The two policy tables the host owns ───────────────────────
|
|
1858
|
+
// Which FAMILY of card each row will take. A row absent from this
|
|
1859
|
+
// map takes nothing — which is how the inert kinds would behave
|
|
1860
|
+
// even if they did register a cell.
|
|
1861
|
+
const rowAccepts = $.const(new Map([
|
|
1862
|
+
["20-m03", "job"], ["20-m04", "job"], ["g1-m11", "job"],
|
|
1863
|
+
["40-dock2", "delivery"],
|
|
1864
|
+
["60-crewA", "shift"],
|
|
1865
|
+
["70-ms", "milestone"],
|
|
1866
|
+
]), DictType(StringType, StringType));
|
|
1867
|
+
const CardRow = StructType({
|
|
1868
|
+
key: StringType, name: StringType, family: StringType, note: StringType, icon: StringType,
|
|
1869
|
+
});
|
|
1870
|
+
const cards = $.const([
|
|
1871
|
+
{ key: "job-weld", name: "Weld cell", family: "job", note: "job · machines", icon: "gear" },
|
|
1872
|
+
{ key: "job-cure", name: "Cure oven", family: "job", note: "job · machines", icon: "fire" },
|
|
1873
|
+
{ key: "dlv-truck", name: "Truck 12", family: "delivery", note: "delivery · docks", icon: "truck" },
|
|
1874
|
+
{ key: "shf-night", name: "Night shift", family: "shift", note: "shift · crews", icon: "moon" },
|
|
1875
|
+
{ key: "mst-audit", name: "Audit gate", family: "milestone", note: "milestone · streams", icon: "flag" },
|
|
1876
|
+
// Belongs to no family, so no row accepts it — the ⊘ stage
|
|
1877
|
+
// everywhere, which is what a card with nowhere to go looks like.
|
|
1878
|
+
{ key: "pallet", name: "PALLET", family: "none", note: "fits nowhere", icon: "box" },
|
|
1879
|
+
], ArrayType(CardRow));
|
|
1880
|
+
const cardFamily = $.const(cards.toDict((_$, c) => c.key, (_$, c) => c.family));
|
|
1881
|
+
const cardName = $.const(cards.toDict((_$, c) => c.key, (_$, c) => c.name));
|
|
1882
|
+
// ── The drop veto ────────────────────────────────────────────
|
|
1883
|
+
// Consulted with the candidate event the pointer's CURRENT bucket
|
|
1884
|
+
// would produce, so the ⊘ appears while dragging rather than after.
|
|
1885
|
+
// Only `add` can reach a Plan (nothing on the canvas starts a drag,
|
|
1886
|
+
// so the surface declares no move / resize), and refusing the rest
|
|
1887
|
+
// says that rather than pretending they are permitted.
|
|
1888
|
+
const canDrop = $.const(East.function([DragEventType], BooleanType, ($, event) => {
|
|
1889
|
+
const no = $.const(false, BooleanType);
|
|
1890
|
+
return event.match({
|
|
1891
|
+
add: ($, add) => {
|
|
1892
|
+
const row = $.let(add.into.row);
|
|
1893
|
+
const card = $.let(add.from.key);
|
|
1894
|
+
return rowAccepts.has(row)
|
|
1895
|
+
.and(_$ => cardFamily.has(card))
|
|
1896
|
+
.and(_$ => rowAccepts.get(row).equal(cardFamily.get(card)));
|
|
1897
|
+
},
|
|
1898
|
+
}, _$ => no);
|
|
1899
|
+
}));
|
|
1900
|
+
// ── The commit ───────────────────────────────────────────────
|
|
1901
|
+
// The drop reports; the HOST writes. Nothing is painted
|
|
1902
|
+
// optimistically — this state is what the series read back, so a
|
|
1903
|
+
// dropped card only appears because the data now says it is there.
|
|
1904
|
+
const dropBind = $.let(State.bind([DictType(StringType, ArrayType(DropRow))], "ex.plan.drops", new Map()));
|
|
1905
|
+
const lastBind = $.let(State.bind([StringType], "ex.plan.lastdrop", "none yet"));
|
|
1906
|
+
const onDrag = $.const(East.function([DragEventType], NullType, ($, event) => {
|
|
1907
|
+
$.match(event, {
|
|
1908
|
+
add: ($, add) => {
|
|
1909
|
+
const next = $.let(dropBind.read());
|
|
1910
|
+
const row = $.let(add.into.row);
|
|
1911
|
+
const card = $.let(add.from.key);
|
|
1912
|
+
// The slot IS an East DateTime in text — that is the
|
|
1913
|
+
// contract the Z-less ISO spelling exists to keep.
|
|
1914
|
+
const at = $.let(add.into.slot.parse(DateTimeType));
|
|
1915
|
+
const one = $.let([{
|
|
1916
|
+
key: East.str `drop-${card}-${add.into.slot}`,
|
|
1917
|
+
at, label: cardName.get(card),
|
|
1918
|
+
}], ArrayType(DropRow));
|
|
1919
|
+
$(next.insertOrUpdate(row, one, (_$, existing, incoming) => existing.concat(incoming)));
|
|
1920
|
+
$(dropBind.write(next));
|
|
1921
|
+
$(lastBind.write(East.str `add ${card} → ${row} · ${add.into.slot}`));
|
|
1922
|
+
},
|
|
1923
|
+
move: (_$) => { }, remove: (_$) => { }, resize: (_$) => { },
|
|
1924
|
+
});
|
|
1925
|
+
}));
|
|
1926
|
+
// Read DIRECTLY in the accessors below — never through a helper
|
|
1927
|
+
// East.function. A series accessor is reified into a standalone
|
|
1928
|
+
// function, so whatever it captures crosses that boundary: a
|
|
1929
|
+
// captured DICT travels as the value it is, but a captured
|
|
1930
|
+
// FUNCTION drags the enclosing block along with it, once per
|
|
1931
|
+
// accessor, and four accessors was enough to take this example's
|
|
1932
|
+
// render payload past 100MB. `planReview` is the shape to copy.
|
|
1933
|
+
const drops = $.let(dropBind.read());
|
|
1934
|
+
const last = $.let(lastBind.read());
|
|
1935
|
+
const axis = $.const(Plan.axis({
|
|
1936
|
+
window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n),
|
|
1937
|
+
}));
|
|
1938
|
+
return (_jsxs(VStack, { gap: "4", align: "stretch", children: [_jsx(Library, { id: "plan-library", data: cards, item: c => ({ key: c.key, label: c.name, sublabel: c.note, icon: c.icon }) }), _jsx(Plan, { axis: axis, data: ops,
|
|
1939
|
+
// The DnD target role: `id` names this surface in every
|
|
1940
|
+
// cell ref, `sources` says which palettes it will take
|
|
1941
|
+
// from, and the two callbacks are the policy and the
|
|
1942
|
+
// commit. Omitting either `id` or `onDrag` registers no
|
|
1943
|
+
// target at all — a drop with nowhere to report is a
|
|
1944
|
+
// gesture that silently loses work.
|
|
1945
|
+
id: "ops-plan", sources: ["plan-library"], canDrop: canDrop, onDrag: onDrag, series: [
|
|
1946
|
+
// INERT — a chart plots a derived series, so there
|
|
1947
|
+
// is nothing a card could become here.
|
|
1948
|
+
Plan.series.chart(OpsRow, {
|
|
1949
|
+
key: "util", title: "Utilisation",
|
|
1950
|
+
match: r => r.series.equal("util"),
|
|
1951
|
+
label: r => r.label, id: true, height: "spark",
|
|
1952
|
+
layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
|
|
1953
|
+
}),
|
|
1954
|
+
// RECEIVES — runs are discrete scheduled objects.
|
|
1955
|
+
// The row's own runs, plus one proposed run per
|
|
1956
|
+
// card dropped on it — a fortnight long, starting
|
|
1957
|
+
// at the bucket the pointer named.
|
|
1958
|
+
Plan.series.span(OpsRow, {
|
|
1959
|
+
key: "mach", title: "Machine jobs",
|
|
1960
|
+
match: r => r.series.equal("mach"),
|
|
1961
|
+
label: r => r.label, id: true,
|
|
1962
|
+
runs: (r, k) => r.jobs.map((_$, j) => Plan.run({
|
|
1963
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
1964
|
+
})).concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
|
|
1965
|
+
.map((_$, d) => Plan.run({
|
|
1966
|
+
key: d.key, start: d.at, end: d.at.addWeeks(2n),
|
|
1967
|
+
label: d.label, state: ADDED,
|
|
1968
|
+
}))),
|
|
1969
|
+
}),
|
|
1970
|
+
// INERT — an intensity field has no object to add to.
|
|
1971
|
+
Plan.series.heat(OpsRow, {
|
|
1972
|
+
key: "load", title: "Line load",
|
|
1973
|
+
match: r => r.series.equal("load"),
|
|
1974
|
+
label: r => r.label,
|
|
1975
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
|
|
1976
|
+
}),
|
|
1977
|
+
// RECEIVES — a dropped delivery becomes a tile in
|
|
1978
|
+
// the bucket under the pointer.
|
|
1979
|
+
Plan.series.buckets(OpsRow, {
|
|
1980
|
+
key: "dock", title: "Dock allocations",
|
|
1981
|
+
match: r => r.series.equal("dock"),
|
|
1982
|
+
label: r => r.label,
|
|
1983
|
+
events: (r, k) => r.allocs.map((_$, a) => Plan.event({ key: a.key, at: a.at, state: a.state }))
|
|
1984
|
+
.concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
|
|
1985
|
+
.map((_$, d) => Plan.event({ key: d.key, at: d.at, state: ADDED }))),
|
|
1986
|
+
}),
|
|
1987
|
+
// INERT — the cells are computed numbers.
|
|
1988
|
+
Plan.series.table(OpsRow, {
|
|
1989
|
+
key: "table", title: "Despatch tonnes",
|
|
1990
|
+
match: r => r.series.equal("table"),
|
|
1991
|
+
label: r => r.label,
|
|
1992
|
+
cells: r => r.nums,
|
|
1993
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
1994
|
+
}),
|
|
1995
|
+
// RECEIVES — a dropped shift becomes a chip.
|
|
1996
|
+
Plan.series.cards(OpsRow, {
|
|
1997
|
+
key: "crew", title: "Crew shifts",
|
|
1998
|
+
match: r => r.series.equal("crew"),
|
|
1999
|
+
label: r => r.label,
|
|
2000
|
+
chips: (r, k) => r.shifts.map((_$, s) => Plan.chip({
|
|
2001
|
+
key: s.key, from: s.from, to: s.to, label: s.label, state: s.state,
|
|
2002
|
+
})).concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
|
|
2003
|
+
.map((_$, d) => Plan.chip({
|
|
2004
|
+
key: d.key, from: d.at, to: d.at.addWeeks(2n),
|
|
2005
|
+
label: d.label, state: ADDED,
|
|
2006
|
+
}))),
|
|
2007
|
+
}),
|
|
2008
|
+
// RECEIVES — a dropped milestone becomes a mark at
|
|
2009
|
+
// the instant, the one kind with no duration.
|
|
2010
|
+
Plan.series.events(OpsRow, {
|
|
2011
|
+
key: "strm", title: "Milestones",
|
|
2012
|
+
match: r => r.series.equal("strm"),
|
|
2013
|
+
label: r => r.label, id: true,
|
|
2014
|
+
marks: (r, k) => r.marks.concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
|
|
2015
|
+
.map((_$, d) => Plan.mark({
|
|
2016
|
+
key: d.key, at: d.at, kind: "milestone", label: d.label,
|
|
2017
|
+
}))),
|
|
2018
|
+
}),
|
|
2019
|
+
// The STRIP is inert; the span row inside it is not.
|
|
2020
|
+
Plan.series.group(OpsRow, { key: "g1-line3", label: "Line 3", meta: "span" }, [
|
|
2021
|
+
Plan.series.span(OpsRow, {
|
|
2022
|
+
key: "gmach", title: "Grouped machine jobs",
|
|
2023
|
+
match: r => r.series.equal("gmach"),
|
|
2024
|
+
label: r => r.label, id: true,
|
|
2025
|
+
runs: (r, k) => r.jobs.map((_$, j) => Plan.run({
|
|
2026
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
2027
|
+
})).concat(drops.has(k).ifElse(() => drops.get(k), () => noDrops)
|
|
2028
|
+
.map((_$, d) => Plan.run({
|
|
2029
|
+
key: d.key, start: d.at, end: d.at.addWeeks(2n),
|
|
2030
|
+
label: d.label, state: ADDED,
|
|
2031
|
+
}))),
|
|
2032
|
+
}),
|
|
2033
|
+
]),
|
|
2034
|
+
], style: { height: "420px" } }), _jsx(Text.MonoLabel, { children: East.str `LAST DROP · ${last}` })] }));
|
|
2035
|
+
} }))),
|
|
2036
|
+
inputs: [],
|
|
2037
|
+
});
|
|
2038
|
+
// ============================================================================
|
|
2039
|
+
// planFill — the bounded sizing isolate (#320 / #567 D1)
|
|
2040
|
+
// ============================================================================
|
|
2041
|
+
/** Fill (#320) — `height="fill"` resolves against the bounded Box and
|
|
2042
|
+
* virtualizes 200 span rows under 8 rollup parents. The bound must land on the
|
|
2043
|
+
* canvas WRAPPER: a percentage passed inward resolves against an auto-height
|
|
2044
|
+
* parent, computes to `auto`, and silently unbinds — the frame reports bounded,
|
|
2045
|
+
* renders its spacer, and never scrolls (#567 D1).
|
|
2046
|
+
*
|
|
2047
|
+
* The grouping is here for what it does to VIRTUALIZATION, not for the
|
|
2048
|
+
* chrome. Collapsing a strip removes its 25 children from the virtualizer's
|
|
2049
|
+
* item list, so `count` and the total size change while the scroll offset does
|
|
2050
|
+
* not — the case where an estimate that disagrees with the rendered height
|
|
2051
|
+
* shows up as drift or a jumping scrollbar. The parents also give the list two
|
|
2052
|
+
* different row heights (a rollup band is taller than a leaf), so the
|
|
2053
|
+
* `estimateSize` path is exercised rather than a single constant. */
|
|
2054
|
+
export const planFill = example({
|
|
2055
|
+
keywords: ["Plan", "fill", "height", "maxHeight", "#320", "virtual", "virtualization", "bounded", "Box", "scroll", "sizing", "data", "series", "span", "chart", "heat", "spark", "sub", "two-line", "mixed", "variable", "estimateSize", "group", "groupBy", "rollup", "union", "collapse", "parent"],
|
|
2056
|
+
description: "Fill sizing over variable row heights — 200 virtualized rows of mixed kinds under 8 rollup parents",
|
|
2057
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
2058
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open).
|
|
2059
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
2060
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
2061
|
+
return w1.addWeeks(n.subtract(1n));
|
|
2062
|
+
}));
|
|
2063
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
2064
|
+
const UnitRow = StructType({
|
|
2065
|
+
line: StringType, series: StringType,
|
|
2066
|
+
sub: OptionType(StringType),
|
|
2067
|
+
start: DateTimeType, end: DateTimeType, tonnes: FloatType,
|
|
2068
|
+
points: ArrayType(MeasureRow),
|
|
2069
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
2070
|
+
});
|
|
2071
|
+
// 200 raw rows, generated in East — the row count is the point. The
|
|
2072
|
+
// KEYS are the point too: they order the canvas, so they are numbered
|
|
2073
|
+
// to sort as written (`UNIT-1000` … `UNIT-1199`) rather than
|
|
2074
|
+
// lexicographically (`UNIT-1`, `UNIT-10`, `UNIT-100`, …).
|
|
2075
|
+
//
|
|
2076
|
+
// `line` is the grouping level: 8 strips of 25, so a single collapse
|
|
2077
|
+
// takes an eighth of the list out of the virtualizer at once.
|
|
2078
|
+
//
|
|
2079
|
+
// Everything else here exists to make the row heights DISAGREE, which
|
|
2080
|
+
// is the case a virtualizer gets wrong. `series` cycles span / chart /
|
|
2081
|
+
// heat (32 / 32 / 28px), `sub` alternates so every other row floors at
|
|
2082
|
+
// the 42px two-line gutter, and the rollup parents add a fourth height
|
|
2083
|
+
// again — so consecutive estimates differ in both directions and no
|
|
2084
|
+
// constant can stand in for `estimateSize`.
|
|
2085
|
+
const units = $.const(East.Array.range(0n, 200n).toDict((_$, i) => East.str `UNIT-${East.print(i.add(1000n))}`, ($, i) => {
|
|
2086
|
+
const m = $.let(i.modulo(10n), IntegerType);
|
|
2087
|
+
return {
|
|
2088
|
+
line: East.str `LINE ${East.print(i.modulo(8n).add(1n))}`,
|
|
2089
|
+
// The kind cycles with the KEY, so every stretch of the canvas
|
|
2090
|
+
// holds the same mix — clustering the tall rows at one end
|
|
2091
|
+
// would leave most of the scroll a single height again.
|
|
2092
|
+
series: m.equal(4n).ifElse(() => "heat", () => m.equal(5n).ifElse(() => "spark", () => m.equal(6n).ifElse(() => "chartM", () => m.equal(7n).ifElse(() => "chartL", () => m.equal(8n).ifElse(() => "chartXL", () => "span"))))),
|
|
2093
|
+
// Alternating one-line / two-line gutters.
|
|
2094
|
+
sub: i.modulo(2n).equal(0n).ifElse(() => some(East.str `cap ${East.print(i.modulo(9n).add(40n))} t`), () => $.const(none, OptionType(StringType))),
|
|
2095
|
+
start: week(i.modulo(9n).add(27n)),
|
|
2096
|
+
end: week(i.modulo(9n).add(30n)),
|
|
2097
|
+
tonnes: i.toFloat().multiply(1.5).add(40.0),
|
|
2098
|
+
points: East.Array.generate(6n, MeasureRow, (_$, j) => ({
|
|
2099
|
+
week: week(j.multiply(2n).add(27n)),
|
|
2100
|
+
pct: j.multiply(17n).add(i).remainder(60n).toFloat().add(40.0),
|
|
2101
|
+
})),
|
|
2102
|
+
cells: East.Array.generate(6n, Plan.Types.HeatCell, (_$, j) => ({
|
|
2103
|
+
at: Plan.at.time(week(j.multiply(2n).add(27n))),
|
|
2104
|
+
value: some(j.multiply(13n).add(i).remainder(100n).toFloat()),
|
|
2105
|
+
label: none,
|
|
2106
|
+
})),
|
|
2107
|
+
};
|
|
2108
|
+
}), DictType(StringType, UnitRow));
|
|
2109
|
+
// ONE discovered group per line, wrapping EVERY kind — the strip form
|
|
2110
|
+
// takes child series, so each line's members are all its rows whatever
|
|
2111
|
+
// kind they are. (`groupBy` on the span series alone would nest only
|
|
2112
|
+
// the spans and leave every chart / heat row a root, which sorts them
|
|
2113
|
+
// as a block after all eight subtrees; the chart series has no
|
|
2114
|
+
// `groupBy` at all.) Inside a strip the members are in KEY order, so
|
|
2115
|
+
// the kinds interleave the way the keys do.
|
|
2116
|
+
const series = $.const([
|
|
2117
|
+
Plan.series.group(UnitRow, { key: "lines", title: "Lines", by: r => r.line }, [
|
|
2118
|
+
Plan.series.span(UnitRow, {
|
|
2119
|
+
key: "span-4", title: "Span",
|
|
2120
|
+
match: r => r.series.equal("span"),
|
|
2121
|
+
label: (_r, k) => k, id: true, sub: r => r.sub,
|
|
2122
|
+
runs: (r, k) => [Plan.run({
|
|
2123
|
+
key: k, start: r.start, end: r.end,
|
|
2124
|
+
label: East.str `RUN · ${k}`,
|
|
2125
|
+
quantity: East.str `${East.Float.printFixed(r.tonnes, 0n)} t`,
|
|
2126
|
+
qty: r.tonnes, state: variant("confirmed", null),
|
|
2127
|
+
})],
|
|
2128
|
+
}),
|
|
2129
|
+
// Heat rows are 28px — shorter than everything around them.
|
|
2130
|
+
Plan.series.heat(UnitRow, {
|
|
2131
|
+
key: "heat-2", title: "Heat",
|
|
2132
|
+
match: r => r.series.equal("heat"),
|
|
2133
|
+
label: (_r, k) => k, id: true, sub: r => r.sub,
|
|
2134
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100 }),
|
|
2135
|
+
}),
|
|
2136
|
+
// Four chart heights. `height` is a SERIES declaration, not a
|
|
2137
|
+
// per-row accessor, so distinct heights mean distinct series —
|
|
2138
|
+
// which is the point here: a spark, then three EXPANDED rows
|
|
2139
|
+
// several times taller, scattered through the same key order.
|
|
2140
|
+
Plan.series.chart(UnitRow, {
|
|
2141
|
+
key: "spark-2", title: "Spark",
|
|
2142
|
+
match: r => r.series.equal("spark"),
|
|
2143
|
+
label: (_r, k) => k, id: true, sub: r => r.sub,
|
|
2144
|
+
height: "spark", expandable: true,
|
|
2145
|
+
layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
|
|
2146
|
+
}),
|
|
2147
|
+
Plan.series.chart(UnitRow, {
|
|
2148
|
+
key: "chartM", title: "Chart · medium",
|
|
2149
|
+
match: r => r.series.equal("chartM"),
|
|
2150
|
+
label: (_r, k) => k, id: true, sub: r => r.sub,
|
|
2151
|
+
height: Plan.fixed("72px"),
|
|
2152
|
+
layers: r => [Chart.Line(r.points, { x: p => p.week, y: p => p.pct })],
|
|
2153
|
+
}),
|
|
2154
|
+
Plan.series.chart(UnitRow, {
|
|
2155
|
+
key: "chartL", title: "Chart · large",
|
|
2156
|
+
match: r => r.series.equal("chartL"),
|
|
2157
|
+
label: (_r, k) => k, id: true, sub: r => r.sub,
|
|
2158
|
+
height: "expanded",
|
|
2159
|
+
layers: r => [Chart.Area(r.points, { x: p => p.week, y: p => p.pct })],
|
|
2160
|
+
}),
|
|
2161
|
+
Plan.series.chart(UnitRow, {
|
|
2162
|
+
key: "chartXL", title: "Chart · x-large",
|
|
2163
|
+
match: r => r.series.equal("chartXL"),
|
|
2164
|
+
label: (_r, k) => k, id: true, sub: r => r.sub,
|
|
2165
|
+
height: "expanded", expandedHeight: "140px",
|
|
2166
|
+
layers: r => [Chart.Column(r.points, { x: p => p.week, y: p => p.pct })],
|
|
2167
|
+
}),
|
|
2168
|
+
]),
|
|
2169
|
+
], ArrayType(Plan.Types.Series(UnitRow)));
|
|
2170
|
+
const axis = $.const(Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }));
|
|
2171
|
+
return (_jsx(Box, { height: "240px", children: _jsx(Plan, { axis: axis, data: units, series: series, style: { height: "fill" } }) }));
|
|
2172
|
+
}),
|
|
2173
|
+
inputs: [],
|
|
2174
|
+
});
|
|
2175
|
+
// ============================================================================
|
|
2176
|
+
// planReview — the review chrome, and what a verdict is FOR (#569)
|
|
2177
|
+
// ============================================================================
|
|
2178
|
+
export const planReview = example({
|
|
2179
|
+
keywords: [
|
|
2180
|
+
"Plan", "review", "approval", "approve", "reject", "verdict", "decision",
|
|
2181
|
+
"deriveApproval", "flagged", "chrome", "batch", "foot", "onApprove",
|
|
2182
|
+
"onReject", "onApproveAll", "onRejectAll", "Reactive", "State", "bind",
|
|
2183
|
+
"write", "live", "derived", "accessor", "data",
|
|
2184
|
+
],
|
|
2185
|
+
description: "Live review chrome — verdicts live in bound `State`, and the canvas re-derives buttons, bar and dot",
|
|
2186
|
+
fn: East.function([], UIComponentType, (_$) => (_jsx(Reactive, { children: $ => {
|
|
2187
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
2188
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
2189
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
2190
|
+
return w1.addWeeks(n.subtract(1n));
|
|
2191
|
+
}));
|
|
2192
|
+
// The RAW record carries NO verdict — deciding is not a property of
|
|
2193
|
+
// the job, it is something the reviewer does to it. So the verdicts
|
|
2194
|
+
// live in their own bound state and the job rows stay untouched.
|
|
2195
|
+
const JobRow = StructType({
|
|
2196
|
+
start: DateTimeType, end: DateTimeType, tonnes: FloatType, flagged: BooleanType,
|
|
2197
|
+
});
|
|
2198
|
+
const jobs = $.const(new Map([
|
|
2199
|
+
["L1-M03", { start: week(28n), end: week(31n), tonnes: 96.0, flagged: true }],
|
|
2200
|
+
["L1-M04", { start: week(29n), end: week(33n), tonnes: 112.0, flagged: true }],
|
|
2201
|
+
["L1-M07", { start: week(30n), end: week(34n), tonnes: 64.0, flagged: true }],
|
|
2202
|
+
["L2-M11", { start: week(27n), end: week(30n), tonnes: 88.0, flagged: false }],
|
|
2203
|
+
]), DictType(StringType, JobRow));
|
|
2204
|
+
// Every key is seeded, so a read is always a hit and the accessors
|
|
2205
|
+
// never carry a "missing" branch.
|
|
2206
|
+
const verdicts = $.let(State.bind([DictType(StringType, StringType)], "plan_review_verdicts", new Map([
|
|
2207
|
+
["L1-M03", "pending"], ["L1-M04", "approved"],
|
|
2208
|
+
["L1-M07", "rejected"], ["L2-M11", "approved"],
|
|
2209
|
+
])));
|
|
2210
|
+
const live = $.let(verdicts.read());
|
|
2211
|
+
// The callbacks WRITE. `onApprove` fires with the row ref, so one
|
|
2212
|
+
// key changes; the batch verbs rewrite every key. Nothing else in
|
|
2213
|
+
// the canvas is told — it re-derives because `Reactive` re-runs.
|
|
2214
|
+
const setOne = $.const(East.function([StringType, StringType], NullType, ($, key, verdict) => {
|
|
2215
|
+
const next = $.let(verdicts.read());
|
|
2216
|
+
$(next.insertOrUpdate(key, verdict));
|
|
2217
|
+
$(verdicts.write(next));
|
|
2218
|
+
}));
|
|
2219
|
+
const setAll = $.const(East.function([StringType], NullType, ($, verdict) => {
|
|
2220
|
+
const next = $.let(new Map(), DictType(StringType, StringType));
|
|
2221
|
+
$(jobs.forEach((_$, _r, k) => next.insertOrUpdate(k, verdict)));
|
|
2222
|
+
$(verdicts.write(next));
|
|
2223
|
+
}));
|
|
2224
|
+
const onApprove = $.const(East.function([Plan.Types.RowRef], NullType, ($, ref) => { $(setOne(ref.key, "approved")); }));
|
|
2225
|
+
const onReject = $.const(East.function([Plan.Types.RowRef], NullType, ($, ref) => { $(setOne(ref.key, "rejected")); }));
|
|
2226
|
+
const onApproveAll = $.const(East.function([], NullType, ($) => { $(setAll("approved")); }));
|
|
2227
|
+
const onRejectAll = $.const(East.function([], NullType, ($) => { $(setAll("rejected")); }));
|
|
2228
|
+
const onRerun = $.const(East.function([], NullType, ($) => { $(setAll("pending")); }));
|
|
2229
|
+
const series = $.const([
|
|
2230
|
+
Plan.series.span(JobRow, {
|
|
2231
|
+
key: "span-5", title: "Span",
|
|
2232
|
+
label: (_r, k) => k, id: true,
|
|
2233
|
+
value: r => some(East.str `${East.Float.printFixed(r.tonnes, 0n)} t`),
|
|
2234
|
+
// Seeds the BUTTONS from the live verdict. `deriveApproval`
|
|
2235
|
+
// covers the undecided case — "clean rests pre-approved,
|
|
2236
|
+
// flagged awaits an explicit call".
|
|
2237
|
+
approval: (r, k) => live.get(k).equal("approved").ifElse(() => some(variant("approved", null)), () => live.get(k).equal("rejected").ifElse(() => some(variant("rejected", null)), () => deriveApproval(r.flagged))),
|
|
2238
|
+
// ...and the SAME fact drives the bar and the dot, because
|
|
2239
|
+
// appearance is derived like everything else. Click Approve
|
|
2240
|
+
// and this repaints — the chrome never touched it.
|
|
2241
|
+
status: (r, k) => live.get(k).equal("rejected").ifElse(() => some(variant("danger", null)), () => r.flagged.and(_$ => live.get(k).equal("pending")).ifElse(() => some(variant("warning", null)), () => none)),
|
|
2242
|
+
runs: (r, k) => [Plan.run({
|
|
2243
|
+
key: k, start: r.start, end: r.end,
|
|
2244
|
+
label: East.str `RUN · ${k}`,
|
|
2245
|
+
quantity: East.str `${East.Float.printFixed(r.tonnes, 0n)} t`,
|
|
2246
|
+
qty: r.tonnes,
|
|
2247
|
+
state: live.get(k).equal("approved").ifElse(() => variant("confirmed", null), () => live.get(k).equal("rejected").ifElse(() => variant("rejected", null), () => variant("proposed", variant("recommended", null)))),
|
|
2248
|
+
})],
|
|
2249
|
+
}),
|
|
2250
|
+
], ArrayType(Plan.Types.Series(JobRow)));
|
|
2251
|
+
const axis = $.const(Plan.axis({
|
|
2252
|
+
window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n),
|
|
2253
|
+
}));
|
|
2254
|
+
const counts = $.let(live.filter((_$, v) => v.equal("pending")).size());
|
|
2255
|
+
const rejected = $.let(live.filter((_$, v) => v.equal("rejected")).size());
|
|
2256
|
+
return (_jsx(Plan, { axis: axis, data: jobs, series: series, review: {
|
|
2257
|
+
summary: _jsx(Text, { children: East.str `${East.Float.printFixed(counts.toFloat(), 0n)} PENDING · ${East.Float.printFixed(rejected.toFloat(), 0n)} REJECTED` }),
|
|
2258
|
+
onApprove, onReject, onApproveAll, onRejectAll, onRerun,
|
|
2259
|
+
} }));
|
|
2260
|
+
} }))),
|
|
2261
|
+
inputs: [],
|
|
2262
|
+
});
|
|
2263
|
+
/**
|
|
2264
|
+
* R2 expand-in-place — the one example that shows the whole gesture.
|
|
2265
|
+
*
|
|
2266
|
+
* Expand is two halves and needs BOTH: a per-row `expand` declaration (data,
|
|
2267
|
+
* so it flows through an accessor like every other envelope field) and the
|
|
2268
|
+
* ROOT's `expandRender` resolver, which builds the mounted body from the row
|
|
2269
|
+
* ref. Neither alone shows the control.
|
|
2270
|
+
*
|
|
2271
|
+
* The rows here are deliberately heterogeneous — span, chart, heat, table,
|
|
2272
|
+
* events — because focusing one is what makes the other five collapse, and
|
|
2273
|
+
* each row KIND collapses differently (#591): geometry shrinks, values
|
|
2274
|
+
* re-encode as a tone strip, shapes keep their silhouette.
|
|
2275
|
+
*/
|
|
2276
|
+
export const planExpand = example({
|
|
2277
|
+
keywords: ["Plan", "expand", "expandRender", "axis", "keep", "dim", "off", "focus", "R2", "collapse", "context strip", "row", "data", "series", "chart", "heat", "table", "events", "span", "raw"],
|
|
2278
|
+
description: "Expand-in-place — a row declares `expand`, the root renders plot and gutter, and unfocused rows collapse to strips",
|
|
2279
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
2280
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
2281
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
2282
|
+
return w1.addWeeks(n.subtract(1n));
|
|
2283
|
+
}));
|
|
2284
|
+
// ONE raw source; `series` picks the series and `expand` is per-row
|
|
2285
|
+
// DATA — presence is what grows the ⤢ control on that row.
|
|
2286
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
2287
|
+
const OpsRow = StructType({
|
|
2288
|
+
series: StringType,
|
|
2289
|
+
label: StringType,
|
|
2290
|
+
expand: OptionType(Plan.Types.Expand),
|
|
2291
|
+
jobs: ArrayType(StructType({
|
|
2292
|
+
key: StringType, label: StringType,
|
|
2293
|
+
start: DateTimeType, end: DateTimeType, state: EventStateType,
|
|
2294
|
+
})),
|
|
2295
|
+
points: ArrayType(MeasureRow),
|
|
2296
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
2297
|
+
nums: ArrayType(Plan.Types.TableCell),
|
|
2298
|
+
marks: ArrayType(Plan.Types.EventMark),
|
|
2299
|
+
});
|
|
2300
|
+
const noJobs = $.const([], ArrayType(StructType({
|
|
2301
|
+
key: StringType, label: StringType,
|
|
2302
|
+
start: DateTimeType, end: DateTimeType, state: EventStateType,
|
|
2303
|
+
})));
|
|
2304
|
+
const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
|
|
2305
|
+
const noNums = $.const([], ArrayType(Plan.Types.TableCell));
|
|
2306
|
+
const noMarks = $.const([], ArrayType(Plan.Types.EventMark));
|
|
2307
|
+
const noPoints = $.const([], ArrayType(MeasureRow));
|
|
2308
|
+
const covPcts = $.const([96.1, 96.4, 96.8, 97.0, 96.2, 95.1, 93.4, 91.0, 88.9, 91.4, 93.8, 94.2], ArrayType(FloatType));
|
|
2309
|
+
const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: covPcts.get(i) })));
|
|
2310
|
+
const ops = $.const(new Map([
|
|
2311
|
+
// A CHART row — the one kind whose marks are a VALUE scale. Its
|
|
2312
|
+
// plot, its gutter ticks and its ref-label gate answer to the band
|
|
2313
|
+
// the marks keep at the top, not to the grown row (#591).
|
|
2314
|
+
["COVERAGE", { series: "chart", label: "COVERAGE",
|
|
2315
|
+
expand: some({ height: some("150px"), axis: variant("keep", null) }),
|
|
2316
|
+
jobs: noJobs, points: coverage, cells: noCells, nums: noNums, marks: noMarks }],
|
|
2317
|
+
// axis: keep — the grid and now-line run THROUGH the render.
|
|
2318
|
+
["L1-M03", { series: "span", label: "L1-M03",
|
|
2319
|
+
expand: some({ height: some("168px"), axis: variant("keep", null) }),
|
|
2320
|
+
jobs: [
|
|
2321
|
+
{ key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: variant("actual", null) },
|
|
2322
|
+
{ key: "qc", label: "QC", start: week(30n), end: week(32n), state: variant("confirmed", null) },
|
|
2323
|
+
{ key: "b231", label: "RUN · B-231", start: week(33n), end: week(38n), state: variant("proposed", variant("recommended", null)) },
|
|
2324
|
+
], points: noPoints, cells: noCells, nums: noNums, marks: noMarks }],
|
|
2325
|
+
// axis: dim — washed to 40% behind a dense render.
|
|
2326
|
+
["L1-M04", { series: "span", label: "L1-M04",
|
|
2327
|
+
expand: some({ height: some("140px"), axis: variant("dim", null) }),
|
|
2328
|
+
jobs: [
|
|
2329
|
+
{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(33n), state: variant("in-progress", null) },
|
|
2330
|
+
], points: noPoints, cells: noCells, nums: noNums, marks: noMarks }],
|
|
2331
|
+
// No declaration — no control. The contrast is the point: one row
|
|
2332
|
+
// that cannot be expanded beside five that can.
|
|
2333
|
+
["L1-M07", { series: "span", label: "L1-M07", expand: none,
|
|
2334
|
+
jobs: [
|
|
2335
|
+
{ key: "hld", label: "HLD · B-197", start: week(27n), end: week(31n), state: variant("actual", null) },
|
|
2336
|
+
], points: noPoints, cells: noCells, nums: noNums, marks: noMarks }],
|
|
2337
|
+
// The kinds that COLLAPSE differently — heat keeps its ramp, the
|
|
2338
|
+
// table re-encodes its numerals, the marks keep their silhouettes.
|
|
2339
|
+
["LOAD", { series: "heat", label: "Line load",
|
|
2340
|
+
expand: some({ height: some("132px"), axis: variant("keep", null) }),
|
|
2341
|
+
jobs: noJobs, points: noPoints, nums: noNums, marks: noMarks,
|
|
2342
|
+
cells: [
|
|
2343
|
+
{ at: Plan.at.time(week(27n)), value: some(46.0), label: some("46") },
|
|
2344
|
+
{ at: Plan.at.time(week(28n)), value: some(58.0), label: some("58") },
|
|
2345
|
+
{ at: Plan.at.time(week(29n)), value: some(66.0), label: some("66") },
|
|
2346
|
+
{ at: Plan.at.time(week(30n)), value: some(72.0), label: some("72") },
|
|
2347
|
+
{ at: Plan.at.time(week(31n)), value: some(84.0), label: some("84") },
|
|
2348
|
+
{ at: Plan.at.time(week(32n)), value: some(90.0), label: some("90") },
|
|
2349
|
+
{ at: Plan.at.time(week(33n)), value: some(96.0), label: some("96") },
|
|
2350
|
+
{ at: Plan.at.time(week(34n)), value: none, label: none },
|
|
2351
|
+
{ at: Plan.at.time(week(35n)), value: some(92.0), label: some("92") },
|
|
2352
|
+
] }],
|
|
2353
|
+
// axis: off — the render draws its own canvas, so the shared lines
|
|
2354
|
+
// are suppressed INSIDE this row only (the ruler never moves).
|
|
2355
|
+
["DESPATCH", { series: "table", label: "Despatch t",
|
|
2356
|
+
expand: some({ height: some("120px"), axis: variant("off", null) }),
|
|
2357
|
+
jobs: noJobs, points: noPoints, cells: noCells, marks: noMarks,
|
|
2358
|
+
nums: [
|
|
2359
|
+
{ at: Plan.at.time(week(27n)), value: some(128.0), text: none, tone: none },
|
|
2360
|
+
{ at: Plan.at.time(week(28n)), value: some(134.0), text: none, tone: none },
|
|
2361
|
+
{ at: Plan.at.time(week(29n)), value: some(119.0), text: none, tone: none },
|
|
2362
|
+
{ at: Plan.at.time(week(30n)), value: some(-96.0), text: none, tone: none },
|
|
2363
|
+
{ at: Plan.at.time(week(31n)), value: some(-88.0), text: none, tone: none },
|
|
2364
|
+
{ at: Plan.at.time(week(32n)), value: none, text: none, tone: none },
|
|
2365
|
+
{ at: Plan.at.time(week(33n)), value: some(151.0), text: none, tone: none },
|
|
2366
|
+
{ at: Plan.at.time(week(34n)), value: some(162.0), text: none, tone: none },
|
|
2367
|
+
{ at: Plan.at.time(week(35n)), value: some(144.0), text: none, tone: none },
|
|
2368
|
+
] }],
|
|
2369
|
+
["MILESTONES", { series: "events", label: "MILESTONES",
|
|
2370
|
+
expand: some({ height: some("112px"), axis: variant("dim", null) }),
|
|
2371
|
+
jobs: noJobs, points: noPoints, cells: noCells, nums: noNums,
|
|
2372
|
+
marks: [
|
|
2373
|
+
{ key: "k", at: Plan.at.time(week(28n)), kind: variant("milestone", null), icon: none, label: some("KICKOFF") },
|
|
2374
|
+
{ key: "d", at: Plan.at.time(week(31n)), kind: variant("decision", { applied: true }), icon: none, label: none },
|
|
2375
|
+
{ key: "a", at: Plan.at.time(week(34n)), kind: variant("exception", null), icon: none, label: some("AUDIT") },
|
|
2376
|
+
] }],
|
|
2377
|
+
]), DictType(StringType, OpsRow));
|
|
2378
|
+
// ONE resolver serves every declaring row, called with the row ref at
|
|
2379
|
+
// interaction time. The canvas hands it the PLOT column with the
|
|
2380
|
+
// shared grid + now-line drawn behind it — so a component that fills
|
|
2381
|
+
// that column edge to edge shares the canvas's x-space and lines up
|
|
2382
|
+
// with the buckets above. `Sparkline` is the axis-free chart, which is
|
|
2383
|
+
// what makes the alignment visible; a `Chart` would draw its own axes
|
|
2384
|
+
// and margins inside the column and align to those instead.
|
|
2385
|
+
const util = $.let(East.Array.generate(12n, FloatType, (_$, i) => i.multiply(19n).remainder(48n).toFloat().add(50.0)));
|
|
2386
|
+
const expandRender = $.const(East.function([Plan.Types.RowRef], UIComponentType, (_$, _ref) => (_jsx(Sparkline, { data: util, type: "area", color: "link", width: "100%", height: "100%" }))));
|
|
2387
|
+
// The GUTTER half. An expanded row's gutter cell grows with the row,
|
|
2388
|
+
// and what fills the space it opens up is the author's — the identity
|
|
2389
|
+
// and measures that only earn their place once the row has the canvas.
|
|
2390
|
+
// Same row ref as `expandRender`, so it can differ per row.
|
|
2391
|
+
// The old spec's drilled-row card, which is what the grown gutter is
|
|
2392
|
+
// for: identity lines then a fill meter. The lines need no styling —
|
|
2393
|
+
// the gutter body already carries the sub-line vocabulary — so the
|
|
2394
|
+
// author writes content, not typography.
|
|
2395
|
+
const GutterFacts = StructType({ a: StringType, b: StringType, fill: FloatType });
|
|
2396
|
+
const expandGutter = $.const(East.function([Plan.Types.RowRef], UIComponentType, ($, ref) => {
|
|
2397
|
+
const facts = $.const(new Map([
|
|
2398
|
+
["COVERAGE", { a: "TARGET 100 · MIN 92", b: "BREACH W34–W36 · 3 wk", fill: 0.94 }],
|
|
2399
|
+
["L1-M03", { a: "120 t · FILL", b: "B-208 · 88 t · 73%", fill: 0.73 }],
|
|
2400
|
+
["L1-M04", { a: "120 t · FILL", b: "B-214 · 89 t · 74%", fill: 0.74 }],
|
|
2401
|
+
["LOAD", { a: "MEAN 74 · PEAK 96", b: "BREACH W33 · 1 wk", fill: 0.96 }],
|
|
2402
|
+
["DESPATCH", { a: "NET 1 629 t", b: "2 SHORT WEEKS", fill: 0.55 }],
|
|
2403
|
+
["MILESTONES", { a: "5 MARKS", b: "1 EXCEPTION · W34", fill: 0.2 }],
|
|
2404
|
+
]), DictType(StringType, GutterFacts));
|
|
2405
|
+
const f = $.let(facts.get(ref.key));
|
|
2406
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: f.a }), _jsx(Text, { children: f.b }), _jsx(Box, { width: "108px", children: _jsx(Progress, { value: f.fill.multiply(100.0), size: "xs", tone: "brand" }) })] }));
|
|
2407
|
+
}));
|
|
2408
|
+
return (_jsx(Plan, { axis: Plan.axis({ window: { min: week(27n), max: week(39n) }, resolution: "week", now: week(31n) }), data: ops, series: [
|
|
2409
|
+
Plan.series.span(OpsRow, {
|
|
2410
|
+
key: "span-6", title: "Span",
|
|
2411
|
+
match: r => r.series.equal("span"),
|
|
2412
|
+
label: r => r.label, id: true, expand: r => r.expand,
|
|
2413
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
2414
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
2415
|
+
})),
|
|
2416
|
+
}),
|
|
2417
|
+
Plan.series.chart(OpsRow, {
|
|
2418
|
+
key: "chart-2", title: "Chart",
|
|
2419
|
+
match: r => r.series.equal("chart"),
|
|
2420
|
+
label: r => r.label, id: true, expand: r => r.expand,
|
|
2421
|
+
height: "spark",
|
|
2422
|
+
left: { domain: [80, 110], tickValues: [80, 100] },
|
|
2423
|
+
layers: r => [
|
|
2424
|
+
Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
|
|
2425
|
+
Chart.refLine({ y: 100, label: "TARGET 100" }),
|
|
2426
|
+
],
|
|
2427
|
+
}),
|
|
2428
|
+
Plan.series.heat(OpsRow, {
|
|
2429
|
+
key: "heat-3", title: "Heat",
|
|
2430
|
+
match: r => r.series.equal("heat"),
|
|
2431
|
+
label: r => r.label, expand: r => r.expand,
|
|
2432
|
+
cells: r => Plan.heatCells(r.cells, { min: 40.0, max: 100.0 }),
|
|
2433
|
+
}),
|
|
2434
|
+
Plan.series.table(OpsRow, {
|
|
2435
|
+
key: "table", title: "Table",
|
|
2436
|
+
match: r => r.series.equal("table"),
|
|
2437
|
+
label: r => r.label, expand: r => r.expand,
|
|
2438
|
+
cells: r => r.nums,
|
|
2439
|
+
}),
|
|
2440
|
+
Plan.series.events(OpsRow, {
|
|
2441
|
+
key: "events-2", title: "Events",
|
|
2442
|
+
match: r => r.series.equal("events"),
|
|
2443
|
+
label: r => r.label, id: true, expand: r => r.expand,
|
|
2444
|
+
marks: r => r.marks,
|
|
2445
|
+
}),
|
|
2446
|
+
], expandRender: expandRender, expandGutter: expandGutter, style: { height: "460px" } }));
|
|
2447
|
+
}),
|
|
2448
|
+
inputs: [],
|
|
2449
|
+
});
|
|
2450
|
+
// ============================================================================
|
|
2451
|
+
// planNarrow — the §10 narrow layout: a phone-width box makes the Plan a
|
|
2452
|
+
// review tool, not a canvas (#570)
|
|
2453
|
+
// ============================================================================
|
|
2454
|
+
/**
|
|
2455
|
+
* Below 480px of CONTAINER width — not the viewport: this example is a 360px
|
|
2456
|
+
* `<Box>` on a desktop page — the Plan reflows to the §10 layout: three tabs
|
|
2457
|
+
* over ONE slice (Groups · Rows · Measures), the group grain as hottest-first
|
|
2458
|
+
* strip cards, one group's rows as cards whose head is the row's gutter
|
|
2459
|
+
* identity and whose body is its plot on the shared window, chart rows
|
|
2460
|
+
* full-width at expanded density. A row declaring `expand` drills in place on
|
|
2461
|
+
* a second tap; horizontal pan is two-finger. The DEFINITION is the desktop
|
|
2462
|
+
* one — the same `data` + `series`, the same slice — only the box changed.
|
|
2463
|
+
*/
|
|
2464
|
+
export const planNarrow = example({
|
|
2465
|
+
keywords: ["Plan", "narrow", "mobile", "phone", "responsive", "compact", "container", "breakpoint", "tabs", "Groups", "Rows", "Measures", "cards", "strip", "hottest", "two-finger", "pan", "review", "cohort", "slice", "§10", "raw"],
|
|
2466
|
+
description: "The narrow layout — a phone-width box turns the same canvas into a review tool: Groups · Rows · Measures tabs, hottest-first strip cards, rows as cards, charts at expanded density",
|
|
2467
|
+
fn: East.function([], UIComponentType, (_$) => {
|
|
2468
|
+
const HorizonRow = StructType({ key: StringType, at: DateTimeType, risk: StringType });
|
|
2469
|
+
const cfg = Slice.config(HorizonRow, {
|
|
2470
|
+
fields: { at: { label: "Despatched", format: { date: "MMM D" } }, risk: { label: "Risk", hints: ["late", "on-time"] } },
|
|
2471
|
+
rangeFieldId: "at",
|
|
2472
|
+
});
|
|
2473
|
+
return (_jsx(Reactive, { children: $ => {
|
|
2474
|
+
// Monday of ISO week n, 2026 — window W27–W38 (half-open), now W31.
|
|
2475
|
+
const week = $.const(East.function([IntegerType], DateTimeType, ($, n) => {
|
|
2476
|
+
const w1 = $.const(new Date("2025-12-29T00:00:00Z"), DateTimeType);
|
|
2477
|
+
return w1.addWeeks(n.subtract(1n));
|
|
2478
|
+
}));
|
|
2479
|
+
const MeasureRow = StructType({ week: DateTimeType, pct: FloatType });
|
|
2480
|
+
const JobRow = StructType({
|
|
2481
|
+
key: StringType, label: StringType,
|
|
2482
|
+
start: DateTimeType, end: DateTimeType, state: EventStateType,
|
|
2483
|
+
});
|
|
2484
|
+
// ONE raw source: `series` picks the series, `line` the group a row
|
|
2485
|
+
// belongs to (the KPI belongs to none — it rides the Measures tab
|
|
2486
|
+
// and an "Other rows" card), and every envelope field is per-row DATA.
|
|
2487
|
+
const OpsRow = StructType({
|
|
2488
|
+
series: StringType, line: StringType, label: StringType,
|
|
2489
|
+
value: OptionType(StringType), status: OptionType(StatusValueType),
|
|
2490
|
+
expand: OptionType(Plan.Types.Expand),
|
|
2491
|
+
jobs: ArrayType(JobRow),
|
|
2492
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
2493
|
+
points: ArrayType(MeasureRow),
|
|
2494
|
+
});
|
|
2495
|
+
const noJobs = $.const([], ArrayType(JobRow));
|
|
2496
|
+
const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
|
|
2497
|
+
const noPoints = $.const([], ArrayType(MeasureRow));
|
|
2498
|
+
const loadPcts = $.const([46.0, 52.0, 58.0, 61.0, 66.0, 72.0, 78.0, 84.0, 90.0, 96.0, 98.0, 92.0], ArrayType(FloatType));
|
|
2499
|
+
// Line 2 runs hotter than Line 1 — the Groups tab sorts it first.
|
|
2500
|
+
const loadA = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
|
|
2501
|
+
at: Plan.at.time(week(i.add(27n))), value: some(loadPcts.get(i).multiply(0.85)),
|
|
2502
|
+
label: some(East.Float.printFixed(loadPcts.get(i).multiply(0.85), 0n)),
|
|
2503
|
+
})));
|
|
2504
|
+
const loadB = $.let(East.Array.generate(12n, Plan.Types.HeatCell, (_$, i) => ({
|
|
2505
|
+
at: Plan.at.time(week(i.add(27n))), value: some(loadPcts.get(i)),
|
|
2506
|
+
label: some(East.Float.printFixed(loadPcts.get(i), 0n)),
|
|
2507
|
+
})));
|
|
2508
|
+
const covPcts = $.const([96.1, 96.4, 96.8, 97.0, 96.2, 95.1, 93.4, 91.0, 88.9, 91.4, 93.8, 94.2], ArrayType(FloatType));
|
|
2509
|
+
const coverage = $.let(East.Array.generate(12n, MeasureRow, (_$, i) => ({ week: week(i.add(27n)), pct: covPcts.get(i) })));
|
|
2510
|
+
const ops = $.const(new Map([
|
|
2511
|
+
["L1-M03", { series: "mach", line: "Line 1 · Fill", label: "L1-M03",
|
|
2512
|
+
value: some("120 t"), status: some(variant("success", null)),
|
|
2513
|
+
expand: some({ height: some("140px"), axis: variant("keep", null) }),
|
|
2514
|
+
jobs: [
|
|
2515
|
+
{ key: "b208", label: "RUN · B-208", start: week(27n), end: week(30n), state: variant("actual", null) },
|
|
2516
|
+
{ key: "qc", label: "QC", start: week(30n), end: week(32n), state: variant("confirmed", null) },
|
|
2517
|
+
{ key: "b231", label: "RUN · B-231", start: week(33n), end: week(38n), state: variant("proposed", variant("recommended", null)) },
|
|
2518
|
+
], cells: noCells, points: noPoints }],
|
|
2519
|
+
["L1-M04", { series: "mach", line: "Line 1 · Fill", label: "L1-M04",
|
|
2520
|
+
value: some("120 t"), status: some(variant("warning", null)), expand: none,
|
|
2521
|
+
jobs: [
|
|
2522
|
+
{ key: "b214", label: "RUN · B-214", start: week(28n), end: week(33n), state: variant("in-progress", null) },
|
|
2523
|
+
], cells: noCells, points: noPoints }],
|
|
2524
|
+
["l1-load", { series: "load", line: "Line 1 · Fill", label: "Line load",
|
|
2525
|
+
value: none, status: none, expand: none,
|
|
2526
|
+
jobs: noJobs, cells: loadA, points: noPoints }],
|
|
2527
|
+
["L2-M11", { series: "mach", line: "Line 2 · Assy", label: "L2-M11",
|
|
2528
|
+
value: some("80 t"), status: none, expand: none,
|
|
2529
|
+
jobs: [
|
|
2530
|
+
{ key: "b241", label: "RUN · B-241", start: week(29n), end: week(34n), state: variant("confirmed", null) },
|
|
2531
|
+
], cells: noCells, points: noPoints }],
|
|
2532
|
+
["l2-load", { series: "load", line: "Line 2 · Assy", label: "Line load",
|
|
2533
|
+
value: none, status: some(variant("warning", null)), expand: none,
|
|
2534
|
+
jobs: noJobs, cells: loadB, points: noPoints }],
|
|
2535
|
+
["coverage", { series: "kpi", line: "", label: "COVERAGE",
|
|
2536
|
+
value: some("94.2%"), status: none, expand: none,
|
|
2537
|
+
jobs: noJobs, cells: noCells, points: coverage }],
|
|
2538
|
+
]), DictType(StringType, OpsRow));
|
|
2539
|
+
// The slice's own rows — despatch orders with a late-risk cohort
|
|
2540
|
+
// seeded active, so the chips row has something to say.
|
|
2541
|
+
const horizon = $.let(East.Array.generate(24n, HorizonRow, (_$, i) => ({
|
|
2542
|
+
key: East.str `h${East.print(i.add(1n))}`,
|
|
2543
|
+
at: week(i.divide(2n).add(27n)),
|
|
2544
|
+
risk: i.remainder(3n).equal(0n).ifElse(() => "late", () => "on-time"),
|
|
2545
|
+
})));
|
|
2546
|
+
const slice = $.let(Slice.bind([HorizonRow], "ex.plan.narrow", cfg, Slice.state({
|
|
2547
|
+
range: some(variant("datetime", { from: week(27n), to: week(39n) })),
|
|
2548
|
+
cohorts: [{ id: "late", name: "Late risk", filters: [variant("string", { fieldId: "risk", op: variant("eq", "late") })] }],
|
|
2549
|
+
activeCohorts: new Set(["late"]),
|
|
2550
|
+
}), horizon, none));
|
|
2551
|
+
const series = $.const([
|
|
2552
|
+
// One strip per line, DISCOVERED from the data; its summary
|
|
2553
|
+
// strip is the max of its heat rows — what "hottest first" reads.
|
|
2554
|
+
Plan.series.group(OpsRow, {
|
|
2555
|
+
key: "lines", title: "Lines",
|
|
2556
|
+
by: r => r.line, match: r => r.line.equal("").not(),
|
|
2557
|
+
keyPrefix: "g-", summaryAggregate: "max",
|
|
2558
|
+
}, [
|
|
2559
|
+
Plan.series.span(OpsRow, {
|
|
2560
|
+
key: "mach", title: "Machines",
|
|
2561
|
+
match: r => r.series.equal("mach"),
|
|
2562
|
+
label: r => r.label, id: true,
|
|
2563
|
+
value: r => r.value, status: r => r.status, expand: r => r.expand,
|
|
2564
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({
|
|
2565
|
+
key: j.key, start: j.start, end: j.end, label: j.label, state: j.state,
|
|
2566
|
+
})),
|
|
2567
|
+
}),
|
|
2568
|
+
Plan.series.heat(OpsRow, {
|
|
2569
|
+
key: "load", title: "Line load",
|
|
2570
|
+
match: r => r.series.equal("load"),
|
|
2571
|
+
label: r => r.label, status: r => r.status,
|
|
2572
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 95 }),
|
|
2573
|
+
}),
|
|
2574
|
+
]),
|
|
2575
|
+
Plan.series.chart(OpsRow, {
|
|
2576
|
+
key: "kpi", title: "Coverage",
|
|
2577
|
+
match: r => r.series.equal("kpi"),
|
|
2578
|
+
label: r => r.label, id: true, value: r => r.value,
|
|
2579
|
+
height: "spark",
|
|
2580
|
+
left: { domain: [80, 110], tickValues: [80, 100] },
|
|
2581
|
+
layers: r => [
|
|
2582
|
+
Plan.layer(Chart.Line(r.points, { x: p => p.week, y: p => p.pct }), { breach: { below: 92 } }),
|
|
2583
|
+
Chart.refLine({ y: 100, label: "TARGET 100" }),
|
|
2584
|
+
],
|
|
2585
|
+
}),
|
|
2586
|
+
], ArrayType(Plan.Types.Series(OpsRow)));
|
|
2587
|
+
const util = $.let(East.Array.generate(12n, FloatType, (_$, i) => i.multiply(19n).remainder(48n).toFloat().add(50.0)));
|
|
2588
|
+
const expandRender = $.const(East.function([Plan.Types.RowRef], UIComponentType, (_$, _ref) => (_jsx(Sparkline, { data: util, type: "area", color: "link", width: "100%", height: "100%" }))));
|
|
2589
|
+
const axis = $.const(Plan.axis({
|
|
2590
|
+
window: { min: week(27n), max: week(39n) },
|
|
2591
|
+
resolution: "week", resolutions: ["month", "week", "day"], now: week(31n),
|
|
2592
|
+
}));
|
|
2593
|
+
// The 360px box is the whole point: the reflow is a property of
|
|
2594
|
+
// the CONTAINER, so a phone, a splitter pane and this box agree.
|
|
2595
|
+
return (_jsx(Box, { width: "360px", children: _jsx(Plan, { axis: axis, data: ops, series: series, slice: { slice, affordances: ["cohort", "filter", "range", "resolution", "summary"] }, expandRender: expandRender, footer: [
|
|
2596
|
+
{ text: "6 ROWS · 2 LINES" },
|
|
2597
|
+
{ text: "RUN 412 · W27–W38", end: true },
|
|
2598
|
+
], style: { height: "560px" } }) }));
|
|
2599
|
+
} }));
|
|
2600
|
+
}),
|
|
2601
|
+
inputs: [],
|
|
2602
|
+
});
|
|
2603
|
+
// ============================================================================
|
|
2604
|
+
// planNumberAxis — the `number` axis (#631): day 1..8 with AM/PM lanes
|
|
2605
|
+
// ============================================================================
|
|
2606
|
+
/**
|
|
2607
|
+
* The `number` axis — the retired Planner's `plannerPoint` canvas on the
|
|
2608
|
+
* Plan: eight days at step 1, AM/PM lanes in the bucket rows, `now` at day
|
|
2609
|
+
* 5. Every kind positions on the one numeric scale, and every instant in the
|
|
2610
|
+
* raw data is a plain number: a `FloatType` field wraps to the `number` arm
|
|
2611
|
+
* through the element builders (`Plan.run({ start: j.start })`), a chart
|
|
2612
|
+
* layer's numeric x accessor lands its columns on the same arm, `Plan.tableCells`
|
|
2613
|
+
* reads a numeric `at`, and the heat cells stored as RECORDS spell it out with
|
|
2614
|
+
* `Plan.at.number`. The slice's range field is a float, so the horizon brush
|
|
2615
|
+
* and the range chip ride the slice's `float` arm exactly as they ride
|
|
2616
|
+
* `datetime` on a time axis; there is no resolution segment — `step` is the
|
|
2617
|
+
* declaration.
|
|
2618
|
+
*/
|
|
2619
|
+
export const planNumberAxis = example({
|
|
2620
|
+
keywords: [
|
|
2621
|
+
"Plan", "axis", "number", "numeric", "step", "Plan.axis.number", "Plan.at", "instant",
|
|
2622
|
+
"day", "AM", "PM", "lanes", "buckets", "Planner", "plannerPoint", "brush", "float", "range",
|
|
2623
|
+
"format", "now", "typed axis", "#631", "raw",
|
|
2624
|
+
],
|
|
2625
|
+
description: "The number axis — day 1..8 at step 1 with AM/PM lanes (the retired Planner's plannerPoint), every row kind on the numeric scale, the horizon brush over the slice's float range",
|
|
2626
|
+
fn: East.function([], UIComponentType, (_$) => {
|
|
2627
|
+
const HorizonRow = StructType({ key: StringType, day: FloatType, line: StringType });
|
|
2628
|
+
const cfg = Slice.config(HorizonRow, {
|
|
2629
|
+
fields: { day: { label: "Day" }, line: { label: "Line" } },
|
|
2630
|
+
rangeFieldId: "day",
|
|
2631
|
+
});
|
|
2632
|
+
return (_jsx(Reactive, { children: $ => {
|
|
2633
|
+
// RAW rows — every instant is a plain number (a day index).
|
|
2634
|
+
const AllocRow = StructType({ key: StringType, day: FloatType, lane: StringType, state: EventStateType });
|
|
2635
|
+
const JobRow = StructType({ key: StringType, label: StringType, start: FloatType, end: FloatType, state: EventStateType });
|
|
2636
|
+
const ShiftRow = StructType({ key: StringType, from: FloatType, to: FloatType, hours: FloatType, state: EventStateType });
|
|
2637
|
+
const MarkRow = StructType({ key: StringType, day: FloatType, label: StringType });
|
|
2638
|
+
const PointRow = StructType({ day: FloatType, t: FloatType });
|
|
2639
|
+
const RawCell = StructType({ at: FloatType, value: OptionType(FloatType) });
|
|
2640
|
+
const OpsRow = StructType({
|
|
2641
|
+
series: StringType, label: StringType, value: OptionType(StringType),
|
|
2642
|
+
allocations: ArrayType(AllocRow), jobs: ArrayType(JobRow), shifts: ArrayType(ShiftRow),
|
|
2643
|
+
marks: ArrayType(MarkRow), points: ArrayType(PointRow), tonnes: ArrayType(RawCell),
|
|
2644
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
2645
|
+
});
|
|
2646
|
+
const noAllocs = $.const([], ArrayType(AllocRow));
|
|
2647
|
+
const noJobs = $.const([], ArrayType(JobRow));
|
|
2648
|
+
const noShifts = $.const([], ArrayType(ShiftRow));
|
|
2649
|
+
const noMarks = $.const([], ArrayType(MarkRow));
|
|
2650
|
+
const noPoints = $.const([], ArrayType(PointRow));
|
|
2651
|
+
const noTonnes = $.const([], ArrayType(RawCell));
|
|
2652
|
+
const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
|
|
2653
|
+
const CONFIRMED = variant("confirmed", null);
|
|
2654
|
+
const ACTUAL = variant("actual", null);
|
|
2655
|
+
const RUNNING = variant("in-progress", null);
|
|
2656
|
+
const PROPOSED = variant("proposed", variant("recommended", null));
|
|
2657
|
+
// Per-day series over days 1..8 — derived, never hand-written.
|
|
2658
|
+
const points = $.let(East.Array.generate(8n, PointRow, (_$, i) => ({ day: i.toFloat().add(1.0), t: i.multiply(17n).remainder(40n).toFloat().add(60.0) })));
|
|
2659
|
+
const tonnes = $.let(East.Array.generate(8n, RawCell, (_$, i) => ({ at: i.toFloat().add(1.0), value: some(i.multiply(23n).remainder(70n).toFloat().add(80.0)) })));
|
|
2660
|
+
// Heat cells as STORED records — the instant spelled explicitly.
|
|
2661
|
+
const cells = $.let(East.Array.generate(8n, Plan.Types.HeatCell, ($, i) => {
|
|
2662
|
+
const load = $.let(i.multiply(29n).remainder(55n).toFloat().add(40.0), FloatType);
|
|
2663
|
+
return { at: Plan.at.number(i.toFloat().add(1.0)), value: some(load), label: some(East.Float.printFixed(load, 0n)) };
|
|
2664
|
+
}));
|
|
2665
|
+
const base = {
|
|
2666
|
+
value: none, allocations: noAllocs, jobs: noJobs, shifts: noShifts,
|
|
2667
|
+
marks: noMarks, points: noPoints, tonnes: noTonnes, cells: noCells,
|
|
2668
|
+
};
|
|
2669
|
+
// Keys order the canvas (#568) — numbered so each series lands where the
|
|
2670
|
+
// §1 layout puts it.
|
|
2671
|
+
const ops = $.const(new Map([
|
|
2672
|
+
["10-dock2", { ...base, series: "dock", label: "Dock 2", value: some("load/day"), allocations: [
|
|
2673
|
+
{ key: "a1", day: 1.0, lane: "am", state: CONFIRMED },
|
|
2674
|
+
{ key: "a2", day: 1.0, lane: "pm", state: CONFIRMED },
|
|
2675
|
+
{ key: "a3", day: 2.0, lane: "am", state: CONFIRMED },
|
|
2676
|
+
{ key: "a4", day: 3.0, lane: "pm", state: PROPOSED },
|
|
2677
|
+
{ key: "a5", day: 5.0, lane: "am", state: PROPOSED },
|
|
2678
|
+
{ key: "a6", day: 6.0, lane: "pm", state: CONFIRMED },
|
|
2679
|
+
{ key: "a7", day: 8.0, lane: "am", state: PROPOSED },
|
|
2680
|
+
] }],
|
|
2681
|
+
["10-dock5", { ...base, series: "dock", label: "Dock 5", value: some("load/day"), allocations: [
|
|
2682
|
+
{ key: "b1", day: 2.0, lane: "pm", state: CONFIRMED },
|
|
2683
|
+
{ key: "b2", day: 4.0, lane: "am", state: CONFIRMED },
|
|
2684
|
+
{ key: "b3", day: 4.0, lane: "pm", state: PROPOSED },
|
|
2685
|
+
{ key: "b4", day: 7.0, lane: "am", state: PROPOSED },
|
|
2686
|
+
] }],
|
|
2687
|
+
["20-m03", { ...base, series: "mach", label: "L1-M03", value: some("120 t"), jobs: [
|
|
2688
|
+
{ key: "set", label: "SET", start: 1.0, end: 2.0, state: ACTUAL },
|
|
2689
|
+
{ key: "b214", label: "RUN · B-214", start: 2.0, end: 5.0, state: RUNNING },
|
|
2690
|
+
{ key: "b221", label: "RUN · B-221", start: 6.0, end: 8.0, state: PROPOSED },
|
|
2691
|
+
] }],
|
|
2692
|
+
["20-m04", { ...base, series: "mach", label: "L1-M04", value: some("80 t"), jobs: [
|
|
2693
|
+
{ key: "b208", label: "RUN · B-208", start: 1.0, end: 4.0, state: ACTUAL },
|
|
2694
|
+
{ key: "qc", label: "QC", start: 4.0, end: 5.0, state: CONFIRMED },
|
|
2695
|
+
{ key: "b231", label: "RUN · B-231", start: 5.0, end: 9.0, state: PROPOSED },
|
|
2696
|
+
] }],
|
|
2697
|
+
["30-load", { ...base, series: "load", label: "Line load", cells }],
|
|
2698
|
+
["40-out", { ...base, series: "out", label: "OUTPUT · t", value: some("612 t"), points }],
|
|
2699
|
+
["50-desp", { ...base, series: "table", label: "Despatch t", tonnes }],
|
|
2700
|
+
["60-crewA", { ...base, series: "crew", label: "Crew A", shifts: [
|
|
2701
|
+
{ key: "s1", from: 1.0, to: 3.0, hours: 24.0, state: CONFIRMED },
|
|
2702
|
+
{ key: "s2", from: 3.0, to: 6.0, hours: 36.0, state: CONFIRMED },
|
|
2703
|
+
{ key: "s3", from: 6.0, to: 8.0, hours: 24.0, state: PROPOSED },
|
|
2704
|
+
] }],
|
|
2705
|
+
["70-ms", { ...base, series: "ms", label: "MILESTONES", marks: [
|
|
2706
|
+
{ key: "kick", day: 2.0, label: "KICKOFF" },
|
|
2707
|
+
{ key: "rel", day: 6.0, label: "REL" },
|
|
2708
|
+
] }],
|
|
2709
|
+
]), DictType(StringType, OpsRow));
|
|
2710
|
+
const series = $.const([
|
|
2711
|
+
Plan.series.group(OpsRow, { key: "10-docks", label: "Docks · In", meta: "2 rs" }, [
|
|
2712
|
+
Plan.series.buckets(OpsRow, {
|
|
2713
|
+
key: "dock", title: "Docks",
|
|
2714
|
+
match: r => r.series.equal("dock"),
|
|
2715
|
+
label: r => r.label, value: r => r.value,
|
|
2716
|
+
// The Planner's AM/PM lanes — sub-slots of each day column.
|
|
2717
|
+
lanes: _r => [Plan.lane({ key: "am", label: "AM" }), Plan.lane({ key: "pm", label: "PM" })],
|
|
2718
|
+
// `a.day` is a FloatType field — the builder wraps it to the number arm.
|
|
2719
|
+
events: r => r.allocations.map((_$, a) => Plan.event({ key: a.key, at: a.day, lane: a.lane, state: a.state })),
|
|
2720
|
+
}),
|
|
2721
|
+
]),
|
|
2722
|
+
Plan.series.span(OpsRow, {
|
|
2723
|
+
key: "mach", title: "Machines",
|
|
2724
|
+
match: r => r.series.equal("mach"),
|
|
2725
|
+
label: r => r.label, id: true, value: r => r.value,
|
|
2726
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state })),
|
|
2727
|
+
}),
|
|
2728
|
+
Plan.series.heat(OpsRow, {
|
|
2729
|
+
key: "load", title: "Line load",
|
|
2730
|
+
match: r => r.series.equal("load"),
|
|
2731
|
+
label: r => r.label,
|
|
2732
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 90 }),
|
|
2733
|
+
}),
|
|
2734
|
+
Plan.series.chart(OpsRow, {
|
|
2735
|
+
key: "out", title: "Output",
|
|
2736
|
+
match: r => r.series.equal("out"),
|
|
2737
|
+
label: r => r.label, id: true, value: r => r.value, height: "expanded",
|
|
2738
|
+
// A numeric x accessor lands the columns on the number arm.
|
|
2739
|
+
layers: r => [Chart.Column(r.points, { x: p => p.day, y: p => p.t })],
|
|
2740
|
+
}),
|
|
2741
|
+
Plan.series.table(OpsRow, {
|
|
2742
|
+
key: "table", title: "Despatch",
|
|
2743
|
+
match: r => r.series.equal("table"),
|
|
2744
|
+
label: r => r.label,
|
|
2745
|
+
// A `{ at: FloatType, value }` record wraps by its field type.
|
|
2746
|
+
cells: r => Plan.tableCells(r.tonnes),
|
|
2747
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
2748
|
+
}),
|
|
2749
|
+
Plan.series.cards(OpsRow, {
|
|
2750
|
+
key: "crew", title: "Crews",
|
|
2751
|
+
match: r => r.series.equal("crew"),
|
|
2752
|
+
label: r => r.label,
|
|
2753
|
+
chips: r => r.shifts.map((_$, s) => Plan.chip({
|
|
2754
|
+
key: s.key, from: s.from, to: s.to,
|
|
2755
|
+
label: East.str `${East.Float.printFixed(s.hours, 0n)}h`, state: s.state,
|
|
2756
|
+
})),
|
|
2757
|
+
}),
|
|
2758
|
+
Plan.series.events(OpsRow, {
|
|
2759
|
+
key: "ms", title: "Milestones",
|
|
2760
|
+
match: r => r.series.equal("ms"),
|
|
2761
|
+
label: r => r.label, id: true,
|
|
2762
|
+
marks: r => r.marks.map((_$, m) => Plan.mark({ key: m.key, at: m.day, kind: "milestone", label: m.label })),
|
|
2763
|
+
}),
|
|
2764
|
+
], ArrayType(Plan.Types.Series(OpsRow)));
|
|
2765
|
+
// The slice's horizon — twelve days of orders behind an eight-day
|
|
2766
|
+
// window. Its range field is a FLOAT, so the brush, the range chip
|
|
2767
|
+
// and the window keys write the slice's `float` arm.
|
|
2768
|
+
const horizon = $.let(East.Array.generate(24n, HorizonRow, (_$, i) => ({
|
|
2769
|
+
key: East.str `o${East.print(i.add(1n))}`,
|
|
2770
|
+
day: i.divide(2n).toFloat().add(1.0),
|
|
2771
|
+
line: i.remainder(2n).equal(0n).ifElse(() => "Line 1", () => "Line 2"),
|
|
2772
|
+
})));
|
|
2773
|
+
const slice = $.let(Slice.bind([HorizonRow], "ex.plan.number", cfg, Slice.state({
|
|
2774
|
+
range: some(variant("float", { from: 1.0, to: 9.0 })),
|
|
2775
|
+
}), horizon, none));
|
|
2776
|
+
// The declaration: `[1, 9)` ÷ 1 = eight day columns, the divider at 5.
|
|
2777
|
+
const axis = $.const(Plan.axis.number({
|
|
2778
|
+
window: { min: 1, max: 9 }, step: 1, now: 5, format: Chart.format.number(),
|
|
2779
|
+
}));
|
|
2780
|
+
return (_jsx(Plan, { axis: axis, data: ops, series: series, slice: { slice, affordances: ["filter", "range", "brush", "summary"] }, footer: [
|
|
2781
|
+
{ text: "8 DAYS · STEP 1 · NOW 5" },
|
|
2782
|
+
{ text: "NUMBER AXIS", end: true },
|
|
2783
|
+
] }));
|
|
2784
|
+
} }));
|
|
2785
|
+
}),
|
|
2786
|
+
inputs: [],
|
|
2787
|
+
});
|
|
2788
|
+
// ============================================================================
|
|
2789
|
+
// planOrdinalAxis — the `ordinal` axis (#631): workflow phases
|
|
2790
|
+
// ============================================================================
|
|
2791
|
+
/**
|
|
2792
|
+
* The `ordinal` axis — a workflow of six phases, each a bucket, in declared
|
|
2793
|
+
* order. Instants are the phase VALUES: a run's `start` / `end` are phase
|
|
2794
|
+
* names (an interval covers `[start, end]` in phase order — on an ordinal
|
|
2795
|
+
* axis the end names the LAST bucket covered, since values are buckets, not
|
|
2796
|
+
* edges), a tile sits in a phase, a chart's string x accessor lands its
|
|
2797
|
+
* columns on the phase arm, `Plan.tableCells` reads a string `at`, and
|
|
2798
|
+
* `Plan.at.ordinal` spells one out where a record is written as data. There
|
|
2799
|
+
* is no slice range for an ordinal axis — the list IS the window, so the
|
|
2800
|
+
* horizon brush does not mount and the window keys idle; `now` names a phase.
|
|
2801
|
+
*/
|
|
2802
|
+
export const planOrdinalAxis = example({
|
|
2803
|
+
keywords: [
|
|
2804
|
+
"Plan", "axis", "ordinal", "phase", "phases", "workflow", "stage", "Plan.axis.ordinal",
|
|
2805
|
+
"Plan.at", "instant", "values", "list", "span", "buckets", "heat", "chart", "table",
|
|
2806
|
+
"cards", "events", "Planner", "typed axis", "#631", "raw",
|
|
2807
|
+
],
|
|
2808
|
+
description: "The ordinal axis — six workflow phases as the buckets, in declared order; orders span phases, tiles and cells sit in them, a string x accessor lands a chart on them",
|
|
2809
|
+
fn: East.function([], UIComponentType, ($) => {
|
|
2810
|
+
const PHASES = $.const(["INTAKE", "PREP", "BUILD", "QC", "PACK", "SHIP"], ArrayType(StringType));
|
|
2811
|
+
// RAW rows — every instant is a phase NAME.
|
|
2812
|
+
const JobRow = StructType({ key: StringType, label: StringType, start: StringType, end: StringType, state: EventStateType });
|
|
2813
|
+
const AllocRow = StructType({ key: StringType, phase: StringType, state: EventStateType });
|
|
2814
|
+
const ShiftRow = StructType({ key: StringType, from: StringType, to: StringType, label: StringType, state: EventStateType });
|
|
2815
|
+
const MarkRow = StructType({ key: StringType, phase: StringType, label: StringType, exception: BooleanType });
|
|
2816
|
+
const PointRow = StructType({ phase: StringType, n: FloatType });
|
|
2817
|
+
const RawCell = StructType({ at: StringType, value: OptionType(FloatType) });
|
|
2818
|
+
const OrderRow = StructType({
|
|
2819
|
+
series: StringType, label: StringType, value: OptionType(StringType),
|
|
2820
|
+
jobs: ArrayType(JobRow), allocations: ArrayType(AllocRow), shifts: ArrayType(ShiftRow),
|
|
2821
|
+
marks: ArrayType(MarkRow), points: ArrayType(PointRow), counts: ArrayType(RawCell),
|
|
2822
|
+
cells: ArrayType(Plan.Types.HeatCell),
|
|
2823
|
+
});
|
|
2824
|
+
const noJobs = $.const([], ArrayType(JobRow));
|
|
2825
|
+
const noAllocs = $.const([], ArrayType(AllocRow));
|
|
2826
|
+
const noShifts = $.const([], ArrayType(ShiftRow));
|
|
2827
|
+
const noMarks = $.const([], ArrayType(MarkRow));
|
|
2828
|
+
const noPoints = $.const([], ArrayType(PointRow));
|
|
2829
|
+
const noCounts = $.const([], ArrayType(RawCell));
|
|
2830
|
+
const noCells = $.const([], ArrayType(Plan.Types.HeatCell));
|
|
2831
|
+
const CONFIRMED = variant("confirmed", null);
|
|
2832
|
+
const ACTUAL = variant("actual", null);
|
|
2833
|
+
const RUNNING = variant("in-progress", null);
|
|
2834
|
+
const PROPOSED = variant("proposed", variant("recommended", null));
|
|
2835
|
+
// Per-phase series derived over the declared list — the index picks the value.
|
|
2836
|
+
const points = $.let(East.Array.generate(6n, PointRow, (_$, i) => ({ phase: PHASES.get(i), n: i.multiply(7n).remainder(20n).toFloat().add(4.0) })));
|
|
2837
|
+
const counts = $.let(East.Array.generate(6n, RawCell, (_$, i) => ({
|
|
2838
|
+
at: PHASES.get(i),
|
|
2839
|
+
value: i.equal(4n).ifElse(() => none, () => some(i.multiply(11n).remainder(30n).toFloat().add(12.0))),
|
|
2840
|
+
})));
|
|
2841
|
+
// Heat cells as STORED records — the phase spelled with `Plan.at.ordinal`.
|
|
2842
|
+
const cells = $.let(East.Array.generate(6n, Plan.Types.HeatCell, ($, i) => {
|
|
2843
|
+
const load = $.let(i.multiply(31n).remainder(60n).toFloat().add(35.0), FloatType);
|
|
2844
|
+
return { at: Plan.at.ordinal(PHASES.get(i)), value: some(load), label: some(East.Float.printFixed(load, 0n)) };
|
|
2845
|
+
}));
|
|
2846
|
+
const base = {
|
|
2847
|
+
value: none, jobs: noJobs, allocations: noAllocs, shifts: noShifts,
|
|
2848
|
+
marks: noMarks, points: noPoints, counts: noCounts, cells: noCells,
|
|
2849
|
+
};
|
|
2850
|
+
const orders = $.const(new Map([
|
|
2851
|
+
["10-or-1188", { ...base, series: "order", label: "OR-1188", value: some("96 t"), jobs: [
|
|
2852
|
+
{ key: "prep", label: "PREP", start: "INTAKE", end: "PREP", state: ACTUAL },
|
|
2853
|
+
{ key: "build", label: "BUILD · B-214", start: "BUILD", end: "QC", state: RUNNING },
|
|
2854
|
+
{ key: "ship", label: "PACK + SHIP", start: "PACK", end: "SHIP", state: PROPOSED },
|
|
2855
|
+
] }],
|
|
2856
|
+
["10-or-1204", { ...base, series: "order", label: "OR-1204", value: some("54 t"), jobs: [
|
|
2857
|
+
{ key: "intake", label: "INTAKE", start: "INTAKE", end: "INTAKE", state: ACTUAL },
|
|
2858
|
+
{ key: "build", label: "BUILD · B-221", start: "PREP", end: "PACK", state: PROPOSED },
|
|
2859
|
+
] }],
|
|
2860
|
+
["20-bench", { ...base, series: "bench", label: "Bench 2", value: some("slots"), allocations: [
|
|
2861
|
+
{ key: "a1", phase: "PREP", state: CONFIRMED }, { key: "a2", phase: "BUILD", state: CONFIRMED },
|
|
2862
|
+
{ key: "a3", phase: "BUILD", state: PROPOSED }, { key: "a4", phase: "PACK", state: PROPOSED },
|
|
2863
|
+
] }],
|
|
2864
|
+
["30-load", { ...base, series: "load", label: "Phase load", cells }],
|
|
2865
|
+
["40-wip", { ...base, series: "wip", label: "WIP · orders", value: some("31"), points }],
|
|
2866
|
+
["50-count", { ...base, series: "count", label: "Orders in phase", counts }],
|
|
2867
|
+
["60-crew", { ...base, series: "crew", label: "Crew B", shifts: [
|
|
2868
|
+
{ key: "s1", from: "INTAKE", to: "PREP", label: "prep crew", state: CONFIRMED },
|
|
2869
|
+
{ key: "s2", from: "BUILD", to: "SHIP", label: "+ finish crew", state: PROPOSED },
|
|
2870
|
+
] }],
|
|
2871
|
+
["70-gates", { ...base, series: "gates", label: "GATES", marks: [
|
|
2872
|
+
{ key: "g1", phase: "QC", label: "HOLD", exception: true },
|
|
2873
|
+
{ key: "g2", phase: "SHIP", label: "RELEASE", exception: false },
|
|
2874
|
+
] }],
|
|
2875
|
+
]), DictType(StringType, OrderRow));
|
|
2876
|
+
const EXCEPTION = $.const(variant("exception", null), Plan.Types.EventMarkKind);
|
|
2877
|
+
const MILESTONE = $.const(variant("milestone", null), Plan.Types.EventMarkKind);
|
|
2878
|
+
const series = $.const([
|
|
2879
|
+
Plan.series.group(OrderRow, { key: "10-orders", label: "Orders", meta: "2 rs" }, [
|
|
2880
|
+
Plan.series.span(OrderRow, {
|
|
2881
|
+
key: "order", title: "Orders",
|
|
2882
|
+
match: r => r.series.equal("order"),
|
|
2883
|
+
label: r => r.label, id: true, value: r => r.value,
|
|
2884
|
+
// `j.start` / `j.end` are StringType fields — the builder wraps them to the ordinal arm.
|
|
2885
|
+
runs: r => r.jobs.map((_$, j) => Plan.run({ key: j.key, start: j.start, end: j.end, label: j.label, state: j.state })),
|
|
2886
|
+
}),
|
|
2887
|
+
]),
|
|
2888
|
+
Plan.series.buckets(OrderRow, {
|
|
2889
|
+
key: "bench", title: "Benches",
|
|
2890
|
+
match: r => r.series.equal("bench"),
|
|
2891
|
+
label: r => r.label, value: r => r.value,
|
|
2892
|
+
events: r => r.allocations.map((_$, a) => Plan.event({ key: a.key, at: a.phase, state: a.state })),
|
|
2893
|
+
}),
|
|
2894
|
+
Plan.series.heat(OrderRow, {
|
|
2895
|
+
key: "load", title: "Phase load",
|
|
2896
|
+
match: r => r.series.equal("load"),
|
|
2897
|
+
label: r => r.label,
|
|
2898
|
+
cells: r => Plan.heatCells(r.cells, { min: 0, max: 100, warnAt: 90 }),
|
|
2899
|
+
}),
|
|
2900
|
+
Plan.series.chart(OrderRow, {
|
|
2901
|
+
key: "wip", title: "WIP",
|
|
2902
|
+
match: r => r.series.equal("wip"),
|
|
2903
|
+
label: r => r.label, id: true, value: r => r.value, height: "expanded",
|
|
2904
|
+
// A string x accessor lands the columns on the ordinal arm.
|
|
2905
|
+
layers: r => [Chart.Column(r.points, { x: p => p.phase, y: p => p.n })],
|
|
2906
|
+
}),
|
|
2907
|
+
Plan.series.table(OrderRow, {
|
|
2908
|
+
key: "count", title: "Counts",
|
|
2909
|
+
match: r => r.series.equal("count"),
|
|
2910
|
+
label: r => r.label,
|
|
2911
|
+
cells: r => Plan.tableCells(r.counts),
|
|
2912
|
+
format: Format.Number({ maximumFractionDigits: 0n }),
|
|
2913
|
+
}),
|
|
2914
|
+
Plan.series.cards(OrderRow, {
|
|
2915
|
+
key: "crew", title: "Crews",
|
|
2916
|
+
match: r => r.series.equal("crew"),
|
|
2917
|
+
label: r => r.label,
|
|
2918
|
+
chips: r => r.shifts.map((_$, s) => Plan.chip({ key: s.key, from: s.from, to: s.to, label: s.label, state: s.state })),
|
|
2919
|
+
}),
|
|
2920
|
+
Plan.series.events(OrderRow, {
|
|
2921
|
+
key: "gates", title: "Gates",
|
|
2922
|
+
match: r => r.series.equal("gates"),
|
|
2923
|
+
label: r => r.label, id: true,
|
|
2924
|
+
marks: r => r.marks.map((_$, m) => Plan.mark({
|
|
2925
|
+
key: m.key, at: m.phase, label: m.label,
|
|
2926
|
+
kind: m.exception.ifElse(() => EXCEPTION, () => MILESTONE),
|
|
2927
|
+
})),
|
|
2928
|
+
}),
|
|
2929
|
+
], ArrayType(Plan.Types.Series(OrderRow)));
|
|
2930
|
+
// The declaration: the list IS the axis — one bucket per phase, `now` at BUILD.
|
|
2931
|
+
const axis = $.const(Plan.axis.ordinal({ values: PHASES, now: "BUILD" }));
|
|
2932
|
+
return (_jsx(Plan, { axis: axis, data: orders, series: series, footer: [
|
|
2933
|
+
{ text: "6 PHASES · ORDINAL AXIS" },
|
|
2934
|
+
{ text: "NOW · BUILD", end: true },
|
|
2935
|
+
] }));
|
|
2936
|
+
}),
|
|
2937
|
+
inputs: [],
|
|
2938
|
+
});
|
|
2939
|
+
//# sourceMappingURL=plan.examples.js.map
|