@perspective-dev/viewer 5.1.0 → 5.2.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 (175) 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/intl/de.css +1 -1
  8. package/dist/css/intl/es.css +1 -1
  9. package/dist/css/intl/fr.css +1 -1
  10. package/dist/css/intl/ja.css +1 -1
  11. package/dist/css/intl/pt.css +1 -1
  12. package/dist/css/intl/zh.css +1 -1
  13. package/dist/css/intl.css +1 -1
  14. package/dist/css/monokai.css +1 -1
  15. package/dist/css/phosphor.css +1 -1
  16. package/dist/css/pro-dark.css +1 -1
  17. package/dist/css/pro.css +1 -1
  18. package/dist/css/solarized-dark.css +1 -1
  19. package/dist/css/solarized.css +1 -1
  20. package/dist/css/themes.css +1 -1
  21. package/dist/css/vaporwave.css +1 -1
  22. package/dist/docs/perspective-docs.json +1 -0
  23. package/dist/esm/column-format.d.ts +19 -28
  24. package/dist/esm/perspective-viewer.d.ts +2 -0
  25. package/dist/esm/perspective-viewer.inline.js +8 -8
  26. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  27. package/dist/esm/perspective-viewer.js +9 -9
  28. package/dist/esm/perspective-viewer.js.map +4 -4
  29. package/dist/esm/providers.d.ts +79 -0
  30. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  31. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  32. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  33. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  34. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  35. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  36. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  37. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  38. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  39. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  40. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  41. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  42. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  43. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  44. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -1
  45. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  46. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  47. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  48. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  49. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  50. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  51. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  52. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  53. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  54. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  55. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  56. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  57. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  58. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  59. package/dist/esm/ts-rs/ViewerConfig.d.ts +41 -1
  60. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  61. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
  62. package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
  63. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
  64. package/dist/wasm/perspective-viewer.d.ts +134 -25
  65. package/dist/wasm/perspective-viewer.js +367 -124
  66. package/dist/wasm/perspective-viewer.wasm +0 -0
  67. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
  68. package/package.json +2 -1
  69. package/src/css/_viewer_bundle.css +1 -0
  70. package/src/css/column-selector.css +8 -0
  71. package/src/css/config-selector.css +38 -3
  72. package/src/css/form/code-editor.css +2 -3
  73. package/src/css/form/mirrored-textarea.css +81 -0
  74. package/src/css/viewer.css +328 -0
  75. package/src/rust/agent/client/mod.rs +19 -0
  76. package/src/rust/agent/client/protocol.rs +320 -0
  77. package/src/rust/agent/client/transport.rs +287 -0
  78. package/src/rust/agent/client/turn.rs +174 -0
  79. package/src/rust/agent/config.rs +159 -0
  80. package/src/rust/agent/docs.rs +293 -0
  81. package/src/rust/agent/markdown/block.rs +509 -0
  82. package/src/rust/agent/markdown/entity.rs +52 -0
  83. package/src/rust/agent/markdown/events.rs +71 -0
  84. package/src/rust/agent/markdown/inline.rs +497 -0
  85. package/src/rust/agent/markdown/mod.rs +466 -0
  86. package/src/rust/agent/mod.rs +23 -0
  87. package/src/rust/agent/preamble.md +16 -0
  88. package/src/rust/agent/runtime.rs +201 -0
  89. package/src/rust/agent/slot.rs +236 -0
  90. package/src/rust/agent/tools.rs +1110 -0
  91. package/src/rust/components/chat_panel.rs +289 -0
  92. package/src/rust/components/column_selector/config_selector.rs +76 -7
  93. package/src/rust/components/column_selector.rs +7 -0
  94. package/src/rust/components/containers/dragdrop_list.rs +37 -0
  95. package/src/rust/components/form/code_editor.rs +14 -23
  96. package/src/rust/components/form/debug.rs +8 -5
  97. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  98. package/src/rust/components/form/mod.rs +1 -0
  99. package/src/rust/components/mod.rs +2 -0
  100. package/src/rust/components/settings_panel.rs +80 -2
  101. package/src/rust/components/viewer/panels.rs +9 -14
  102. package/src/rust/components/viewer/render.rs +2 -0
  103. package/src/rust/components/window_editor.rs +75 -103
  104. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  105. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  106. package/src/rust/config/number_string_format.rs +33 -0
  107. package/src/rust/config/options.rs +18 -0
  108. package/src/rust/config/plugin_static_config.rs +120 -5
  109. package/src/rust/config/viewer_config.rs +220 -1
  110. package/src/rust/config/workspace_config.rs +7 -6
  111. package/src/rust/custom_elements/viewer.rs +209 -18
  112. package/src/rust/custom_events.rs +29 -0
  113. package/src/rust/lib.rs +13 -0
  114. package/src/rust/presentation.rs +8 -0
  115. package/src/rust/queries/get_viewer_config.rs +5 -1
  116. package/src/rust/session/column_defaults_update.rs +18 -0
  117. package/src/rust/session/metadata.rs +18 -20
  118. package/src/rust/session/replace_expression_update.rs +1 -0
  119. package/src/rust/tasks/create_panel.rs +18 -8
  120. package/src/rust/tasks/edit_window.rs +1 -1
  121. package/src/rust/tasks/restore_panel.rs +34 -17
  122. package/src/rust/tasks/sync_update_panels.rs +7 -23
  123. package/src/rust/workspace.rs +112 -20
  124. package/src/themes/intl/de.css +16 -0
  125. package/src/themes/intl/es.css +16 -0
  126. package/src/themes/intl/fr.css +16 -0
  127. package/src/themes/intl/ja.css +16 -0
  128. package/src/themes/intl/pt.css +16 -0
  129. package/src/themes/intl/zh.css +16 -0
  130. package/src/themes/intl.css +16 -0
  131. package/src/ts/column-format.ts +67 -46
  132. package/src/ts/perspective-viewer.ts +2 -0
  133. package/src/ts/providers.ts +96 -0
  134. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  135. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  136. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  137. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  138. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  139. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  140. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  141. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  142. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  143. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  144. package/src/ts/ts-rs/FormatMode.ts +3 -0
  145. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  146. package/src/ts/ts-rs/Notation.ts +11 -0
  147. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  148. package/src/ts/ts-rs/PanelViewerConfig.ts +47 -1
  149. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  150. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  151. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  152. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  153. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  154. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  155. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  156. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  157. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  158. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  159. package/src/ts/ts-rs/Unit.ts +3 -0
  160. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  161. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  162. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  163. package/src/ts/ts-rs/ViewerConfig.ts +47 -1
  164. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  165. package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
  166. package/src/ts/ts-rs/WindowSpec.ts +3 -2
  167. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
  168. package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
  169. package/src/ts/ts-rs/WindowAggregate.ts +0 -3
  170. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
  171. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
  172. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
  173. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
  174. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
  175. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/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
  <>
