@perspective-dev/viewer 5.5.1 → 5.6.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 (194) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  43. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  44. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  45. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  46. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  47. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  48. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  49. package/dist/wasm/perspective-viewer.d.ts +115 -35
  50. package/dist/wasm/perspective-viewer.js +314 -190
  51. package/dist/wasm/perspective-viewer.wasm +0 -0
  52. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  53. package/package.json +31 -18
  54. package/src/css/column-selector.css +205 -50
  55. package/src/css/ui/containers/context-menu.css +12 -1
  56. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  57. package/src/css/ui/containers/scroll-panel.css +4 -1
  58. package/src/rust/agent/client/protocol.rs +0 -89
  59. package/src/rust/agent/markdown/mod.rs +0 -238
  60. package/src/rust/agent/preamble.md +1 -0
  61. package/src/rust/agent/tools.rs +2 -16
  62. package/src/rust/components/column_selector/active_column.rs +15 -5
  63. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  64. package/src/rust/components/column_selector/config_selector.rs +34 -6
  65. package/src/rust/components/column_selector/derived_control.rs +229 -0
  66. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  67. package/src/rust/components/column_selector/filter_column.rs +0 -2
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  69. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  70. package/src/rust/components/column_selector/sort_column.rs +4 -3
  71. package/src/rust/components/column_selector.rs +21 -1
  72. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  73. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  74. package/src/rust/components/column_settings_sidebar.rs +25 -19
  75. package/src/rust/components/debug_panel.rs +38 -29
  76. package/src/rust/components/dragdrop_list.rs +8 -9
  77. package/src/rust/components/main_panel/msg.rs +6 -4
  78. package/src/rust/components/main_panel/reconcile.rs +6 -1
  79. package/src/rust/components/main_panel/render.rs +70 -51
  80. package/src/rust/components/main_panel/update.rs +39 -1
  81. package/src/rust/components/main_panel.rs +97 -59
  82. package/src/rust/components/new_panel_menu.rs +69 -1
  83. package/src/rust/components/panel_menu.rs +63 -12
  84. package/src/rust/components/plugin_tab.rs +3 -2
  85. package/src/rust/components/render_warning.rs +2 -1
  86. package/src/rust/components/settings_panel.rs +4 -4
  87. package/src/rust/components/status_bar.rs +74 -24
  88. package/src/rust/components/status_indicator.rs +8 -1
  89. package/src/rust/components/viewer/filters.rs +13 -1
  90. package/src/rust/components/viewer/msg.rs +4 -0
  91. package/src/rust/components/viewer/panels.rs +41 -1
  92. package/src/rust/components/viewer/render.rs +1 -0
  93. package/src/rust/components/viewer/settings.rs +0 -22
  94. package/src/rust/components/viewer/snapshots.rs +2 -2
  95. package/src/rust/components/viewer/wiring.rs +14 -0
  96. package/src/rust/components/viewer.rs +45 -25
  97. package/src/rust/components/window_editor.rs +5 -4
  98. package/src/rust/config/column_config_schema.rs +38 -377
  99. package/src/rust/config/number_string_format.rs +0 -94
  100. package/src/rust/config/options.rs +3 -7
  101. package/src/rust/config/plugin_static_config.rs +0 -42
  102. package/src/rust/config/viewer_config.rs +47 -17
  103. package/src/rust/config/workspace_config.rs +158 -128
  104. package/src/rust/custom_elements/viewer.rs +187 -131
  105. package/src/rust/custom_events.rs +33 -18
  106. package/src/rust/derived.rs +266 -0
  107. package/src/rust/exprtk/tokenize.rs +0 -54
  108. package/src/rust/js/plugin.rs +1 -1
  109. package/src/rust/lib.rs +3 -0
  110. package/src/rust/presentation.rs +23 -4
  111. package/src/rust/queries/column_locator.rs +55 -48
  112. package/src/rust/queries/get_viewer_config.rs +1 -1
  113. package/src/rust/queries/plugin_column_styles.rs +22 -23
  114. package/src/rust/renderer/dispatch.rs +2 -0
  115. package/src/rust/renderer/plugin_config.rs +652 -487
  116. package/src/rust/renderer/props.rs +4 -0
  117. package/src/rust/renderer/registry.rs +2 -6
  118. package/src/rust/renderer/state.rs +205 -0
  119. package/src/rust/renderer.rs +201 -59
  120. package/src/rust/session/metadata.rs +30 -9
  121. package/src/rust/session/op_queue.rs +430 -0
  122. package/src/rust/session/panel_state.rs +327 -0
  123. package/src/rust/session/view_subscription.rs +42 -3
  124. package/src/rust/session.rs +900 -395
  125. package/src/rust/tasks/apply_global_filters.rs +49 -35
  126. package/src/rust/tasks/auto_pause.rs +22 -12
  127. package/src/rust/tasks/copy_export.rs +5 -0
  128. package/src/rust/tasks/create_panel.rs +34 -9
  129. package/src/rust/tasks/derive.rs +312 -0
  130. package/src/rust/tasks/mod.rs +6 -2
  131. package/src/rust/tasks/pipeline.rs +145 -89
  132. package/src/rust/tasks/reset_all.rs +31 -20
  133. package/src/rust/tasks/restore_derived.rs +200 -0
  134. package/src/rust/tasks/restore_panel.rs +153 -141
  135. package/src/rust/tasks/send_column_config.rs +56 -29
  136. package/src/rust/tasks/send_plugin_config.rs +52 -23
  137. package/src/rust/tasks/set_edit_mode.rs +33 -25
  138. package/src/rust/tasks/sync_update_panels.rs +39 -12
  139. package/src/rust/tasks/table_lifecycle.rs +70 -34
  140. package/src/rust/tasks/transactional_restore.rs +387 -0
  141. package/src/rust/tasks/update_theme.rs +45 -6
  142. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  143. package/src/rust/ui/containers/split_panel.rs +0 -41
  144. package/src/rust/ui/context_menu.rs +157 -18
  145. package/src/rust/ui/font_loader.rs +23 -35
  146. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  147. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  148. package/src/rust/ui/form/palette_selector.rs +0 -51
  149. package/src/rust/ui/portal.rs +4 -10
  150. package/src/rust/utils/browser/mod.rs +2 -3
  151. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  152. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  153. package/src/rust/utils/css.rs +0 -360
  154. package/src/rust/utils/local_fonts.rs +0 -36
  155. package/src/rust/utils/mod.rs +2 -3
  156. package/src/rust/utils/modal_position.rs +15 -21
  157. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  158. package/src/rust/utils/pubsub.rs +0 -1
  159. package/src/rust/workspace.rs +187 -140
  160. package/src/svg/derived.svg +6 -0
  161. package/src/themes/icons.css +1 -0
  162. package/src/themes/intl/ar.css +10 -0
  163. package/src/themes/intl/de.css +10 -0
  164. package/src/themes/intl/es.css +10 -0
  165. package/src/themes/intl/fr.css +10 -0
  166. package/src/themes/intl/ja.css +10 -0
  167. package/src/themes/intl/pt.css +10 -0
  168. package/src/themes/intl/zh.css +10 -0
  169. package/src/themes/intl.css +10 -0
  170. package/src/ts/extensions.ts +3 -36
  171. package/src/ts/perspective-viewer.ts +1 -1
  172. package/src/ts/plugin.ts +4 -1
  173. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  174. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  175. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  176. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  177. package/src/ts/ts-rs/TableRef.ts +9 -0
  178. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  179. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  180. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  181. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  182. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  183. package/dist/esm/perspective-viewer.inline.js +0 -21
  184. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  185. package/src/rust/tasks/restore_and_render.rs +0 -107
  186. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  187. package/src/rust/utils/tests/mod.rs +0 -14
  188. package/src/rust/utils/tests/pubsub.rs +0 -95
  189. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  190. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  191. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  192. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  193. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  194. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -61,6 +61,14 @@ impl ColumnConfigSchema {
61
61
  }
