@perspective-dev/viewer 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +3 -0
  26. package/dist/esm/perspective-viewer.inline.js +9 -9
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/plugin.d.ts +10 -0
  31. package/dist/esm/providers.d.ts +79 -0
  32. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  33. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  35. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  36. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  38. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  39. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  40. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  41. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  42. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  43. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  44. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  45. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  46. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +42 -2
  47. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  48. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  49. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  50. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  51. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  52. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  53. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  54. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  55. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  56. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  57. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  59. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  60. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  61. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  62. package/dist/esm/ts-rs/ViewerConfig.d.ts +42 -2
  63. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  64. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
  66. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +20 -9
  68. package/dist/wasm/perspective-viewer.d.ts +157 -41
  69. package/dist/wasm/perspective-viewer.js +452 -151
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +22 -17
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +8 -0
  75. package/src/css/column-settings-panel.css +9 -5
  76. package/src/css/column-style.css +209 -31
  77. package/src/css/config-selector.css +38 -3
  78. package/src/css/dom/select.css +2 -1
  79. package/src/css/form/code-editor.css +6 -7
  80. package/src/css/form/mirrored-textarea.css +81 -0
  81. package/src/css/viewer.css +328 -0
  82. package/src/rust/agent/client/mod.rs +19 -0
  83. package/src/rust/agent/client/protocol.rs +320 -0
  84. package/src/rust/agent/client/transport.rs +287 -0
  85. package/src/rust/agent/client/turn.rs +174 -0
  86. package/src/rust/agent/config.rs +159 -0
  87. package/src/rust/agent/docs.rs +293 -0
  88. package/src/rust/agent/markdown/block.rs +509 -0
  89. package/src/rust/agent/markdown/entity.rs +52 -0
  90. package/src/rust/agent/markdown/events.rs +71 -0
  91. package/src/rust/agent/markdown/inline.rs +497 -0
  92. package/src/rust/agent/markdown/mod.rs +466 -0
  93. package/src/rust/agent/mod.rs +23 -0
  94. package/src/rust/agent/preamble.md +16 -0
  95. package/src/rust/agent/runtime.rs +201 -0
  96. package/src/rust/agent/slot.rs +236 -0
  97. package/src/rust/agent/tools.rs +1141 -0
  98. package/src/rust/components/chat_panel.rs +289 -0
  99. package/src/rust/components/column_selector/active_column.rs +2 -2
  100. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  101. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  102. package/src/rust/components/column_selector/config_selector.rs +84 -7
  103. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  104. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  105. package/src/rust/components/column_selector.rs +55 -4
  106. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  107. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  108. package/src/rust/components/column_settings_sidebar.rs +88 -69
  109. package/src/rust/components/containers/dragdrop_list.rs +37 -0
  110. package/src/rust/components/editable_header.rs +85 -144
  111. package/src/rust/components/expression_editor.rs +9 -16
  112. package/src/rust/components/form/code_editor.rs +29 -28
  113. package/src/rust/components/form/debug.rs +9 -5
  114. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  115. package/src/rust/components/form/mod.rs +4 -1
  116. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  117. package/src/rust/components/form/named_value_picker.rs +86 -0
  118. package/src/rust/components/form/palette_selector.rs +440 -0
  119. package/src/rust/components/main_panel/msg.rs +2 -2
  120. package/src/rust/components/main_panel/reconcile.rs +4 -4
  121. package/src/rust/components/mod.rs +2 -0
  122. package/src/rust/components/plugin_tab.rs +4 -28
  123. package/src/rust/components/settings_panel.rs +105 -22
  124. package/src/rust/components/viewer/msg.rs +46 -7
  125. package/src/rust/components/viewer/panels.rs +17 -20
  126. package/src/rust/components/viewer/render.rs +10 -3
  127. package/src/rust/components/viewer/settings.rs +163 -23
  128. package/src/rust/components/viewer/snapshots.rs +46 -21
  129. package/src/rust/components/viewer/wiring.rs +14 -25
  130. package/src/rust/components/viewer.rs +12 -8
  131. package/src/rust/components/window_editor.rs +75 -103
  132. package/src/rust/config/column_config_schema.rs +100 -41
  133. package/src/rust/config/css_scale.rs +882 -0
  134. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  135. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  136. package/src/rust/config/mod.rs +2 -0
  137. package/src/rust/config/number_string_format.rs +33 -0
  138. package/src/rust/config/options.rs +28 -0
  139. package/src/rust/config/plugin_static_config.rs +120 -5
  140. package/src/rust/config/viewer_config.rs +216 -3
  141. package/src/rust/config/workspace_config.rs +138 -10
  142. package/src/rust/custom_elements/viewer.rs +369 -119
  143. package/src/rust/custom_events.rs +43 -1
  144. package/src/rust/js/regular_layout.rs +3 -3
  145. package/src/rust/lib.rs +15 -0
  146. package/src/rust/presentation/column_locator.rs +19 -5
  147. package/src/rust/presentation.rs +200 -36
  148. package/src/rust/queries/column_locator.rs +74 -44
  149. package/src/rust/queries/get_viewer_config.rs +2 -1
  150. package/src/rust/queries/mod.rs +2 -0
  151. package/src/rust/queries/palette_set.rs +82 -0
  152. package/src/rust/queries/plugin_column_styles.rs +6 -2
  153. package/src/rust/renderer/dispatch.rs +16 -4
  154. package/src/rust/renderer/plugin_config.rs +309 -47
  155. package/src/rust/renderer.rs +15 -42
  156. package/src/rust/session/column_defaults_update.rs +32 -7
  157. package/src/rust/session/metadata.rs +23 -21
  158. package/src/rust/session/props.rs +1 -0
  159. package/src/rust/session/replace_expression_update.rs +1 -0
  160. package/src/rust/session.rs +9 -6
  161. package/src/rust/tasks/create_panel.rs +46 -49
  162. package/src/rust/tasks/edit_expression.rs +19 -23
  163. package/src/rust/tasks/edit_window.rs +9 -11
  164. package/src/rust/tasks/eject.rs +4 -1
  165. package/src/rust/tasks/pipeline.rs +2 -2
  166. package/src/rust/tasks/presize_panels.rs +44 -5
  167. package/src/rust/tasks/reset_all.rs +4 -4
  168. package/src/rust/tasks/restore_and_render.rs +17 -44
  169. package/src/rust/tasks/restore_panel.rs +37 -22
  170. package/src/rust/tasks/set_edit_mode.rs +3 -1
  171. package/src/rust/tasks/sync_update_panels.rs +18 -24
  172. package/src/rust/tasks/update_theme.rs +23 -18
  173. package/src/rust/utils/css_vars.rs +26 -0
  174. package/src/rust/utils/mod.rs +2 -0
  175. package/src/rust/utils/spawn.rs +1 -1
  176. package/src/rust/workspace.rs +454 -199
  177. package/src/svg/lock.svg +7 -0
  178. package/src/themes/botanical.css +10 -0
  179. package/src/themes/defaults.css +12 -0
  180. package/src/themes/dracula.css +17 -0
  181. package/src/themes/gruvbox-dark.css +20 -0
  182. package/src/themes/gruvbox.css +10 -0
  183. package/src/themes/icons.css +1 -0
  184. package/src/themes/intl/de.css +16 -0
  185. package/src/themes/intl/es.css +16 -0
  186. package/src/themes/intl/fr.css +16 -0
  187. package/src/themes/intl/ja.css +16 -0
  188. package/src/themes/intl/pt.css +16 -0
  189. package/src/themes/intl/zh.css +16 -0
  190. package/src/themes/intl.css +16 -0
  191. package/src/themes/monokai.css +22 -0
  192. package/src/themes/phosphor.css +5 -0
  193. package/src/themes/pro-dark.css +10 -0
  194. package/src/themes/solarized-dark.css +7 -0
  195. package/src/themes/solarized.css +7 -0
  196. package/src/themes/vaporwave.css +20 -0
  197. package/src/ts/column-format.ts +67 -46
  198. package/src/ts/perspective-viewer.ts +3 -0
  199. package/src/ts/plugin.ts +14 -0
  200. package/src/ts/providers.ts +96 -0
  201. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  202. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  203. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  204. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  205. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  206. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  207. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  208. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  209. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  210. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  211. package/src/ts/ts-rs/FormatMode.ts +3 -0
  212. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  213. package/src/ts/ts-rs/Notation.ts +11 -0
  214. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  215. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -2
  216. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  217. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  218. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  219. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  220. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  221. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  222. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  223. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  224. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  225. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  226. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  227. package/src/ts/ts-rs/Unit.ts +3 -0
  228. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  229. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  230. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  231. package/src/ts/ts-rs/ViewerConfig.ts +48 -2
  232. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  233. package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
  234. package/src/ts/ts-rs/WindowSpec.ts +3 -2
  235. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  236. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +18 -9
  237. package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
  238. package/src/rust/components/form/color_range_selector.rs +0 -137
  239. package/src/ts/ts-rs/WindowAggregate.ts +0 -3
  240. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  241. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  242. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  243. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  244. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  245. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -13,9 +13,7 @@
