@jupytergis/base 0.16.0-alpha.0 → 0.16.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/lib/commands/index.js +1 -2
  2. package/lib/constants.js +4 -0
  3. package/lib/features/layers/forms/layer/index.d.ts +0 -1
  4. package/lib/features/layers/forms/layer/index.js +0 -1
  5. package/lib/features/layers/symbology/Grammar.js +101 -20
  6. package/lib/features/layers/symbology/components/MappingRow.js +32 -28
  7. package/lib/features/layers/symbology/components/ScaleEditor.js +3 -3
  8. package/lib/features/layers/symbology/components/color_ramp/ColorRampControls.js +1 -1
  9. package/lib/features/layers/symbology/components/color_stops/StopContainer.js +1 -1
  10. package/lib/features/layers/symbology/components/color_stops/StopRow.js +13 -1
  11. package/lib/features/layers/symbology/grammarToOLStyle.js +22 -2
  12. package/lib/features/layers/symbology/styleBuilder.d.ts +3 -0
  13. package/lib/features/layers/symbology/styleBuilder.js +15 -2
  14. package/lib/features/layers/symbology/symbologyDialog.js +3 -5
  15. package/lib/features/story/SpectaPanel.js +1 -1
  16. package/lib/features/story/components/ListStoryStageOverlay.d.ts +0 -1
  17. package/lib/features/story/components/ListStoryStageOverlay.js +52 -34
  18. package/lib/features/story/components/ListStoryTitleBar.d.ts +7 -0
  19. package/lib/features/story/components/ListStoryTitleBar.js +20 -0
  20. package/lib/features/story/components/ListStoryTitleBarDesktop.d.ts +2 -0
  21. package/lib/features/story/components/ListStoryTitleBarDesktop.js +55 -0
  22. package/lib/features/story/components/ListStoryTitleBarMobile.d.ts +2 -0
  23. package/lib/features/story/components/ListStoryTitleBarMobile.js +41 -0
  24. package/lib/features/story/components/SpectaMobileListModeContent.d.ts +13 -0
  25. package/lib/features/story/components/SpectaMobileListModeContent.js +36 -0
  26. package/lib/features/story/components/SpectaMobileSingleModeContent.d.ts +14 -0
  27. package/lib/features/story/components/SpectaMobileSingleModeContent.js +98 -0
  28. package/lib/features/story/components/SpectaMobileView.d.ts +7 -3
  29. package/lib/features/story/components/SpectaMobileView.js +11 -99
  30. package/lib/features/story/context/ListStoryScrollTrackContext.d.ts +4 -0
  31. package/lib/features/story/context/ListStoryScrollTrackContext.js +50 -6
  32. package/lib/features/story/hooks/useStoryMap.js +1 -16
  33. package/lib/features/story/types/types.d.ts +5 -0
  34. package/lib/features/story/utils/computeListStoryScrollState.d.ts +2 -0
  35. package/lib/features/story/utils/computeListStoryScrollState.js +34 -25
  36. package/lib/mainview/components/MainViewMapSurface.d.ts +15 -0
  37. package/lib/mainview/components/MainViewMapSurface.js +13 -0
  38. package/lib/mainview/components/MainViewOverlayLayer.d.ts +9 -0
  39. package/lib/mainview/components/MainViewOverlayLayer.js +11 -0
  40. package/lib/mainview/components/MainViewSidePanels.d.ts +17 -0
  41. package/lib/mainview/components/MainViewSidePanels.js +10 -0
  42. package/lib/mainview/components/MainViewSpectaPanel.d.ts +17 -0
  43. package/lib/mainview/components/MainViewSpectaPanel.js +8 -0
  44. package/lib/mainview/components/MainViewStoryStage.d.ts +13 -0
  45. package/lib/mainview/components/MainViewStoryStage.js +17 -0
  46. package/lib/mainview/components/PositionedFloater.d.ts +10 -0
  47. package/lib/mainview/components/PositionedFloater.js +7 -0
  48. package/lib/mainview/mainView.d.ts +3 -7
  49. package/lib/mainview/mainView.js +84 -164
  50. package/lib/shared/formbuilder/formselectors.js +1 -4
  51. package/lib/types.js +0 -1
  52. package/package.json +2 -2
  53. package/style/base.css +18 -4
  54. package/style/layerBrowser.css +3 -3
  55. package/style/storyPanel.css +192 -2
  56. package/style/symbologyDialog.css +269 -32
  57. package/lib/features/layers/forms/layer/heatmapLayerForm.d.ts +0 -3
  58. package/lib/features/layers/forms/layer/heatmapLayerForm.js +0 -96
  59. package/lib/features/layers/symbology/Heatmap.d.ts +0 -4
  60. package/lib/features/layers/symbology/Heatmap.js +0 -109
@@ -230,10 +230,9 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
230
230
  if (!layerType) {
231
231
  return false;
232
232
  }