@@ -62,14 +62,22 @@ const fn numeric_default() -> u32 {
62
62
  0
63
63
  }
64
64
 
65
+ /// A datetime column's `date_format` in its `Custom` form: per-part
66
+ /// `Intl.DateTimeFormatOptions` overrides, discriminated from the `Simple`
67
+ /// preset form by the required `format: "custom"` key. Each part is a
68
+ /// [`CustomDatetimeFormat`] variant valid for that `Intl` option;
69
+ /// `"disabled"` omits the part from the formatted output entirely.
65
70
  #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TS)]
66
71
  pub struct CustomDatetimeStyleConfig {
67
72
  format: FormatUnit,
68
73
 
74
+ /// An IANA time zone name (e.g. `"America/New_York"`); defaults to the
75
+ /// browser's local time zone.
69
76
  #[serde(default)]
70
77
  #[serde(rename = "timeZone", skip_serializing_if = "Option::is_none")]
71
78
  pub time_zone: Option<String>,
72
79
 
80
+ /// Sub-second digits to display, 0-3 (0 = none).
73
81
  #[serde(
74
82
  skip_serializing_if = "is_zero",
75
83
  rename = "fractionalSecondDigits",
@@ -77,27 +85,36 @@ pub struct CustomDatetimeStyleConfig {
77
85
  )]
78
86
  pub fractional_seconds: u32,
79
87
 
88
+ /// Defaults to `"numeric"`.
80
89
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
81
90
  pub second: CustomDatetimeFormat,
82
91
 
92
+ /// Defaults to `"numeric"`.
83
93
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
84
94
  pub minute: CustomDatetimeFormat,
85
95
 