13
13
  use std::collections::HashSet;
14
14
  use std::rc::Rc;
15
15
 
16
- use perspective_client::config::{
17
- ColumnType, WindowAggregate, WindowFrame, WindowSort, WindowSortDir, WindowSpec,
18
- };
16
+ use perspective_client::config::{ColumnType, WindowFrame, WindowSort, WindowSortDir, WindowSpec};
19
17
  use wasm_bindgen::JsCast;
20
18
  use web_sys::{DragEvent, HtmlInputElement, MouseEvent};
21
19
  use yew::prelude::*;
@@ -30,63 +28,33 @@ use crate::presentation::Presentation;
30
28
  use crate::session::{Session, SessionMetadataRc};
31
29
  use crate::utils::{AddListener, DragEffect, DragTarget, Subscription};
32
30
 
33
- fn op_label(op: WindowAggregate) -> &'static str {
34
- match op {
35
- WindowAggregate::Sum => "sum",
36
- WindowAggregate::Avg => "avg",
37
- WindowAggregate::Count => "count",
38
- WindowAggregate::Min => "min",
39
- WindowAggregate::Max => "max",
40
- WindowAggregate::Stddev => "stddev",
41
- WindowAggregate::Var => "var",
42
- WindowAggregate::First => "first",
43
- WindowAggregate::Last => "last",
44
- WindowAggregate::Lag => "lag",
45
- WindowAggregate::Lead => "lead",
46
- WindowAggregate::Diff => "diff",
47
- WindowAggregate::Rate => "rate",
48
- WindowAggregate::Ema => "ema",
49
- }
50
- }
51
-
52
- fn op_from_label(label: &str) -> Option<WindowAggregate> {
53
- Some(match label {
54
- "sum" => WindowAggregate::Sum,
55
- "avg" => WindowAggregate::Avg,
56
- "count" => WindowAggregate::Count,
57
- "min" => WindowAggregate::Min,
58
- "max" => WindowAggregate::Max,
59
- "stddev" => WindowAggregate::Stddev,
60
- "var" => WindowAggregate::Var,
61
- "first" => WindowAggregate::First,
62
- "last" => WindowAggregate::Last,
63
- "lag" => WindowAggregate::Lag,
64
- "lead" => WindowAggregate::Lead,
65
- "diff" => WindowAggregate::Diff,
66
- "rate" => WindowAggregate::Rate,
67
- "ema" => WindowAggregate::Ema,
68
- _ => return None,
69
- })
31
+ /// The declared capabilities of one window aggregate, for a `source` column
32
+ /// type. Which controls an aggregate needs is the data model's to state - the
33
+ /// editor cannot infer it from a name it has never seen.
34
+ fn op_spec(
35
+ metadata: &SessionMetadataRc,
36
+ source: &str,
37
+ op: &str,
38
+ ) -> Option<perspective_client::proto::WindowAggregateArgs> {
39
+ let ty = metadata.get_column_table_type(source)?;
40
+ metadata.get_window_aggregate(ty, op)
70
41
  }
71
42
 
72
- fn is_aggregating(op: WindowAggregate) -> bool {
73
- matches!(
74
- op,
75
- WindowAggregate::Sum
76
- | WindowAggregate::Avg
77
- | WindowAggregate::Count
78
- | WindowAggregate::Min
79
- | WindowAggregate::Max
80
- | WindowAggregate::Stddev
81
- | WindowAggregate::Var
82
- )
43
+ /// The editor's frame-type labels, as the `frames` a declaration lists.
44
+ fn frame_label(frame: &str) -> &'static str {
45
+ match frame {
46
+ "rows" => "Rows",
47
+ "range" => "Range",
48
+ _ => "Cumulative",
49
+ }
83
50
  }
84
51
 
85
- fn is_positional(op: WindowAggregate) -> bool {
86
- matches!(
87
- op,
88
- WindowAggregate::Lag | WindowAggregate::Lead | WindowAggregate::Diff
89
- )
52
+ fn frame_name(label: &str) -> &'static str {
53
+ match label {
54
+ "Rows" => "rows",
55
+ "Range" => "range",
56
+ _ => "cumulative",
57
+ }
90
58
  }
91
59
 
92
60
  fn is_orderable_for_range(ty: ColumnType) -> bool {
@@ -182,7 +150,7 @@ impl WindowDraft {
182
150
  };
183
151
 
184
152
  Self {
185
- op: op_label(spec.aggregate).to_string(),
153
+ op: spec.aggregate.clone(),
186
154
  source: spec.column.clone(),
187
155
  order_by: spec
188
156
  .order_by
@@ -212,7 +180,7 @@ impl WindowDraft {
212
180
  }
213
181
 
214
182
  fn validate(&self, metadata: &SessionMetadataRc) -> Result<WindowSpec, String> {
215
- let op = op_from_label(&self.op).ok_or("Unknown op")?;
183
+ let op = self.op.clone();
216
184
 
217
185
  // Every slot takes true `Table` columns ONLY - expression aliases
218
186
  // and other window columns would create dependency cycles (and
@@ -245,18 +213,13 @@ impl WindowDraft {
245
213
 
246
214
  // Backstop for API-authored specs opened in the editor - the op
247
215
  // menu only offers the feature-declared set, so this is
248
- // unreachable from the UI.
249
- let available = metadata
250
- .get_features()
251
- .map(|x| x.get_window_aggregates(source_ty))
252
- .unwrap_or_default();
253
-
254
- if !available.contains(&op) {
255
- return Err(format!(
256
- "\"{}\" is not a supported window aggregate for this column",
257
- op_label(op)
258
- ));
259
- }
216
+ // unreachable from the UI. The declaration also supplies the
217
+ // controls this op takes, below.
218
+ let declared = metadata
219
+ .get_window_aggregate(source_ty, &op)
220
+ .ok_or_else(|| {
221
+ format!("\"{op}\" is not a supported window aggregate for this column")
222
+ })?;
260
223
 
261
224
  // An EMPTY order slot is valid when the backend has a natural row
262
225
  // order to fall back on (primary key order in the engine, `rowid`
@@ -300,11 +263,15 @@ impl WindowDraft {
300
263
 
301
264
  // The numeric fields are typed and input-clamped to their domains,
302
265
  // so no parse or range errors are reachable here.
303
- let frame = if is_aggregating(op) || op == WindowAggregate::Rate {
266
+ let frame = if declared.frames.is_empty() {
267
+ None
268
+ } else {
269
+ let chosen = frame_name(&self.frame_type);
270
+ if !declared.frames.iter().any(|x| x == chosen) {
271
+ return Err(format!("\"{op}\" does not support a {chosen} frame"));
272
+ }
273
+
304
274
  match self.frame_type.as_str() {
305
- "Rows" | "Cumulative" if op == WindowAggregate::Rate => {
306
- return Err("\"rate\" requires a range frame".to_string());
307
- },
308
275
  "Rows" => Some(WindowFrame::Rows(self.frame_rows)),
309
276
  "Range" => {
310
277
  // The natural-order fallback has no units, so `range`
@@ -323,15 +290,13 @@ impl WindowDraft {
323
290
  },
324
291
  _ => None,
325
292
  }
326
- } else {
327
- None
328
293
  };
329
294
 
330
295
  // Emit `None` at the engine default so a spec saved without an
331
296
  // explicit `offset` round-trips unchanged (the name-stripped
332
297
  // change-detection baseline compares specs structurally).
333
- let offset = (is_positional(op) && self.offset != 1).then_some(self.offset);
334
- let alpha = (op == WindowAggregate::Ema).then_some(self.alpha);
298
+ let offset = (declared.offset && self.offset != 1).then_some(self.offset);
299
+ let alpha = declared.alpha.then_some(self.alpha);
335
300
 
336
301
  let mut partition_by = self.partition_by.clone();
337
302
  partition_by.retain(|col| !col.is_empty());
@@ -676,12 +641,10 @@ impl Component for WindowEditor {
676
641
  })
677
642
  .unwrap_or_default();
678
643
 
679
- if !op_from_label(&self.draft.op)
680
- .map(|op| available.contains(&op))
681
- .unwrap_or_default()
644
+ if !available.iter().any(|x| x.name == self.draft.op)
682
645
  && let Some(first) = available.first()
683
646
  {
684
- self.draft.op = op_label(*first).to_string();
647
+ self.draft.op = first.name.clone();
685
648
  }
686
649
  },
687
650
  DragTarget::WindowOrderBy => self.draft.order_by = column,
@@ -698,11 +661,19 @@ impl Component for WindowEditor {
698
661
  WindowEditorMsg::SetOp(op) => {
699
662
  self.draft.op = op;
700
663
 
701
- // Keep the frame coherent as the op class changes (`rate`
702
- // requires a Range frame; the frame-type dropdown omits the
703
- // rest).
704
- if op_from_label(&self.draft.op) == Some(WindowAggregate::Rate) {
705
- self.draft.frame_type = "Range".to_string();
664
+ // Keep the frame coherent as the op changes - an op that
665
+ // does not accept the current frame kind coerces to its
666
+ // first declared one (the dropdown omits the rest).
667
+ if let Some(declared) =
668
+ op_spec(&ctx.props().metadata, &self.draft.source, &self.draft.op)
669
+ && !declared.frames.is_empty()
670
+ && !declared
671
+ .frames
672
+ .iter()
673
+ .any(|x| x == frame_name(&self.draft.frame_type))
674
+ && let Some(first) = declared.frames.first()
675
+ {
676
+ self.draft.frame_type = frame_label(first).to_string();
706
677
  }
707
678
  },
708
679
  WindowEditorMsg::ClearSource => self.draft.source = String::default(),
@@ -745,7 +716,7 @@ impl Component for WindowEditor {
745
716
  }
746
717
 
747
718
  fn view(&self, ctx: &Context<Self>) -> Html {
748
- let op = op_from_label(&self.draft.op);
719
+ let declared = op_spec(&ctx.props().metadata, &self.draft.source, &self.draft.op);
749
720
 
750
721
  // The op selector renders the FEATURE-DECLARED window aggregates
751
722
  // for the source column's type, in the server's declared order -
@@ -763,7 +734,7 @@ impl Component for WindowEditor {
763
734
  })
764
735
  .unwrap_or_default()
765
736
  .into_iter()
766
- .map(|x| SelectItem::Option(op_label(x).to_string()))
737
+ .map(|x| SelectItem::Option(x.name.clone()))
767
738
  .collect(),
768
739
  );
769
740
 
@@ -782,17 +753,16 @@ impl Component for WindowEditor {
782
753
  };
783
754
 
784
755
  // Frame type as a dropdown; like the op selector, invalid choices
785
- // are omitted rather than disabled (`rate` requires a Range frame,
786
- // and `SetOp` already coerces the draft there).
756
+ // are omitted rather than disabled - the declared `frames` are the
757
+ // menu, and `SetOp` already coerces the draft into them.
787
758
  let frame_types: Rc<Vec<SelectItem<String>>> = Rc::new(
788
- if op == Some(WindowAggregate::Rate) {
789
- vec!["Range"]
790
- } else {
791
- vec!["Rows", "Range", "Cumulative"]
792
- }
793
- .into_iter()
794
- .map(|x| SelectItem::Option(x.to_string()))
795
- .collect(),
759
+ declared
760
+ .as_ref()
761
+ .map(|x| x.frames.clone())
762
+ .unwrap_or_default()
763
+ .iter()
764
+ .map(|x| SelectItem::Option(frame_label(x).to_string()))
765
+ .collect(),
796
766
  );
797
767
 
798
768
  // Slots take true `Table` columns ONLY - expression aliases and
@@ -927,10 +897,12 @@ impl Component for WindowEditor {
927
897
  </DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>>
928
898
  };
929
899
 
930
- let show_frame =
931
- op.map(is_aggregating).unwrap_or_default() || op == Some(WindowAggregate::Rate);
932
- let show_offset = op.map(is_positional).unwrap_or_default();
933
- let show_alpha = op == Some(WindowAggregate::Ema);
900
+ let show_frame = declared
901
+ .as_ref()
902
+ .map(|x| !x.frames.is_empty())
903
+ .unwrap_or_default();
904
+ let show_offset = declared.as_ref().map(|x| x.offset).unwrap_or_default();
905
+ let show_alpha = declared.as_ref().map(|x| x.alpha).unwrap_or_default();
934
906
 
935
907
  html! {
936
908
  <>
@@ -15,18 +15,12 @@ use std::collections::HashSet;
15
15
  use serde::{Deserialize, Serialize};
16
16
  use serde_json::Value;
17
17
 
18
- use super::{KeyValueOpts, NumberSeriesStyleDefaultConfig};
18
+ use super::{CssKind, KeyValueOpts, NumberSeriesStyleDefaultConfig};
19
19
 
20
20
  /// The full schema for one column at one point in time. Plugins may return
21
21
  /// different schemas for the same column based on the column's current
22
22
  /// stored value (e.g. to hide dependent fields), so this is re-queried on
23
23
  /// every field update.
24
- ///
25
- /// Each entry is a [`ControlSpec`]. Primitive variants carry their own
26
- /// `key` (JSON storage key) inline; the sidebar UI label is supplied by
27
- /// CSS via `--psp-label--<key>--content`. Composite variants render a
28
- /// self-contained Yew component that supplies its own labels and owns a
29
- /// fixed key namespace via [`ControlSpec::serialized_keys`].
30
24
  #[derive(Clone, Debug, Default, Deserialize, Serialize)]
31
25
  pub struct ColumnConfigSchema {
32
26
  pub fields: Vec<ControlSpec>,
@@ -93,33 +87,21 @@ pub enum ControlSpec {
93
87
  key: String,
94
88
  default: String,
95
89
  },
96
- /// Paired pos/neg color picker rendered as a single horizontal
97
- /// gradient/range bar. Used to expose the existing
98
- /// [`crate::components::form::color_range_selector::ColorRangeSelector`]
99
- /// widget at primitive granularity. Owns two top-level keys
100
- /// (`key_pos` + `key_neg`); the visible label is derived from
101
- /// `key_pos`.
102
- ColorRange {
103
- key_pos: String,
104
- key_neg: String,
105
- default_pos: String,
106
- default_neg: String,
107
- /// When `true`, the bar renders as a continuous gradient
108
- /// (e.g. for `gradient` color modes); when `false`, the bar
109
- /// renders as a hard pos/neg split. Currently only changes the
110
- /// visual; pos/neg semantics are unchanged.
111
- #[serde(default)]
112
- is_gradient: bool,
90
+ Palette {
91
+ key: String,
92
+ default: String,
93
+
94
+ #[serde(default, skip_serializing_if = "Option::is_none")]
95
+ max: Option<usize>,
113
96
  },
97
+ GradientStops {
98
+ key: String,
99
+ default: String,
114
100
 
115
- /// Residual format-only widget for datetime columns — owns
116
- /// `date_format` only. Pair with primitive `Enum` and `Color` fields
117
- /// for `datetime_color_mode` + `color` to fully decompose datetime
118
- /// styling.
101
+ #[serde(default, skip_serializing_if = "std::ops::Not::not")]
102
+ discrete: bool,
103
+ },
119
104
  DatetimeFormat,
120
- /// Residual format-only widget for string columns — owns `format`
121
- /// only. Pair with primitive `Enum` and `Color` fields for
122
- /// `string_color_mode` + `color` to fully decompose string styling.
123
105
  StringFormat,
124
106
  NumberSeriesStyle {
125
107
  default: NumberSeriesStyleDefaultConfig,
@@ -138,6 +120,90 @@ pub struct EnumVariant {
138
120
  pub label: Option<String>,
139
121
  }
140
122
 
123
+ /// One stop of a [`ControlSpec::GradientStops`] value in its in-memory
124
+ /// form: a `#rrggbb` color at `offset` ∈ `[0, 1]`.
125
+ #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
126
+ pub struct GradientStopSpec {
127
+ pub color: String,
128
+ pub offset: f64,
129
+ }
130
+
131
+ /// The [`ControlSpec::GradientStops`] canonical stop order: offsets
132
+ /// clamped to `[0, 1]` and rounded to 3 decimals, stops sorted stably
133
+ /// by offset.
134
+ pub fn canonicalize_gradient_stops(mut stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
135
+ for stop in &mut stops {
136
+ stop.offset = (stop.offset.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
137
+ }
138
+
139
+ stops.sort_by(|a, b| {
140
+ a.offset
141
+ .partial_cmp(&b.offset)
142
+ .unwrap_or(std::cmp::Ordering::Equal)
143
+ });
144
+
145
+ stops
146
+ }
147
+
148
+ /// Fit `stops` to a `discrete` field's fixed pair: an over-length value
149
+ /// keeps only its two end colors, pinned to `0`/`1`.
150
+ pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
151
+ let stops = canonicalize_gradient_stops(stops);
152
+ match (stops.first(), stops.last()) {
153
+ (Some(first), Some(last)) if stops.len() > 2 => vec![
154
+ GradientStopSpec {
155
+ color: first.color.clone(),
156
+ offset: 0.0,
157
+ },
158
+ GradientStopSpec {
159
+ color: last.color.clone(),
160
+ offset: 1.0,
161
+ },
162
+ ],
163
+ _ => stops,
164
+ }
165
+ }
166
+
167
+ impl ColumnConfigSchema {
168
+ /// Canonicalize every CSS-valued default at schema ingest, dropping
169
+ /// (and logging) fields whose default fails its kind's reader.
170
+ pub fn canonicalize_defaults(mut self) -> Self {
171
+ self.fields.retain_mut(|spec| {
172
+ let (kind, key, default) = match spec {
173
+ ControlSpec::Color { key, default } => (CssKind::Color, key, default),
174
+ ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
175
+ ControlSpec::GradientStops { key, default, .. } => {
176
+ (CssKind::Gradient, key, default)
177
+ },
178
+ _ => return true,
179
+ };
180
+
181
+ match kind.canonicalize(default) {
182
+ Ok(canonical) => {
183
+ *default = canonical;
184
+ true
185
+ },
186
+ Err(error) => {
187
+ tracing::error!("Dropping `{key}` — invalid schema default: {error}");
188
+ false
189
+ },
190
+ }
191
+ });
192
+
193
+ self
194
+ }
195
+
196
+ /// The CSS kind of the control owning `key`, if it is CSS-valued.
197
+ pub fn css_kind_of(&self, key: &str) -> Option<CssKind> {
198
+ self.fields.iter().find_map(|spec| match spec {
199
+ ControlSpec::Color { key: k, .. } if k == key => Some(CssKind::Color),
200
+ ControlSpec::Palette { key: k, .. } if k == key => Some(CssKind::Palette),
201
+ ControlSpec::GradientStops { key: k, .. } if k == key => Some(CssKind::Gradient),
202
+ _ => None,
203
+ })
204
+ }
205
+ }
206
+
141
207
  impl ControlSpec {
142
208
  /// Top-level JSON keys this control owns when its value is serialized
143
209
  /// into a column's config map. For primitives this is just `[key]`;
@@ -152,14 +218,13 @@ impl ControlSpec {
152
218
  ControlSpec::Symbols { .. } => vec!["symbols"],
153
219
  ControlSpec::NumberFormat => vec!["number_format"],
154
220
  ControlSpec::AggregateDepth => vec!["aggregate_depth"],
155
- ControlSpec::ColorRange {
156
- key_pos, key_neg, ..
157
- } => vec![key_pos.as_str(), key_neg.as_str()],
158
221
  ControlSpec::Enum { key, .. }
159
222
  | ControlSpec::Bool { key, .. }
160
223
  | ControlSpec::Number { key, .. }
161
224
  | ControlSpec::String { key, .. }
162
- | ControlSpec::Color { key, .. } => vec![key.as_str()],
225
+ | ControlSpec::Color { key, .. }
226
+ | ControlSpec::Palette { key, .. }
227
+ | ControlSpec::GradientStops { key, .. } => vec![key.as_str()],
163
228
  }
164
229
  }
165
230
  }
@@ -168,12 +233,6 @@ impl ControlSpec {
168
233
  /// declares which top-level keys the update is allowed to write
169
234
  /// (`keys` — equivalent to the field's [`ControlSpec::serialized_keys`])
170
235
  /// and a partial new sub-state (`value`).
171
- ///
172
- /// Apply semantics: keys in `keys` are *cleared* from the column's config
173
- /// map, then keys present in `value` are *inserted*. Defaults are
174
- /// pre-stripped by the caller (typically via `skip_serializing_if`), so
175
- /// "no value set for key K" means the schema default applies and K is
176
- /// not serialized.
177
236
  #[derive(Clone, Debug, Deserialize, Serialize)]
178
237
  pub struct ColumnConfigFieldUpdate {
179
238
  pub keys: Vec<String>,