233
- // Selection should only be one vector or heatmap layer
234
233
  return (Object.keys(selectedLayers).length === 1 &&
235
234
  !model.getSource(layerId) &&
236
- ['VectorLayer', 'HeatmapLayer'].includes(layerType));
235
+ layerType === 'VectorLayer');
237
236
  }, execute: (args) => {
238
237
  const filePath = args === null || args === void 0 ? void 0 : args.filePath;
239
238
  const current = filePath
package/lib/constants.js CHANGED
@@ -17,9 +17,13 @@ const iconObject = {
17
17
  RasterLayer: { icon: rasterIcon },
18
18
  OpenEOTileLayer: { icon: rasterIcon },
19
19
  VectorLayer: { iconClass: 'fa fa-vector-square' },
20
+ VectorTileLayer: { iconClass: 'fa fa-vector-square' },
20
21
  HillshadeLayer: { icon: moundIcon },
22
+ GeoTiffLayer: { iconClass: 'fa fa-image' },
23
+ StacLayer: { icon: rasterIcon },
21
24
  ImageLayer: { iconClass: 'fa fa-image' },
22
25
  VideoLayer: { iconClass: 'fa fa-video' },
26
+ StorySegmentLayer: { iconClass: 'fa fa-link' },
23
27
  [CommandIDs.redo]: { icon: redoIcon },
24
28
  [CommandIDs.undo]: { icon: undoIcon },
25
29
  [CommandIDs.openLayerBrowser]: { icon: bookOpenIcon },
@@ -1,4 +1,3 @@
1
- export * from './heatmapLayerForm';
2
1
  export * from './hillshadeLayerForm';
3
2
  export * from './layerform';
4
3
  export * from './vectorlayerform';
@@ -1,4 +1,3 @@
1
- export * from './heatmapLayerForm';
2
1
  export * from './hillshadeLayerForm';
3
2
  export * from './layerform';
4
3
  export * from './vectorlayerform';
@@ -5,10 +5,10 @@
5
5
  * transforms (KDE, cluster) followed by (field → scale → channels) mapping rows.
6
6
  * Multiple layers allow independent rendering pipelines on the same source.
7
7
  */
8
- import { faPlus, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
8
+ import { faArrowDown, faArrowUp, faGripVertical, faPlus, faTrash, faXmark, } from '@fortawesome/free-solid-svg-icons';
9
9
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
10
10
  import { UUID } from '@lumino/coreutils';
11
- import React, { useCallback, useEffect, useState } from 'react';
11
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
12
12
  import MappingRow, { WhenAddForm, formatPredicate, } from "./components/MappingRow";
13
13
  import { NumericInput } from "./components/NumericInput";
14
14
  import { useEffectiveSymbologyParams } from "./hooks/useEffectiveSymbologyParams";
@@ -57,12 +57,24 @@ const TransformRow = ({ transform, availableFields, onChange, onDelete, }) => {
57
57
  transform.type === 'cluster' && (React.createElement(React.Fragment, null,
58
58
  React.createElement("label", null, "radius"),
59
59
  React.createElement(NumericInput, { style: { width: 52 }, value: transform.radius, onChange: v => onChange(Object.assign(Object.assign({}, transform), { radius: v })) }))),
60
- React.createElement(Button, { type: "button", className: "jp-gis-grammar-delete-btn", onClick: onDelete, title: "Remove transform", style: { marginLeft: 'auto' } },
60
+ React.createElement(Button, { type: "button", variant: "ghost", onClick: onDelete, title: "Remove transform", style: { marginLeft: 'auto' } },
61
61
  React.createElement(FontAwesomeIcon, { icon: faTrash }))));
62
62
  };
63
- const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, featureValues, isRasterLayer = false, onChange, onDelete, }) => {
63
+ const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, featureValues, isRasterLayer = false, onChange, onDelete, onMoveUp, onMoveDown, }) => {
64
64
  var _a, _b, _c, _d;
65
65
  const [addingLayerWhen, setAddingLayerWhen] = useState(false);
66
+ const dragIndexRef = useRef(null);
67
+ const dragOverRef = useRef(null);
68
+ const [dragOverIndex, setDragOverIndex] = useState(null);
69
+ const moveRow = useCallback((fromIndex, toIndex) => {
70
+ if (fromIndex === toIndex) {
71
+ return;
72
+ }
73
+ const next = [...layer.rows];
74
+ const [moved] = next.splice(fromIndex, 1);
75
+ next.splice(toIndex, 0, moved);
76
+ onChange(Object.assign(Object.assign({}, layer), { rows: next }));
77
+ }, [layer, onChange]);
66
78
  const addLayerPredicate = useCallback((pred) => {
67
79
  var _a;
68
80
  onChange(Object.assign(Object.assign({}, layer), { when: [...((_a = layer.when) !== null && _a !== void 0 ? _a : []), pred] }));
@@ -73,16 +85,17 @@ const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, feature
73
85
  const next = ((_a = layer.when) !== null && _a !== void 0 ? _a : []).filter((_, i) => i !== index);
74
86
  onChange(Object.assign(Object.assign({}, layer), { when: next.length > 0 ? next : undefined }));
75
87
  }, [layer, onChange]);
76
- const updateTransform = useCallback((index, t) => {
77
- const next = [...layer.transforms];
78
- next[index] = t;
79
- onChange(Object.assign(Object.assign({}, layer), { transforms: next }));
88
+ const addTransform = useCallback(() => {
89
+ if (layer.transforms.length > 0) {
90
+ return;
91
+ }
92
+ onChange(Object.assign(Object.assign({}, layer), { transforms: [defaultTransform('kde')] }));
80
93
  }, [layer, onChange]);
81
- const removeTransform = useCallback((index) => {
82
- onChange(Object.assign(Object.assign({}, layer), { transforms: layer.transforms.filter((_, i) => i !== index) }));
94
+ const updateTransform = useCallback((t) => {
95
+ onChange(Object.assign(Object.assign({}, layer), { transforms: [t] }));
83
96
  }, [layer, onChange]);
84
- const addTransform = useCallback(() => {
85
- onChange(Object.assign(Object.assign({}, layer), { transforms: [...layer.transforms, defaultTransform('kde')] }));
97
+ const removeTransform = useCallback(() => {
98
+ onChange(Object.assign(Object.assign({}, layer), { transforms: [] }));
86
99
  }, [layer, onChange]);
87
100
  const updateRow = useCallback((index, row) => {
88
101
  const next = [...layer.rows];
@@ -115,10 +128,14 @@ const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, feature
115
128
  React.createElement("span", { className: "jp-gis-grammar-layer-label" },
116
129
  "Layer ",
117
130
  layerIndex + 1),
118
- React.createElement(Button, { type: "button", variant: "outline", onClick: addTransform, title: "Add transform" },
131
+ layer.transforms.length === 0 && (React.createElement(Button, { type: "button", variant: "ghost", onClick: addTransform, title: "Add transform" },
119
132
  React.createElement(FontAwesomeIcon, { "data-icon": "inline-start", icon: faPlus }),
120
- "Transform"),
121
- totalLayers > 1 && (React.createElement(Button, { type: "button", variant: "outline", style: { height: 32, width: 32 }, onClick: onDelete, title: "Remove layer" },
133
+ "Transform")),
134
+ totalLayers > 1 && onMoveUp && (React.createElement(Button, { type: "button", variant: "ghost", style: { height: 32, width: 32 }, onClick: onMoveUp, title: "Move layer up" },
135
+ React.createElement(FontAwesomeIcon, { icon: faArrowUp }))),
136
+ totalLayers > 1 && onMoveDown && (React.createElement(Button, { type: "button", variant: "ghost", style: { height: 32, width: 32 }, onClick: onMoveDown, title: "Move layer down" },
137
+ React.createElement(FontAwesomeIcon, { icon: faArrowDown }))),
138
+ totalLayers > 1 && (React.createElement(Button, { type: "button", variant: "ghost", style: { height: 32, width: 32 }, onClick: onDelete, title: "Remove layer" },
122
139
  React.createElement(FontAwesomeIcon, { icon: faTrash })))),
123
140
  React.createElement("div", { className: "jp-gis-grammar-when-row" },
124
141
  React.createElement("span", { className: "jp-gis-grammar-when-label" }, "when"),
@@ -132,10 +149,63 @@ const LayerSection = ({ layer, layerIndex, totalLayers, availableFields, feature
132
149
  React.createElement(FontAwesomeIcon, { icon: faXmark }))))),
133
150
  addingLayerWhen ? (React.createElement(WhenAddForm, { availableFields: availableFields, onAdd: addLayerPredicate, onCancel: () => setAddingLayerWhen(false) })) : (React.createElement(Button, { type: "button", className: "jp-gis-grammar-when-add-btn", onClick: () => setAddingLayerWhen(true) },
134
151
  React.createElement(FontAwesomeIcon, { icon: faPlus })))),
135
- layer.transforms.map((t, i) => (React.createElement(TransformRow, { key: i, transform: t, availableFields: availableFields, onChange: updated => updateTransform(i, updated), onDelete: () => removeTransform(i) }))),
136
- layer.rows.map((row, i) => (React.createElement(MappingRow, { key: row.id, row: row, availableFields: encodingFields, featureValues: featureValues, isRaster: isRaster, onChange: updated => updateRow(i, updated), onDelete: () => removeRow(i) }))),
152
+ layer.transforms[0] && (React.createElement(TransformRow, { transform: layer.transforms[0], availableFields: availableFields, onChange: updateTransform, onDelete: removeTransform })),
153
+ React.createElement("div", { className: "jp-gis-grammar-rules-container", onDragOver: e => {
154
+ e.preventDefault();
155
+ // Find which wrapper the cursor is closest to
156
+ const wrappers = Array.from(e.currentTarget.querySelectorAll(':scope > .jp-gis-grammar-drag-wrapper'));
157
+ let idx = layer.rows.length;
158
+ for (let j = 0; j < wrappers.length; j++) {
159
+ const rect = wrappers[j].getBoundingClientRect();
160
+ const midY = rect.top + rect.height / 2;
161
+ if (e.clientY < midY) {
162
+ idx = j;
163
+ break;
164
+ }
165
+ }
166
+ dragOverRef.current = idx;
167
+ setDragOverIndex(idx);
168
+ }, onDrop: () => {
169
+ const over = dragOverRef.current;
170
+ if (dragIndexRef.current !== null && over !== null) {
171
+ const to = over > dragIndexRef.current ? over - 1 : over;
172
+ moveRow(dragIndexRef.current, to);
173
+ }
174
+ dragIndexRef.current = null;
175
+ dragOverRef.current = null;
176
+ setDragOverIndex(null);
177
+ }, onDragEnd: () => {
178
+ dragIndexRef.current = null;
179
+ dragOverRef.current = null;
180
+ setDragOverIndex(null);
181
+ } }, layer.rows.map((row, i) => (React.createElement("div", { key: row.id, className: "jp-gis-grammar-drag-wrapper", style: {
182
+ borderTop: dragOverIndex === i && dragIndexRef.current !== i
183
+ ? '2px solid var(--jp-brand-color1)'
184
+ : '2px solid transparent',
185
+ borderBottom: dragOverIndex === layer.rows.length &&
186
+ i === layer.rows.length - 1 &&
187
+ dragIndexRef.current !== i
188
+ ? '2px solid var(--jp-brand-color1)'
189
+ : '2px solid transparent',
190
+ } },
191
+ layer.rows.length > 1 && (React.createElement("div", { className: "jp-gis-grammar-reorder-bar" },
192
+ React.createElement(Button, { type: "button", disabled: i === 0, onClick: () => moveRow(i, i - 1), title: "Move up" },
193
+ React.createElement(FontAwesomeIcon, { icon: faArrowUp })),
194
+ React.createElement("div", { className: "jp-gis-grammar-drag-handle", draggable: true, onDragStart: e => {
195
+ dragIndexRef.current = i;
196
+ const wrapper = e.currentTarget.closest('.jp-gis-grammar-drag-wrapper');
197
+ if (wrapper) {
198
+ e.dataTransfer.setDragImage(wrapper, 0, 0);
199
+ }
200
+ }, title: "Drag to reorder" },
201
+ React.createElement(FontAwesomeIcon, { icon: faGripVertical })),
202
+ React.createElement(Button, { type: "button", disabled: i === layer.rows.length - 1, onClick: () => moveRow(i, i + 1), title: "Move down" },
203
+ React.createElement(FontAwesomeIcon, { icon: faArrowDown })))),
204
+ React.createElement(MappingRow, { row: row, availableFields: encodingFields, featureValues: featureValues, isRaster: isRaster, onChange: updated => updateRow(i, updated), onDelete: () => removeRow(i) }))))),
137
205
  React.createElement("div", { className: "jp-gis-symbology-button-container" },
138
- React.createElement(Button, { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", style: { margin: '0 0 0.5rem 1rem' }, onClick: addRow }, "Add Mapping"))));
206
+ React.createElement(Button, { variant: "ghost", style: { margin: '0 0 0.5rem 1rem' }, onClick: addRow },
207
+ React.createElement(FontAwesomeIcon, { icon: faPlus }),
208
+ "Add Mapping"))));
139
209
  };
140
210
  // ---------------------------------------------------------------------------
141
211
  // Grammar panel
@@ -224,12 +294,23 @@ const Grammar = ({ model, okSignalPromise, layerId, isStorySegmentOverride, segm
224
294
  { id: UUID.uuid4(), transforms: [], rows: [] },
225
295
  ]);
226
296
  };
297
+ const moveLayer = useCallback((from, to) => {
298
+ if (from === to) {
299
+ return;
300
+ }
301
+ setLayers(prev => {
302
+ const next = [...prev];
303
+ const [moved] = next.splice(from, 1);
304
+ next.splice(to, 0, moved);
305
+ return next;
306
+ });
307
+ }, [setLayers]);
227
308
  const availableFields = isRasterLayer
228
309
  ? bandRows.map(b => `$band-${b.band}`)
229
310
  : Object.keys(selectableAttributesAndValues);
230
311
  return (React.createElement("div", { className: "jp-gis-layer-symbology-container" },
231
- layers.map((uiLayer, i) => (React.createElement(LayerSection, { key: uiLayer.id, layer: uiLayer, layerIndex: i, totalLayers: layers.length, availableFields: availableFields, featureValues: selectableAttributesAndValues, isRasterLayer: isRasterLayer, onChange: updated => setLayers(prev => prev.map((l, j) => (j === i ? updated : l))), onDelete: () => setLayers(prev => prev.filter((_, j) => j !== i)) }))),
312
+ layers.map((uiLayer, i) => (React.createElement(LayerSection, { key: uiLayer.id, layer: uiLayer, layerIndex: i, totalLayers: layers.length, availableFields: availableFields, featureValues: selectableAttributesAndValues, isRasterLayer: isRasterLayer, onChange: updated => setLayers(prev => prev.map((l, j) => (j === i ? updated : l))), onDelete: () => setLayers(prev => prev.filter((_, j) => j !== i)), onMoveUp: i > 0 ? () => moveLayer(i, i - 1) : undefined, onMoveDown: i < layers.length - 1 ? () => moveLayer(i, i + 1) : undefined }))),
232
313
  React.createElement("div", { className: "jp-gis-symbology-button-container" },
233
- React.createElement(Button, { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: addLayer }, "Add Layer"))));
314
+ React.createElement(Button, { className: "jp-gis-grammar-action-btn", onClick: addLayer }, "Add Layer"))));
234
315
  };
235
316
  export default Grammar;
@@ -96,7 +96,7 @@ function defaultScaleForScheme(scheme, _currentChannels) {
96
96
  return {
97
97
  scheme: 'categorical',
98
98
  params: {
99
- colorRamp: 'viridis',
99
+ colorRamp: 'schemeCategory10',
100
100
  reverse: false,
101
101
  fallback: [0, 0, 0, 0],
102
102
  },
@@ -455,36 +455,40 @@ const MappingRow = ({ row, availableFields, featureValues, isRaster = false, onC
455
455
  }, [row, onChange]);
456
456
  const compat = compatibleChannels(row.scale, isRaster);
457
457
  const availableToAdd = compat.filter(ch => !row.channels.includes(ch));
458
- const previewRowSpan = row.channels.length + (availableToAdd.length > 0 ? 1 : 0);
459
458
  return (React.createElement("div", { className: "jp-gis-grammar-rule" },
460
459
  React.createElement("div", { className: "jp-gis-grammar-rule-grid" },
461
- React.createElement(FieldSelector, { fieldCount: fieldCountForScale(row.scale.scheme), fields: (_a = row.fields) !== null && _a !== void 0 ? _a : [], availableFields: availableFields, onFieldChange: handleFieldChange, onAddField: addField }),
462
- React.createElement("div", { style: { gridRow: 1, gridColumn: 2 } },
463
- React.createElement(NativeSelect, { value: row.scale.scheme, onChange: e => handleSchemeChange(e.target.value) }, SCHEME_OPTIONS.filter(({ disabled }) => !disabled).map(({ value, label }) => (React.createElement(NativeSelectOption, { key: value, value: value }, label))))),
464
- React.createElement("button", { type: "button", className: "jp-gis-grammar-preview-btn", style: { gridRow: `1 / span ${previewRowSpan}`, gridColumn: 3 }, onClick: () => setExpanded(v => !v), title: expanded ? 'Collapse editor' : 'Edit scale' },
465
- React.createElement(ScalePreview, { scale: row.scale })),
466
- row.channels.map((ch, i) => (React.createElement(React.Fragment, { key: `${ch}-${i}` },
467
- React.createElement("span", { className: "jp-gis-grammar-arrow", style: { gridRow: i + 1, gridColumn: 4 } }, "\u2192"),
468
- React.createElement("div", { className: "jp-gis-grammar-channel-select", style: { gridRow: i + 1, gridColumn: 5 } },
469
- React.createElement(NativeSelect, { value: ch, onChange: e => handleChannelChange(i, e.target.value) }, compat.map(c => {
470
- var _a;
471
- return (React.createElement(NativeSelectOption, { key: c, value: c }, (_a = CHANNEL_LABELS[c]) !== null && _a !== void 0 ? _a : c));
472
- }))),
473
- React.createElement(Button, { type: "button", variant: "outline", size: "icon-md", className: "jp-mod-styled", style: { gridRow: i + 1, gridColumn: 6 }, onClick: () => removeChannel(ch), title: row.channels.length === 1 ? 'Remove mapping' : 'Remove channel' },
474
- React.createElement(FontAwesomeIcon, { icon: faTrash }))))),
475
- availableToAdd.length > 0 && (React.createElement(React.Fragment, null,
476
- React.createElement("span", { className: "jp-gis-grammar-arrow", style: { gridRow: row.channels.length + 1, gridColumn: 4 } }, "+"),
477
- React.createElement("div", { className: "jp-gis-grammar-channel-select", style: { gridRow: row.channels.length + 1, gridColumn: 5 } },
478
- React.createElement(NativeSelect, { value: "", onChange: e => {
479
- if (e.target.value) {
480
- addChannel(e.target.value);
481
- }
482
- } },
483
- React.createElement(NativeSelectOption, { value: "" }, "(add channel)"),
484
- availableToAdd.map(ch => {
460
+ React.createElement("div", { className: "jp-gis-grammar-section jp-gis-grammar-input-section" },
461
+ React.createElement(FieldSelector, { fieldCount: fieldCountForScale(row.scale.scheme), fields: (_a = row.fields) !== null && _a !== void 0 ? _a : [], availableFields: availableFields, onFieldChange: handleFieldChange, onAddField: addField })),
462
+ React.createElement("span", { className: "jp-gis-grammar-arrow jp-gis-grammar-arrow-input" }, "\u2192"),
463
+ React.createElement("div", { className: "jp-gis-grammar-section jp-gis-grammar-scale-section" },
464
+ React.createElement(NativeSelect, { value: row.scale.scheme, onChange: e => handleSchemeChange(e.target.value) }, SCHEME_OPTIONS.filter(({ disabled }) => !disabled).map(({ value, label }) => (React.createElement(NativeSelectOption, { key: value, value: value }, label)))),
465
+ React.createElement("button", { type: "button", className: "jp-gis-grammar-preview-btn", onClick: () => setExpanded(v => !v), title: expanded ? 'Collapse editor' : 'Edit scale' },
466
+ React.createElement(ScalePreview, { scale: row.scale }),
467
+ React.createElement("span", { className: "jp-gis-grammar-preview-chevron", "aria-hidden": "true" }, expanded ? '▾' : '▸'))),
468
+ React.createElement("span", { className: "jp-gis-grammar-arrow jp-gis-grammar-arrow-output" }, "\u2192"),
469
+ React.createElement("div", { className: "jp-gis-grammar-section jp-gis-grammar-output-section" },
470
+ row.channels.map((ch, i) => (React.createElement("div", { key: `${ch}-${i}`, className: "jp-gis-grammar-channel-row" },
471
+ React.createElement("div", { className: "jp-gis-grammar-channel-select" },
472
+ React.createElement(NativeSelect, { value: ch, onChange: e => handleChannelChange(i, e.target.value) }, compat.map(c => {
485
473
  var _a;
486
- return (React.createElement(NativeSelectOption, { key: ch, value: ch }, (_a = CHANNEL_LABELS[ch]) !== null && _a !== void 0 ? _a : ch));
487
- })))))),
474
+ return (React.createElement(NativeSelectOption, { key: c, value: c }, (_a = CHANNEL_LABELS[c]) !== null && _a !== void 0 ? _a : c));
475
+ }))),
476
+ React.createElement(Button, { type: "button", variant: "ghost", size: "icon-md", className: "jp-mod-styled", onClick: () => removeChannel(ch), title: row.channels.length === 1
477
+ ? 'Remove mapping'
478
+ : 'Remove channel' },
479
+ React.createElement(FontAwesomeIcon, { icon: faTrash }))))),
480
+ availableToAdd.length > 0 && (React.createElement("div", { className: "jp-gis-grammar-channel-row" },
481
+ React.createElement("div", { className: "jp-gis-grammar-channel-select" },
482
+ React.createElement(NativeSelect, { value: "", onChange: e => {
483
+ if (e.target.value) {
484
+ addChannel(e.target.value);
485
+ }
486
+ } },
487
+ React.createElement(NativeSelectOption, { value: "" }, "(add channel)"),
488
+ availableToAdd.map(ch => {
489
+ var _a;
490
+ return (React.createElement(NativeSelectOption, { key: ch, value: ch }, (_a = CHANNEL_LABELS[ch]) !== null && _a !== void 0 ? _a : ch));
491
+ }))))))),
488
492
  React.createElement("div", { className: "jp-gis-grammar-when-row" },
489
493
  React.createElement("span", { className: "jp-gis-grammar-when-label" }, "when"),
490
494
  ((_c = (_b = row.when) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 1 && (React.createElement(Button, { type: "button", className: "jp-gis-grammar-when-op", onClick: () => {
@@ -112,7 +112,7 @@ export const ColorRampEditor = ({ scale, field, featureValues, onChange, }) => {
112
112
  React.createElement("div", { className: "jp-gis-symbology-row" },
113
113
  React.createElement("label", null, "Fallback"),
114
114
  React.createElement(RgbaColorPicker, { color: params.fallback, onChange: color => update({ fallback: color }) })),
115
- React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", disabled: !field, onClick: classify }, "Classify"),
115
+ React.createElement("button", { className: "jp-gis-grammar-action-btn", disabled: !field, onClick: classify }, "Classify"),
116
116
  stopRows.length > 0 && (React.createElement(StopContainer, { selectedMethod: "color", stopRows: stopRows, setStopRows: handleStopRowsChange }))));
117
117
  };
118
118
  export const CategoricalEditor = ({ scale, field, featureValues, onChange, }) => {
@@ -163,7 +163,7 @@ export const CategoricalEditor = ({ scale, field, featureValues, onChange, }) =>
163
163
  React.createElement("div", { className: "jp-gis-symbology-row" },
164
164
  React.createElement("label", null, "Fallback"),
165
165
  React.createElement(RgbaColorPicker, { color: params.fallback, onChange: color => update({ fallback: color }) })),
166
- React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", disabled: !field, onClick: classify }, "Classify"),
166
+ React.createElement("button", { className: "jp-gis-grammar-action-btn", disabled: !field, onClick: classify }, "Classify"),
167
167
  stopRows.length > 0 && (React.createElement(StopContainer, { selectedMethod: "color", stopRows: stopRows, setStopRows: handleStopRowsChange }))));
168
168
  };
169
169
  export const ScalarEditor = ({ scale, field, featureValues, onChange, }) => {
@@ -216,6 +216,6 @@ export const ScalarEditor = ({ scale, field, featureValues, onChange, }) => {
216
216
  React.createElement("div", { className: "jp-gis-symbology-row" },
217
217
  React.createElement("label", null, "Fallback"),
218
218
  React.createElement(NumericInput, { className: "jp-mod-styled", value: params.fallback, onChange: v => update({ fallback: v }) })),
219
- React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: classify }, "Set stops"),
219
+ React.createElement("button", { className: "jp-gis-grammar-action-btn", onClick: classify }, "Set stops"),
220
220
  stopRows.length > 0 && (React.createElement(StopContainer, { selectedMethod: "radius", stopRows: stopRows, setStopRows: handleStopRowsChange }))));
221
221
  };
@@ -80,6 +80,6 @@ const ColorRampControls = ({ layerParams, modeOptions, classifyFunc, showModeRow
80
80
  React.createElement(ColorRampSelector, { selectedRamp: selectedRamp, setSelected: handleRampChange, reverse: reverseRamp, setReverse: setReverseRamp }))),
81
81
  showModeRow && (React.createElement(ModeSelectRow, { modeOptions: modeOptions, numberOfShades: numberOfShades, setNumberOfShades: setNumberOfShades, selectedMode: selectedMode, setSelectedMode: setSelectedMode })),
82
82
  warning && (React.createElement("div", { className: "jp-gis-warning", style: { color: 'orange', marginTop: 4 } }, warning)),
83
- isLoading ? (React.createElement(LoadingIcon, null)) : (React.createElement(Button, { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", disabled: !isValidNumberOfShades(numberOfShades) || !selectedMode || !!warning, onClick: () => classifyFunc(selectedMode, numberOfShades, selectedRamp, reverseRamp, setIsLoading) }, "Classify"))));
83
+ isLoading ? (React.createElement(LoadingIcon, null)) : (React.createElement(Button, { className: "jp-gis-grammar-action-btn", disabled: !isValidNumberOfShades(numberOfShades) || !selectedMode || !!warning, onClick: () => classifyFunc(selectedMode, numberOfShades, selectedRamp, reverseRamp, setIsLoading) }, "Classify"))));
84
84
  };
85
85
  export default ColorRampControls;
@@ -25,6 +25,6 @@ const StopContainer = ({ selectedMethod, stopRows, setStopRows, }) => {
25
25
  React.createElement("span", null, "Output Value")),
26
26
  stopRows.map((stop, index) => (React.createElement(StopRow, { key: stop.id, index: index, dataValue: stop.stop, symbologyValue: stop.output, stopRows: stopRows, setStopRows: setStopRows, deleteRow: () => deleteStopRow(index), useNumber: selectedMethod === 'radius' ? true : false })))),
27
27
  React.createElement("div", { className: "jp-gis-symbology-button-container" },
28
- React.createElement(Button, { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: addStopRow }, "Add Stop"))));
28
+ React.createElement(Button, { className: "jp-gis-grammar-action-btn", onClick: addStopRow }, "Add Stop"))));
29
29
  };