96
+ /// Defaults to `"numeric"`.
86
97
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
87
98
  pub hour: CustomDatetimeFormat,
88
99
 
100
+ /// Defaults to `"numeric"`.
89
101
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
90
102
  pub day: CustomDatetimeFormat,
91
103
 
104
+ /// A name form (`"long"`/`"short"`/`"narrow"`); defaults to
105
+ /// `"disabled"` (weekday not shown).
92
106
  #[serde(skip_serializing_if = "is_disabled", default = "weekday_default")]
93
107
  pub weekday: CustomDatetimeFormat,
94
108
 
109
+ /// Numeric or name form; defaults to `"numeric"`.
95
110
  #[serde(skip_serializing_if = "is_numeric", default = "second_default")]
96
111
  pub month: CustomDatetimeFormat,
97
112
 
113
+ /// Defaults to `"2-digit"`.
98
114
  #[serde(skip_serializing_if = "is_two_digit", default = "year_default")]
99
115
  pub year: CustomDatetimeFormat,
100
116
 
117
+ /// 12-hour clock; defaults to `true`.
101
118
  #[serde(skip_serializing_if = "is_true", default = "hour12_default")]
102
119
  pub hour12: bool,
103
120
  }
@@ -23,12 +23,20 @@ const fn time_style_default() -> SimpleDatetimeFormat {
23
23
  SimpleDatetimeFormat::Medium
24
24
  }
25
25
 
26
+ /// A datetime column's `date_format` in its `Simple` preset form:
27
+ /// `Intl.DateTimeFormatOptions`' `dateStyle`/`timeStyle` presets. This is
28
+ /// the default form (no `format` key); setting `format: "custom"` selects
29
+ /// the per-part `CustomDatetimeStyleConfig` instead.
26
30
  #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TS)]
27
31
  pub struct SimpleDatetimeStyleConfig {
32
+ /// An IANA time zone name (e.g. `"America/New_York"`); defaults to the
33
+ /// browser's local time zone.
28
34
  #[serde(default)]
29
35
  #[serde(rename = "timeZone", skip_serializing_if = "Option::is_none")]
30
36
  pub time_zone: Option<String>,
31
37
 
38
+ /// Date preset breadth, or `"disabled"` to omit the date entirely;
39
+ /// defaults to `"short"`.
32
40
  #[serde(
33
41
  default = "date_style_default",
34
42
  rename = "dateStyle",
@@ -36,6 +44,8 @@ pub struct SimpleDatetimeStyleConfig {
36
44
  )]
37
45
  pub date_style: SimpleDatetimeFormat,
38
46
 
47
+ /// Time preset breadth, or `"disabled"` to omit the time entirely;
48
+ /// defaults to `"medium"`.
39
49
  #[serde(
40
50
  default = "time_style_default",
41
51
  rename = "timeStyle",
@@ -16,6 +16,11 @@ use serde::{Deserialize, Serialize};
16
16
  use strum::{Display, EnumIter};
17
17
  use ts_rs::TS;
18
18
 
19
+ /// The `style` family of a numeric column's `number_format` — serialized
20
+ /// FLATTENED into [`CustomNumberFormatConfig`]'s object, discriminated by
21
+ /// the `style` key (`"decimal"` default, `"currency"` + `currency`/
22
+ /// `currencyDisplay`/`currencySign`, `"percent"`, `"unit"` + `unit`/
23
+ /// `unitDisplay`), mirroring `Intl.NumberFormat` options.
19
24
  #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
20
25
  #[serde(rename_all = "camelCase", tag = "style")]