62
62
  }
63
63
 
64
+ /// A per-column statistic a [`ControlSpec::Number`] may take its default from.
65
+ #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
66
+ #[serde(rename_all = "snake_case")]
67
+ pub enum NumberStat {
68
+ /// The largest absolute value in the column.
69
+ AbsMax,
70
+ }
71
+
64
72
  /// Discriminated union of widget kinds the viewer can render. Composite
65
73
  /// variants wrap an existing rich Yew component and carry only the
66
74
  /// component's `*DefaultConfig`. Primitive variants render generic scalar
@@ -126,6 +134,11 @@ pub enum ControlSpec {
126
134
 
127
135
  #[serde(default, skip_serializing_if = "Option::is_none")]
128
136
  step: Option<f64>,
137
+
138
+ /// The column statistic the HOST substitutes for `default`, once it has
139
+ /// fetched it.
140
+ #[serde(default, skip_serializing_if = "Option::is_none")]
141
+ default_stat: Option<NumberStat>,
129
142
  },
130
143
  String {
131
144
  key: String,
@@ -311,6 +324,31 @@ impl ColumnConfigSchema {
311
324
  self.canonicalize_defaults().group_format_controls()
312
325
  }
313
326
 
327
+ /// Replace the `default` of every [`ControlSpec::Number`] that declares a
328
+ /// `default_stat` with that statistic, where the host has it.
329
+ pub fn resolve_stat_defaults(mut self, abs_max: Option<f64>) -> Self {
330
+ fn walk(fields: &mut [ControlSpec], abs_max: Option<f64>) {
331
+ for spec in fields {
332
+ match spec {
333
+ ControlSpec::Group { fields, .. } => walk(fields, abs_max),
334
+ ControlSpec::Number {
335
+ default,
336
+ default_stat: Some(NumberStat::AbsMax),
337
+ ..
338
+ } => {
339
+ if let Some(abs_max) = abs_max {
340
+ *default = abs_max;
341
+ }
342
+ },
343
+ _ => {},
344
+ }
345
+ }
346
+ }
347
+
348
+ walk(&mut self.fields, abs_max);
349
+ self
350
+ }
351
+
314
352
  pub fn group_format_controls(mut self) -> Self {
315
353
  fn is_format(spec: &ControlSpec) -> bool {
316
354
  matches!(
@@ -485,380 +523,3 @@ pub fn filter_to_schema(
485
523
  .map(|(k, v)| (k.clone(), v.clone()))
486
524
  .collect()
487
525
  }
488
-
489
- #[cfg(test)]
490
- mod tests {
491
- use serde_json::json;
492
-
493
- use super::*;
494
-
495
- fn color(key: &str, default: &str) -> ControlSpec {
496
- ControlSpec::Color {
497
- key: key.to_owned(),
498
- default: default.to_owned(),
499
- }
500
- }
501
-
502
- fn flag(key: &str) -> ControlSpec {
503
- ControlSpec::Bool {
504
- key: key.to_owned(),
505
- default: false,
506
- }
507
- }
508
-
509
- fn group(key: &str, fields: Vec<ControlSpec>) -> ControlSpec {
510
- ControlSpec::Group {
511
- key: key.to_owned(),
512
- fields,
513
- }
514
- }
515
-
516
- #[test]
517
- fn group_deserializes_recursively() {
518
- let schema: ColumnConfigSchema = serde_json::from_value(json!({
519
- "fields": [{
520
- "kind": "Group",
521
- "key": "legend",
522
- "fields": [
523
- { "kind": "Bool", "key": "legend_on", "default": false },
524
- {
525
- "kind": "Group",
526
- "key": "inner",
527
- "fields": [{ "kind": "Color", "key": "color", "default": "#ff0000" }]
528
- }
529
- ]
530
- }]
531
- }))
532
- .unwrap();
533
-
534
- let keys = schema.active_keys();
535
- assert_eq!(
536
- keys,
537
- HashSet::from(["legend_on".to_owned(), "color".to_owned()])
538
- );
539
-
540
- let leaves = schema.leaf_fields();
541
- assert_eq!(leaves.len(), 2);
542
- assert!(
543
- leaves
544
- .iter()
545
- .all(|s| !matches!(s, ControlSpec::Group { .. }))
546
- );
547
- }
548
-
549
- #[test]
550
- fn grouped_schema_is_equivalent_to_flat() {
551
- let flat = ColumnConfigSchema {
552
- fields: vec![flag("stack"), color("color", "#0366d6")],
553
- };
554
-
555
- let grouped = ColumnConfigSchema {
556
- fields: vec![group("series", vec![
557
- flag("stack"),
558
- color("color", "#0366d6"),
559
- ])],
560
- };
561
-
562
- assert_eq!(flat.active_keys(), grouped.active_keys());
563
- assert_eq!(flat.css_kind_of("color"), grouped.css_kind_of("color"));
564
- assert_eq!(flat.css_kind_of("stack"), grouped.css_kind_of("stack"));
565
- }
566
-
567
- #[test]
568
- fn enum_variants_see_through_groups() {
569
- let mode = ControlSpec::Enum {
570
- key: "fg_mode".to_owned(),
571
- default: "color".to_owned(),
572
- variants: vec![
573
- EnumVariant {
574
- value: "disabled".to_owned(),
575
- label: None,
576
- },
577
- EnumVariant {
578
- value: "color".to_owned(),
579
- label: None,
580
- },
581
- ],
582
- };
583
-
584
- let grouped = ColumnConfigSchema {
585
- fields: vec![group("color", vec![mode, flag("flag")])],
586
- };
587
-
588
- let values: Vec<&str> = grouped
589
- .enum_variants_of("fg_mode")
590
- .unwrap()
591
- .iter()
592
- .map(|v| v.value.as_str())
593
- .collect();
594
-
595
- assert_eq!(values, vec!["disabled", "color"]);
596
- assert!(grouped.enum_variants_of("flag").is_none());
597
- assert!(grouped.enum_variants_of("missing").is_none());
598
- }
599
-
600
- #[test]
601
- fn format_controls_group_and_merge() {
602
- let schema = ColumnConfigSchema {
603
- fields: vec![
604
- ControlSpec::NumberFormat { default: None },
605
- flag("flag"),
606
- ControlSpec::DatetimeFormat { default: None },
607
- ],
608
- }
609
- .group_format_controls();
610
-
611
- assert_eq!(schema.fields.len(), 2);
612
- let ControlSpec::Group { key, fields } = &schema.fields[0] else {
613
- panic!("expected format group first");
614
- };
615
-
616
- assert_eq!(key, "format");
617
- assert!(matches!(fields[0], ControlSpec::NumberFormat { .. }));
618
- assert!(matches!(fields[1], ControlSpec::DatetimeFormat { .. }));
619
- assert!(matches!(&schema.fields[1], ControlSpec::Bool { .. }));
620
-
621
- assert_eq!(
622
- schema.active_keys(),
623
- HashSet::from([
624
- "number_format".to_owned(),
625
- "date_format".to_owned(),
626
- "flag".to_owned()
627
- ])
628
- );
629
- }
630
-
631
- #[test]
632
- fn font_owns_its_size_and_toggle_keys() {
633
- let spec = ControlSpec::Font {
634
- key: "font_family".to_owned(),
635
- default: "inherit".to_owned(),
636
- size: Some(FontSize {
637
- key: "font_size".to_owned(),
638
- default: 12.0,
639
- min: None,
640
- max: None,
641
- step: None,
642
- }),
643
- bold: Some(FontToggle {
644
- key: "bold".to_owned(),
645
- default: false,
646
- }),
647
- italic: None,
648
- };
649
-
650
- assert_eq!(spec.serialized_keys(), vec![
651
- "font_family",
652
- "font_size",
653
- "bold"
654
- ]);
655
- }
656
-
657
- #[test]
658
- fn format_grouping_recurses_but_never_double_wraps() {
659
- let schema = ColumnConfigSchema {
660
- fields: vec![
661
- group("format", vec![ControlSpec::NumberFormat { default: None }]),
662
- group("styling", vec![flag("x"), ControlSpec::DatetimeFormat {
663
- default: None,
664
- }]),
665
- ],
666
- }
667
- .group_format_controls();
668
-
669
- let ControlSpec::Group { key, fields } = &schema.fields[0] else {
670
- panic!("expected group");
671
- };
672
-
673
- assert_eq!(key, "format");
674
- assert!(matches!(fields[0], ControlSpec::NumberFormat { .. }));
675
-
676
- let ControlSpec::Group { fields, .. } = &schema.fields[1] else {
677
- panic!("expected group");
678
- };
679
-
680
- assert!(matches!(
681
- &fields[1],
682
- ControlSpec::Group { key, fields }
683
- if key == "format" && matches!(fields[0], ControlSpec::DatetimeFormat { .. })
684
- ));
685
- }
686
-
687
- #[test]
688
- fn format_controls_deserialize_without_default_payload() {
689
- let schema: ColumnConfigSchema = serde_json::from_value(json!({
690
- "fields": [{ "kind": "NumberFormat" }, { "kind": "DatetimeFormat" }]
691
- }))
692
- .unwrap();
693
-
694
- assert!(matches!(&schema.fields[0], ControlSpec::NumberFormat {
695
- default: None
696
- }));
697
- assert!(matches!(&schema.fields[1], ControlSpec::DatetimeFormat {
698
- default: None
699
- }));
700
- }
701
-
702
- #[test]
703
- fn number_format_default_payload_deserializes_flattened_families() {
704
- let schema: ColumnConfigSchema = serde_json::from_value(json!({
705
- "fields": [{
706
- "kind": "NumberFormat",
707
- "default": {
708
- "notation": "compact",
709
- "compactDisplay": "short",
710
- "minimumFractionDigits": 0,
711
- "maximumFractionDigits": 1
712
- }
713
- }]
714
- }))
715
- .unwrap();
716
-
717
- let ControlSpec::NumberFormat {
718
- default: Some(default),
719
- } = &schema.fields[0]
720
- else {
721
- panic!("expected NumberFormat with default");
722
- };
723
-
724
- assert_eq!(
725
- default._notation,
726
- Some(crate::config::Notation::Compact(
727
- crate::config::CompactDisplay::Short
728
- ))
729
- );
730
- assert_eq!(default._style, None);
731
- assert_eq!(default.minimum_fraction_digits, Some(0.));
732
- assert_eq!(default.maximum_fraction_digits, Some(1.));
733
- assert_eq!(
734
- schema.active_keys(),
735
- HashSet::from(["number_format".to_owned()])
736
- );
737
- }
738
-
739
- #[test]
740
- fn datetime_format_default_payload_deserializes_simple_arm() {
741
- let schema: ColumnConfigSchema = serde_json::from_value(json!({
742
- "fields": [{
743
- "kind": "DatetimeFormat",
744
- "default": { "dateStyle": "medium", "timeStyle": "disabled" }
745
- }]
746
- }))
747
- .unwrap();
748
-
749
- let ControlSpec::DatetimeFormat {
750
- default: Some(DatetimeFormatType::Simple(simple)),
751
- } = &schema.fields[0]
752
- else {
753
- panic!("expected DatetimeFormat with Simple default");
754
- };
755
-
756
- assert_eq!(
757
- simple.date_style,
758
- crate::config::SimpleDatetimeFormat::Medium
759
- );
760
- assert_eq!(
761
- simple.time_style,
762
- crate::config::SimpleDatetimeFormat::Disabled
763
- );
764
- }
765
-
766
- #[test]
767
- fn alignment_tokens_round_trip() {
768
- for align in Alignment::ALL {
769
- assert_eq!(Alignment::parse(align.as_str()), Some(align));
770
- assert_eq!(serde_json::to_value(align).unwrap(), json!(align.as_str()));
771
- }
772
-
773
- for bad in ["middle", "left-top", "top-center", "middle-left", ""] {
774
- assert_eq!(Alignment::parse(bad), None, "{bad}");
775
- }
776
-
777
- assert_eq!(Alignment::ALL.iter().filter(|x| x.is_corner()).count(), 4);
778
- assert_eq!(Alignment::TopLeft.humanized(), "Top Left");
779
- assert_eq!(Alignment::Center.humanized(), "Center");
780
- }
781
-
782
- #[test]
783
- fn alignment_deserializes_with_optional_default_and_corners() {
784
- let schema: ColumnConfigSchema = serde_json::from_value(json!({
785
- "fields": [
786
- { "kind": "Alignment", "key": "align" },
787
- { "kind": "Alignment", "key": "legend_anchor", "default": "top-right", "corners": true }
788
- ]
789
- }))
790
- .unwrap();
791
-
792
- assert!(matches!(&schema.fields[0], ControlSpec::Alignment {
793
- default: None,
794
- corners: false,
795
- ..
796
- }));
797
- assert!(matches!(&schema.fields[1], ControlSpec::Alignment {
798
- default: Some(Alignment::TopRight),
799
- corners: true,
800
- ..
801
- }));
802
- assert_eq!(
803
- schema.active_keys(),
804
- HashSet::from(["align".to_owned(), "legend_anchor".to_owned()])
805
- );
806
- assert!(
807
- serde_json::from_value::<ColumnConfigSchema>(json!({
808
- "fields": [{ "kind": "Alignment", "key": "x", "default": "middle" }]
809
- }))
810
- .is_err()
811
- );
812
- }
813
-
814
- #[test]
815
- fn canonicalize_defaults_drops_non_corner_default_of_corners_field() {
816
- let schema = ColumnConfigSchema {
817
- fields: vec![
818
- ControlSpec::Alignment {
819
- key: "bad".to_owned(),
820
- default: Some(Alignment::Top),
821
- corners: true,
822
- },
823
- ControlSpec::Alignment {
824
- key: "ok".to_owned(),
825
- default: Some(Alignment::Top),
826
- corners: false,
827
- },
828
- ControlSpec::Alignment {
829
- key: "unset".to_owned(),
830
- default: None,
831
- corners: true,
832
- },
833
- ],
834
- }
835
- .canonicalize_defaults();
836
-
837
- assert_eq!(
838
- schema.active_keys(),
839
- HashSet::from(["ok".to_owned(), "unset".to_owned()])
840
- );
841
- }
842
-
843
- #[test]
844
- fn canonicalize_defaults_recurses_and_drops_empty_groups() {
845
- let schema = ColumnConfigSchema {
846
- fields: vec![
847
- group("ok", vec![color("good", "RGB(255,0,0)"), flag("flag")]),
848
- group("doomed", vec![color("bad", "not-a-color")]),
849
- ],
850
- }
851
- .canonicalize_defaults();
852
-
853
- assert_eq!(schema.fields.len(), 1);
854
- let ControlSpec::Group { key, fields } = &schema.fields[0] else {
855
- panic!("expected group");
856
- };
857
-
858
- assert_eq!(key, "ok");
859
- assert!(matches!(
860
- &fields[0],
861
- ControlSpec::Color { default, .. } if default == "#ff0000"
862
- ));
863
- }
864
- }
@@ -422,97 +422,3 @@ impl CustomNumberFormatConfig {
422
422
  }
423
423
  }
424
424
  }
425
-
426
- #[cfg(test)]
427
- mod tests {
428
- use super::*;
429
-
430
- fn sig(min: f64, max: f64) -> CustomNumberFormatConfig {
431
- CustomNumberFormatConfig {
432
- minimum_significant_digits: Some(min),
433
- maximum_significant_digits: Some(max),
434
- ..Default::default()
435
- }
436
- }
437
-
438
- #[test]
439
- fn resolve_overlays_partial_default_on_builtin() {
440
- let spec = CustomNumberFormatConfig {
441
- _notation: Some(Notation::Compact(CompactDisplay::Short)),
442
- minimum_fraction_digits: Some(0.),
443
- maximum_fraction_digits: Some(1.),
444
- ..Default::default()
445
- };
446
-
447
- let defaults = NumberFormatDefaults::resolve(Some(&spec), true);
448
- assert_eq!(defaults.notation, Notation::Compact(CompactDisplay::Short));
449
- assert_eq!(defaults.fraction, (0., 1.));
450
- assert_eq!(defaults.style, NumberFormatStyle::Decimal);
451
- assert_eq!(defaults.significant, (1., 21.));
452
- assert_eq!(
453
- NumberFormatDefaults::resolve(None, true),
454
- NumberFormatDefaults::builtin(true)
455
- );
456
- }
457
-
458
- #[test]
459
- fn filter_preserves_max_significant_digits() {
460
- let filtered = sig(1., 5.).filter_default(&NumberFormatDefaults::builtin(true));
461
- assert_eq!(filtered.minimum_significant_digits, Some(1.));
462
- assert_eq!(filtered.maximum_significant_digits, Some(5.));
463
-
464
- let filtered = sig(3., 21.).filter_default(&NumberFormatDefaults::builtin(true));
465
- assert_eq!(filtered.minimum_significant_digits, Some(3.));
466
- assert_eq!(filtered.maximum_significant_digits, Some(21.));
467
- }
468
-
469
- #[test]
470
- fn filter_elides_values_equal_to_resolved_defaults() {
471
- let defaults = NumberFormatDefaults {
472
- fraction: (0., 1.),
473
- notation: Notation::Compact(CompactDisplay::Short),
474
- ..NumberFormatDefaults::builtin(true)
475
- };
476
-
477
- let config = CustomNumberFormatConfig {
478
- _notation: Some(Notation::Compact(CompactDisplay::Short)),
479
- minimum_fraction_digits: Some(0.),
480
- maximum_fraction_digits: Some(1.),
481
- ..Default::default()
482
- };
483
-
484
- assert_eq!(
485
- config.filter_default(&defaults),
486
- CustomNumberFormatConfig::default()
487
- );
488
- }
489
-
490
- #[test]
491
- fn filter_serializes_builtin_values_under_override() {
492
- let defaults = NumberFormatDefaults {
493
- notation: Notation::Compact(CompactDisplay::Short),
494
- ..NumberFormatDefaults::builtin(true)
495
- };
496
-
497
- let config = CustomNumberFormatConfig {
498
- _notation: Some(Notation::Standard),
499
- ..Default::default()
500
- };
501
-
502
- let filtered = config.filter_default(&defaults);
503
- assert_eq!(filtered._notation, Some(Notation::Standard));
504
- }
505
-
506
- #[test]
507
- fn filter_bakes_fraction_digits_for_percent_style() {
508
- let config = CustomNumberFormatConfig {
509
- _style: Some(NumberFormatStyle::Percent),
510
- ..Default::default()
511
- };
512
-
513
- let filtered = config.filter_default(&NumberFormatDefaults::builtin(true));
514
- assert_eq!(filtered._style, Some(NumberFormatStyle::Percent));
515
- assert_eq!(filtered.minimum_fraction_digits, Some(2.));
516
- assert_eq!(filtered.maximum_fraction_digits, Some(2.));
517
- }
518
- }
@@ -35,13 +35,9 @@ pub struct RestoreOptions {
35
35
  #[ts(optional)]
36
36
  pub panel: Option<String>,
37
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.
38
+ /// When `true`, a config that was applied and then failed to render only
39
+ /// rejects the returned `Promise`, without raising the panel's visible
40
+ /// error state.
45
41
  #[ts(optional)]
46
42
  pub suppress_errors: Option<bool>,
47
43
 
@@ -215,45 +215,3 @@ impl PluginStaticConfig {
215
215
  .unwrap_or_default()
216
216
  }
217
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
- }
259
- }