30
30
  export default StopContainer;
@@ -4,6 +4,8 @@ import { Button } from '@jupyterlab/ui-components';
4
4
  import React, { useEffect, useRef } from 'react';
5
5
  import { colorToRgba, } from "../../colorRampUtils";
6
6
  import RgbaColorPicker from "../color_ramp/RgbaColorPicker";
7
+ import { STOP_NULL, STOP_UNDEFINED, } from "../../styleBuilder";
8
+ const SENTINELS = new Set([STOP_NULL, STOP_UNDEFINED]);
7
9
  const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, deleteRow, useNumber, }) => {
8
10
  const inputRef = useRef(null);
9
11
  useEffect(() => {
@@ -21,6 +23,14 @@ const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, dele
21
23
  const handleBlur = () => {
22
24
  const newRows = [...stopRows];
23
25
  newRows.sort((a, b) => {
26
+ const aIsSentinel = SENTINELS.has(a.stop);
27
+ const bIsSentinel = SENTINELS.has(b.stop);
28
+ if (aIsSentinel && !bIsSentinel) {
29
+ return 1;
30
+ }
31
+ if (!aIsSentinel && bIsSentinel) {
32
+ return -1;
33
+ }
24
34
  if (a.stop < b.stop) {
25
35
  return -1;
26
36
  }
@@ -42,7 +52,9 @@ const StopRow = ({ index, dataValue, symbologyValue, stopRows, setStopRows, dele
42
52
  setStopRows(newRows);
43
53
  };
44
54
  return (React.createElement("div", { className: "jp-gis-color-row" },
45
- React.createElement("input", { id: `jp-gis-color-value-${index}`, type: useNumber ? 'number' : 'text', value: dataValue, onChange: handleStopChange, onBlur: handleBlur, className: "jp-mod-styled jp-gis-color-row-value-input" }),
55
+ React.createElement("input", { id: `jp-gis-color-value-${index}`, type: useNumber ? 'number' : 'text', value: dataValue, onChange: handleStopChange, onBlur: handleBlur, className: "jp-mod-styled jp-gis-color-row-value-input", style: SENTINELS.has(dataValue)
56
+ ? { fontStyle: 'italic' }
57
+ : undefined }),
46
58
  useNumber ? (React.createElement("input", { type: "number", ref: inputRef, value: symbologyValue, onChange: handleOutputChange, className: "jp-mod-styled jp-gis-color-row-output-input" })) : (React.createElement(RgbaColorPicker, { color: colorToRgba(symbologyValue), onChange: handleColorOutputChange })),
47
59
  React.createElement(Button, { id: `jp-gis-remove-color-${index}`, className: "jp-Button jp-gis-filter-icon" },
48
60
  React.createElement(FontAwesomeIcon, { icon: faTrash, onClick: deleteRow }))));
@@ -11,7 +11,7 @@
11
11
  * 3. Assemble sub-channels (fill-red/green/blue/alpha) into a composite
12
12
  * fill-color ['array', r, g, b, a] expression.
13
13
  */
14
- import { computeCategorizedColorStops, computeGraduatedColorStops, } from './styleBuilder';
14
+ import { computeCategorizedColorStops, computeGraduatedColorStops, STOP_NULL, STOP_UNDEFINED, } from './styleBuilder';
15
15
  // '$density' is the pseudo-field produced by a kde transform (KDE density raster).
16
16
  // Encoding rules referencing it are compiled only when a kde transform is present;
17
17
  // the actual OL HeatmapLayer instantiation happens outside this compiler.
@@ -433,7 +433,27 @@ function compileCategorical(field, scale, featureValues) {
433
433
  }
434
434
  const caseExpr = ['case'];
435
435
  for (const stop of stops) {
436
- caseExpr.push(['==', fieldExpr(field), stop.value], stop.color);
436
+ let condition;
437
+ if (stop.value === STOP_UNDEFINED) {
438
+ // Property missing entirely
439
+ condition = ['!', ['has', field]];
440
+ }
441
+ else if (stop.value === STOP_NULL) {
442
+ // Property exists but value is null
443
+ condition = [
444
+ 'all',
445
+ ['has', field],
446
+ ['==', ['coalesce', fieldExpr(field), '__jgis_ns__'], '__jgis_ns__'],
447
+ ];
448
+ }
449
+ else {
450
+ condition = [
451
+ '==',
452
+ fieldExpr(field),
453
+ stop.value,
454
+ ];
455
+ }
456
+ caseExpr.push(condition, stop.color);
437
457
  }
438
458
  caseExpr.push(scale.params.fallback);
439
459
  return caseExpr;
@@ -6,6 +6,9 @@ export type SymbologyState = NonNullable<IVectorLayer['symbologyState']>;
6
6
  export type GeometryType = 'fill' | 'circle' | 'line';
7
7
  /** Default OL flat style used when no Grammar rules produce output. */
8
8
  export declare const DEFAULT_FLAT_STYLE: FlatStyle;
9
+ /** Sentinel stop values for missing data. */
10
+ export declare const STOP_NULL = "__null__";
11
+ export declare const STOP_UNDEFINED = "__undefined__";
9
12
  /** A computed stop: value → RGBA color. */
10
13
  export interface IComputedStop {
11
14
  value: number | string | boolean;
@@ -12,6 +12,9 @@ export const DEFAULT_FLAT_STYLE = {
12
12
  'circle-stroke-width': 1.25,
13
13
  'circle-stroke-color': '#3399CC',
14
14
  };
15
+ /** Sentinel stop values for missing data. */
16
+ export const STOP_NULL = '__null__';
17
+ export const STOP_UNDEFINED = '__undefined__';
15
18
  // Stop computation helpers — used by the Grammar compiler
16
19
  /**
17
20
  * Compute color stops for Graduated symbology from the config + feature values.
@@ -80,8 +83,18 @@ export function computeCategorizedColorStops(state, featureValues) {
80
83
  const rampName = (_a = state.colorRamp) !== null && _a !== void 0 ? _a : 'viridis';
81
84
  const reverse = (_b = state.reverseRamp) !== null && _b !== void 0 ? _b : false;
82
85
  const uniqueValues = [
83
- ...new Set(featureValues.filter(v => v !== undefined && v !== null)),
84
- ].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
86
+ ...new Set(featureValues.map(v => v === null ? STOP_NULL : v === undefined ? STOP_UNDEFINED : v)),
87
+ ].sort((a, b) => {
88
+ const aIsSentinel = a === STOP_NULL || a === STOP_UNDEFINED;
89
+ const bIsSentinel = b === STOP_NULL || b === STOP_UNDEFINED;
90
+ if (aIsSentinel && !bIsSentinel) {
91
+ return 1;
92
+ }
93
+ if (!aIsSentinel && bIsSentinel) {
94
+ return -1;
95
+ }
96
+ return a < b ? -1 : a > b ? 1 : 0;
97
+ });
85
98
  if (uniqueValues.length === 0) {
86
99
  return [];
87
100
  }
@@ -3,7 +3,6 @@ import { PromiseDelegate } from '@lumino/coreutils';
3
3
  import { Signal } from '@lumino/signaling';
4
4
  import React, { useEffect, useState } from 'react';
5
5
  import Grammar from './Grammar';
6
- import Heatmap from './Heatmap';
7
6
  const SymbologyDialog = ({ model, okSignalPromise, isStorySegmentOverride, segmentId, }) => {
8
7
  const [selectedLayer, setSelectedLayer] = useState(null);
9
8
  const [componentToRender, setComponentToRender] = useState(null);
@@ -34,9 +33,6 @@ const SymbologyDialog = ({ model, okSignalPromise, isStorySegmentOverride, segme
34
33
  }
35
34
  // TODO GeoTiffLayers can also be used for other layers, need a better way to determine source + layer combo
36
35
  switch (layer.type) {
37
- case 'HeatmapLayer':
38
- LayerSymbology = (React.createElement(Heatmap, { model: model, okSignalPromise: okSignalPromise, layerId: selectedLayer, isStorySegmentOverride: isStorySegmentOverride, segmentId: segmentId }));
39
- break;
40
36
  case 'VectorLayer':
41
37
  case 'VectorTileLayer':
42
38
  case 'GeoTiffLayer':
@@ -53,7 +49,9 @@ export class SymbologyWidget extends Dialog {
53
49
  constructor(options) {
54
50
  const okSignalPromise = new PromiseDelegate();
55
51
  const body = (React.createElement(SymbologyDialog, { model: options.model, okSignalPromise: okSignalPromise, isStorySegmentOverride: options.isStorySegmentOverride, segmentId: options.segmentId }));
56
- super({ title: 'Symbology', body });
52
+ const layerId = Object.keys(options.model.localState.selected.value)[0];
53
+ const layerName = options.model.getLayer(layerId).name;
54
+ super({ title: `Symbology — ${layerName}`, body });
57
55
  this.id = 'jupytergis::symbologyWidget';
58
56
  this.okSignal = new Signal(this);
59
57
  okSignalPromise.resolve(this.okSignal);
@@ -28,7 +28,7 @@ export function SpectaPanel({ model, isSpecta, isMobile, onSegmentTransitionEnd,
28
28
  return () => el.removeEventListener('animationend', handleAnimationEnd);
29
29
  }, [currentIndex, onSegmentTransitionEnd]);
30
30
  if (isMobile) {
31
- return (React.createElement(SpectaMobileView, { segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext }));
31
+ return (React.createElement(SpectaMobileView, { model: model, segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, setIndex: setIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext, onSegmentTransitionChange: onSegmentTransitionChange }));
32
32
  }
33
33
  return (React.createElement(SpectaDesktopView, { model: model, isSpecta: isSpecta, containerRef: containerRef, storyViewerPanelRef: storyViewerPanelRef, segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext, showGradient: showGradient, setIndex: setIndex, onSegmentTransitionChange: onSegmentTransitionChange }));
34
34
  }
@@ -6,7 +6,6 @@ interface IListStoryStageOverlayProps {
6
6
  }
7
7
  /**
8
8
  * List-story stage overlay: map + markdown segments on the map stage.
9
- * The story column scrolls only the virtual track; this is the visible UI.
10
9
  */
11
10
  export declare function ListStoryStageOverlay({ model, segmentTransition, }: IListStoryStageOverlayProps): JSX.Element | null;
12
11
  export {};