@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.
- package/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/docs/perspective-docs.json +1 -0
- package/dist/esm/column-format.d.ts +19 -28
- package/dist/esm/perspective-viewer.d.ts +2 -0
- package/dist/esm/perspective-viewer.inline.js +8 -8
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/providers.d.ts +79 -0
- package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
- package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
- package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
- package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
- package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
- package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
- package/dist/esm/ts-rs/Notation.d.ts +17 -0
- package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -1
- package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
- package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
- package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
- package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
- package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
- package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/Unit.d.ts +1 -0
- package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
- package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +41 -1
- package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
- package/dist/wasm/perspective-viewer.d.ts +134 -25
- package/dist/wasm/perspective-viewer.js +367 -124
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
- package/package.json +2 -1
- package/src/css/_viewer_bundle.css +1 -0
- package/src/css/column-selector.css +8 -0
- package/src/css/config-selector.css +38 -3
- package/src/css/form/code-editor.css +2 -3
- package/src/css/form/mirrored-textarea.css +81 -0
- package/src/css/viewer.css +328 -0
- package/src/rust/agent/client/mod.rs +19 -0
- package/src/rust/agent/client/protocol.rs +320 -0
- package/src/rust/agent/client/transport.rs +287 -0
- package/src/rust/agent/client/turn.rs +174 -0
- package/src/rust/agent/config.rs +159 -0
- package/src/rust/agent/docs.rs +293 -0
- package/src/rust/agent/markdown/block.rs +509 -0
- package/src/rust/agent/markdown/entity.rs +52 -0
- package/src/rust/agent/markdown/events.rs +71 -0
- package/src/rust/agent/markdown/inline.rs +497 -0
- package/src/rust/agent/markdown/mod.rs +466 -0
- package/src/rust/agent/mod.rs +23 -0
- package/src/rust/agent/preamble.md +16 -0
- package/src/rust/agent/runtime.rs +201 -0
- package/src/rust/agent/slot.rs +236 -0
- package/src/rust/agent/tools.rs +1110 -0
- package/src/rust/components/chat_panel.rs +289 -0
- package/src/rust/components/column_selector/config_selector.rs +76 -7
- package/src/rust/components/column_selector.rs +7 -0
- package/src/rust/components/containers/dragdrop_list.rs +37 -0
- package/src/rust/components/form/code_editor.rs +14 -23
- package/src/rust/components/form/debug.rs +8 -5
- package/src/rust/components/form/mirrored_textarea.rs +165 -0
- package/src/rust/components/form/mod.rs +1 -0
- package/src/rust/components/mod.rs +2 -0
- package/src/rust/components/settings_panel.rs +80 -2
- package/src/rust/components/viewer/panels.rs +9 -14
- package/src/rust/components/viewer/render.rs +2 -0
- package/src/rust/components/window_editor.rs +75 -103
- package/src/rust/config/datetime_column_style/custom.rs +17 -0
- package/src/rust/config/datetime_column_style/simple.rs +10 -0
- package/src/rust/config/number_string_format.rs +33 -0
- package/src/rust/config/options.rs +18 -0
- package/src/rust/config/plugin_static_config.rs +120 -5
- package/src/rust/config/viewer_config.rs +220 -1
- package/src/rust/config/workspace_config.rs +7 -6
- package/src/rust/custom_elements/viewer.rs +209 -18
- package/src/rust/custom_events.rs +29 -0
- package/src/rust/lib.rs +13 -0
- package/src/rust/presentation.rs +8 -0
- package/src/rust/queries/get_viewer_config.rs +5 -1
- package/src/rust/session/column_defaults_update.rs +18 -0
- package/src/rust/session/metadata.rs +18 -20
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/tasks/create_panel.rs +18 -8
- package/src/rust/tasks/edit_window.rs +1 -1
- package/src/rust/tasks/restore_panel.rs +34 -17
- package/src/rust/tasks/sync_update_panels.rs +7 -23
- package/src/rust/workspace.rs +112 -20
- package/src/themes/intl/de.css +16 -0
- package/src/themes/intl/es.css +16 -0
- package/src/themes/intl/fr.css +16 -0
- package/src/themes/intl/ja.css +16 -0
- package/src/themes/intl/pt.css +16 -0
- package/src/themes/intl/zh.css +16 -0
- package/src/themes/intl.css +16 -0
- package/src/ts/column-format.ts +67 -46
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/providers.ts +96 -0
- package/src/ts/ts-rs/CompactDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyCode.ts +3 -0
- package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
- package/src/ts/ts-rs/CurrencySign.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
- package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
- package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
- package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
- package/src/ts/ts-rs/FormatMode.ts +3 -0
- package/src/ts/ts-rs/FormatUnit.ts +3 -0
- package/src/ts/ts-rs/Notation.ts +11 -0
- package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +47 -1
- package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
- package/src/ts/ts-rs/RestoreOptions.ts +20 -0
- package/src/ts/ts-rs/RoundingMode.ts +3 -0
- package/src/ts/ts-rs/RoundingPriority.ts +3 -0
- package/src/ts/ts-rs/SignDisplay.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
- package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
- package/src/ts/ts-rs/StringColorMode.ts +3 -0
- package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
- package/src/ts/ts-rs/Unit.ts +3 -0
- package/src/ts/ts-rs/UnitDisplay.ts +3 -0
- package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
- package/src/ts/ts-rs/UseGrouping.ts +3 -0
- package/src/ts/ts-rs/ViewerConfig.ts +47 -1
- package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
- package/src/ts/ts-rs/WindowSpec.ts +3 -2
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
- package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
- package/src/ts/ts-rs/WindowAggregate.ts +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
- /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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
.
|
|
252
|
-
.
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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 = (
|
|
334
|
-
let 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 !
|
|
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 =
|
|
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
|
|
702
|
-
//
|
|
703
|
-
// rest).
|
|
704
|
-
if
|
|
705
|
-
self.draft.
|
|
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
|
|
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(
|
|
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
|
|
786
|
-
// and `SetOp` already coerces the draft
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|