21
26
  pub enum NumberFormatStyle {
@@ -127,6 +132,11 @@ pub enum TrailingZeroDisplay {
127
132
  StripIfInteger,
128
133
  }
129
134
 
135
+ /// The `notation` family of a numeric column's `number_format` —
136
+ /// serialized FLATTENED into [`CustomNumberFormatConfig`]'s object,
137
+ /// discriminated by the `notation` key (`"standard"` default,
138
+ /// `"scientific"`, `"engineering"`, `"compact"` + `compactDisplay`),
139
+ /// mirroring `Intl.NumberFormat` options.
130
140
  #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
131
141
  #[serde(rename_all = "camelCase", tag = "notation")]
132
142
  pub enum Notation {
@@ -169,6 +179,14 @@ pub enum SignDisplay {
169
179
  Never,
170
180
  }
171
181
 
182
+ /// A numeric column's `number_format` (`columns_config` value) —
183
+ /// `Intl.NumberFormat`-shaped options, written by the Style tab's number
184
+ /// format editor and read by `createNumberFormatter`. The `style` and
185
+ /// `notation` families ([`NumberFormatStyle`] / [`Notation`]) are serde-
186
+ /// FLATTENED into this object but `#[ts(skip)]`'d (ts-rs cannot flatten
187
+ /// `Option<enum>`) — the package's `NumberFormatConfig` re-composes the
188
+ /// full wire type as `CustomNumberFormatConfig & Partial<NumberFormatStyle>
189
+ /// & Partial<Notation>` (see `column-format.ts`).
172
190
  #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, TS)]
173
191
  #[serde(rename_all = "camelCase")]
174
192
  pub struct CustomNumberFormatConfig {
@@ -181,33 +199,42 @@ pub struct CustomNumberFormatConfig {
181
199
  // these min/max props can all be specified but it results in possible conflicts
182
200
  // may consider making them distinct options
183
201
  #[serde(skip_serializing_if = "Option::is_none")]
202
+ #[ts(optional, as = "Option<_>")]
184
203
  pub minimum_integer_digits: Option<f64>,
185
204
 
186
205
  #[serde(skip_serializing_if = "Option::is_none")]
206
+ #[ts(optional, as = "Option<_>")]
187
207
  pub minimum_fraction_digits: Option<f64>,
188
208
 
189
209
  #[serde(skip_serializing_if = "Option::is_none")]
210
+ #[ts(optional, as = "Option<_>")]
190
211
  pub maximum_fraction_digits: Option<f64>,
191
212
 
192
213
  #[serde(skip_serializing_if = "Option::is_none")]
214
+ #[ts(optional, as = "Option<_>")]
193
215
  pub minimum_significant_digits: Option<f64>,
194
216
 
195
217
  #[serde(skip_serializing_if = "Option::is_none")]
218
+ #[ts(optional, as = "Option<_>")]
196
219
  pub maximum_significant_digits: Option<f64>,
197
220
 
198
221
  #[serde(skip_serializing_if = "Option::is_none")]
222
+ #[ts(optional, as = "Option<_>")]
199
223
  pub rounding_priority: Option<RoundingPriority>,
200
224
 
201
225
  // specific values https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement
202
226
  // Only available with automatic rounding priority
203
227
  // Cannot be mixed with sigfig rounding. (Does this mean max/min sigfig must be unset?)
204
228
  #[serde(skip_serializing_if = "Option::is_none")]
229
+ #[ts(optional, as = "Option<_>")]
205
230
  pub rounding_increment: Option<f64>,
206
231
 
207
232
  #[serde(skip_serializing_if = "Option::is_none")]
233
+ #[ts(optional, as = "Option<_>")]
208
234
  pub rounding_mode: Option<RoundingMode>,
209
235
 
210
236
  #[serde(skip_serializing_if = "Option::is_none")]
237
+ #[ts(optional, as = "Option<_>")]
211
238
  pub trailing_zero_display: Option<TrailingZeroDisplay>,
212
239
 
213
240
  #[serde(flatten)]
@@ -215,10 +242,16 @@ pub struct CustomNumberFormatConfig {
215
242
  #[serde(skip_serializing_if = "Option::is_none")]
216
243
  pub _notation: Option<Notation>,
217
244
 
245
+ /// NOTE (audit 2026-08-05): serialized values are the STRINGS
246
+ /// `"always"`/`"auto"`/`"min2"` or the untagged BOOLEAN `false` —
247
+ /// the former hand-written TS `useGrouping?: boolean` was wrong for
248
+ /// the string cases.
218
249
  #[serde(skip_serializing_if = "Option::is_none")]
250
+ #[ts(optional, as = "Option<_>")]
219
251
  pub use_grouping: Option<UseGrouping>,
220
252
 
221
253
  #[serde(skip_serializing_if = "Option::is_none")]
254
+ #[ts(optional, as = "Option<_>")]
222
255
  pub sign_display: Option<SignDisplay>,
223
256
  }
224
257
 
@@ -28,6 +28,24 @@ pub struct PanelOptions {
28
28
  pub panel: Option<String>,
29
29
  }
30
30
 
31
+ /// Options for the `restore()` method.
32
+ #[derive(Deserialize, Default, TS)]
33
+ pub struct RestoreOptions {
34
+ /// The target panel; the active panel when omitted.
35
+ #[ts(optional)]
36
+ pub panel: Option<String>,
37
+
38
+ /// When `true`, a failed restore only REJECTS the returned `Promise` —
39
+ /// the error is not committed to the viewer's visible error state, and
40
+ /// the session remains usable for subsequent calls. For programmatic
41
+ /// callers (e.g. the LLM agent's `set_view_config` tool) for whom a
42
+ /// failed config patch is feedback rather than a user-facing fault.
43
+ /// The config may be partially applied on failure; restore a
44
+ /// known-good config to recover exactly.
45
+ #[ts(optional)]
46
+ pub suppress_errors: Option<bool>,
47
+ }
48
+
31
49
  /// The `eject` argument: the loaded client to remove by name; the active
32
50
  /// panel's client when omitted.
33
51
  #[derive(Deserialize, Default, TS)]
@@ -10,7 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use perspective_client::config::GroupRollupMode;
13
+ use perspective_client::config::{GroupRollupMode, SplitRollupMode};
14
14
  use serde::Deserialize;
15
15
  use ts_rs::TS;
16
16
 
@@ -103,6 +103,13 @@ pub struct PluginStaticConfig {
103
103
  #[ts(optional)]
104
104
  pub group_rollup_modes: Option<Vec<GroupRollupMode>>,
105
105
 
106
+ /// Split-rollup modes the plugin accepts, in preference order.
107
+ /// The first entry that matches a feature flag becomes the default.
108
+ #[serde(default)]
109
+ #[ts(as = "Option<_>")]
110
+ #[ts(optional)]
111
+ pub split_rollup_modes: Option<Vec<SplitRollupMode>>,
112
+
106
113
  /// Plugin load priority. Higher numbers win; ties resolve in
107
114
  /// registration order. The highest-priority plugin is loaded by
108
115
  /// default unless `restore({ plugin })` overrides it.
@@ -120,15 +127,70 @@ pub struct PluginStaticConfig {
120
127
  #[ts(as = "Option<_>")]
121
128
  #[ts(optional)]
122
129
  pub can_render_column_styles: bool,
130
+
131
+ /// What `group_by` MEANS visually for this plugin, e.g. `"X Axis"`
132
+ /// for the Y-series charts or `"Hierarchy"` for treemap/sunburst.
133
+ /// `None` where the field has no visual role of its own and is a
134
+ /// plain aggregation key (the X/Y charts, whose axes both come from
135
+ /// `columns`).
136
+ ///
137
+ /// This is the `group_by` counterpart of [`Self::config_column_names`]:
138
+ /// the same declaration that names the positional `columns` slots
139
+ /// should say what the other view fields draw, so that consumers —
140
+ /// the settings UI's field labels, and the agent's `list_plugins`
141
+ /// contract — read one source instead of restating the mapping.
142
+ #[serde(default)]
143
+ #[ts(as = "Option<_>")]
144
+ #[ts(optional)]
145
+ pub group_by_role: Option<String>,
146
+
147
+ /// What `split_by` MEANS visually for this plugin, e.g. `"Series"`.
148
+ /// See [`Self::group_by_role`].
149
+ #[serde(default)]
150
+ #[ts(as = "Option<_>")]
151
+ #[ts(optional)]
152
+ pub split_by_role: Option<String>,
153
+
154
+ /// `true` when this plugin CONNECTS its points in row order, so the
155
+ /// `View`'s row order is visible in the drawing: an unsorted config
156
+ /// renders the table's natural order, which reads as a tangle unless
157
+ /// the rows already arrive ordered along the axis. Declared rather
158
+ /// than inferred, because "unsorted" is not itself a mistake — a
159
+ /// pre-ordered table needs no `sort`, and the point plugins
160
+ /// (scatter, density) do not care at all.
161
+ #[serde(default)]
162
+ #[ts(as = "Option<_>")]
163
+ #[ts(optional)]
164
+ pub connects_row_order: bool,
123
165
  }
124
166
 
125
167
  impl PluginStaticConfig {
168
+ /// The number of leading `columns` slots that are POSITIONAL: a drop
169
+ /// there swaps with the column already present, and the slot's
170
+ /// meaning is fixed by [`Self::config_column_names`]. Everything from
171
+ /// this index on is the insert TAIL, which repeats the last named
172
+ /// role — which is why a `Y Line` (named slots: `["Y Axis"]`) takes
173
+ /// any number of columns as additional Y series, while an
174
+ /// `X/Y Line` (`["X Axis", "Y Axis", "Tooltip"]`) pins its two axes
175
+ /// and treats the rest as further tooltips.
176
+ ///
177
+ /// The single definition of that rule: [`Self::is_swap`] and the
178
+ /// agent's `list_plugins` contract both derive from it, so the
179
+ /// convention cannot be read two different ways.
180
+ pub fn positional_columns(&self) -> usize {
181
+ self.config_column_names.len().saturating_sub(1)
182
+ }
183
+
184
+ /// The role that `columns` past the positional slots repeat, i.e.
185
+ /// the last named slot. `None` when the plugin names no slots.
186
+ pub fn tail_column_role(&self) -> Option<&str> {
187
+ self.config_column_names.last().map(|x| x.as_str())
188
+ }
189
+
126
190
  /// `true` if dropping a column at `index` should swap with the
127
- /// column already there rather than insert. Only the named slots
128
- /// (`config_column_names[..len()-1]`) participate in swap behaviour;
129
- /// the trailing unnamed tail inserts.
191
+ /// column already there rather than insert.
130
192
  pub fn is_swap(&self, index: usize) -> bool {
131
- !self.config_column_names.is_empty() && index < self.config_column_names.len() - 1
193
+ !self.config_column_names.is_empty() && index < self.positional_columns()
132
194
  }
133
195
 
134
196
  pub fn get_group_rollups(&self, rollup_features: &[GroupRollupMode]) -> Vec<GroupRollupMode> {
@@ -141,4 +203,57 @@ impl PluginStaticConfig {
141
203
  })
142
204
  .unwrap_or_default()
143
205
  }
206
+
207
+ pub fn get_split_rollups(&self, rollup_features: &[SplitRollupMode]) -> Vec<SplitRollupMode> {
208
+ self.split_rollup_modes
209
+ .clone()
210
+ .map(|x| {
211
+ x.into_iter()
212
+ .filter(|y| rollup_features.is_empty() || rollup_features.contains(y))
213
+ .collect()
214
+ })
215
+ .unwrap_or_default()
216
+ }
217
+ }
218
+
219
+ #[cfg(test)]
220
+ mod tests {
221
+ use super::*;
222
+
223
+ fn plugin(names: &[&str]) -> PluginStaticConfig {
224
+ PluginStaticConfig {
225
+ config_column_names: names.iter().map(|x| (*x).to_owned()).collect(),
226
+ ..Default::default()
227
+ }
228
+ }
229
+
230
+ /// The tail rule has ONE definition: `is_swap` and the role a
231
+ /// trailing column repeats must never disagree about where the
232
+ /// positional slots end.
233
+ #[test]
234
+ fn positional_slots_and_tail_agree() {
235
+ // `Y Line` — the only named slot IS the tail, so every column
236
+ // is another Y series and none of them swap.
237
+ let y_line = plugin(&["Y Axis"]);
238
+ assert_eq!(y_line.positional_columns(), 0);
239
+ assert_eq!(y_line.tail_column_role(), Some("Y Axis"));
240
+ assert!(!y_line.is_swap(0));
241
+
242
+ // `X/Y Line` — two pinned axes, then tooltips.
243
+ let xy_line = plugin(&["X Axis", "Y Axis", "Tooltip"]);
244
+ assert_eq!(xy_line.positional_columns(), 2);
245
+ assert_eq!(xy_line.tail_column_role(), Some("Tooltip"));
246
+ assert!(xy_line.is_swap(0));
247
+ assert!(xy_line.is_swap(1));
248
+ assert!(!xy_line.is_swap(2));
249
+
250
+ for index in 0..4 {
251
+ assert_eq!(xy_line.is_swap(index), index < xy_line.positional_columns());
252
+ }
253
+
254
+ let unnamed = plugin(&[]);
255
+ assert_eq!(unnamed.positional_columns(), 0);
256
+ assert_eq!(unnamed.tail_column_role(), None);
257
+ assert!(!unnamed.is_swap(0));
258
+ }
144
259
  }