Skip to content

Commit 6878090

Browse files
committed
Add viewer support for chart per-column config
Signed-off-by: Andrew Stein <steinlink@gmail.com>
1 parent 244bd76 commit 6878090

25 files changed

Lines changed: 613 additions & 613 deletions

rust/perspective-viewer/src/css/column-settings-panel.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@
139139
content: var(--psp-label--background--content, "Background");
140140
}
141141

142+
label#chart-type-label:before {
143+
content: var(--psp-label--chart-type--content, "Chart Type");
144+
}
145+
146+
label#stack-label:before {
147+
content: var(--psp-label--stack--content, "Stack");
148+
}
149+
142150
label#series-label:before {
143151
content: var(--psp-label--series--content, "Series");
144152
}

rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ mod stub;
1515
mod symbol;
1616

1717
use itertools::Itertools;
18-
use perspective_client::config::ColumnType;
18+
use perspective_client::config::{ColumnType, GroupRollupMode};
1919
use yew::{Html, Properties, function_component, html};
2020

2121
use self::agg_depth_selector::*;
2222
use crate::components::column_settings_sidebar::style_tab::stub::Stub;
2323
use crate::components::column_settings_sidebar::style_tab::symbol::SymbolStyle;
2424
use crate::components::datetime_column_style::DatetimeColumnStyle;
2525
use crate::components::number_column_style::NumberColumnStyle;
26+
use crate::components::number_series_style::NumberSeriesStyle;
2627
use crate::components::string_column_style::StringColumnStyle;
2728
use crate::components::style_controls::CustomNumberFormat;
2829
use crate::custom_events::CustomEvents;
@@ -99,7 +100,13 @@ pub fn StyleTab(props: &StyleTabProps) -> Html {
99100
)
100101
.map(|opts| {
101102
let mut components = vec![];
102-
if !props.view_config.group_by.is_empty() {
103+
// Aggregate Depth only applies when group_rollup_mode is Rollup —
104+
// Flat emits only leaves (depth == group_by.len()) and Total emits
105+
// only the grand-total (depth 0), so no depth is selectable. The
106+
// stored value persists in ColumnConfigValues across mode changes
107+
// and resurfaces when the user returns to Rollup.
108+
let is_rollup = props.view_config.group_rollup_mode == GroupRollupMode::Rollup;
109+
if !props.view_config.group_by.is_empty() && is_rollup {
103110
let aggregate_depth = config.as_ref().map(|x| x.aggregate_depth as f64);
104111
components.push(("Aggregate Depth", html! {
105112
<AggregateDepthSelector
@@ -126,6 +133,15 @@ pub fn StyleTab(props: &StyleTabProps) -> Html {
126133
/>
127134
}));
128135
}
136+
if let Some(default_config) = opts.number_series_style {
137+
let config = config
138+
.as_ref()
139+
.map(|config| config.number_series_style.clone());
140+
141+
components.push(("Chart Type", html! {
142+
<NumberSeriesStyle {config} {default_config} on_change={on_change.clone()} />
143+
}));
144+
}
129145
if let Some(default_config) = opts.datagrid_string_style {
130146
let config = config
131147
.as_ref()

rust/perspective-viewer/src/rust/components/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub mod function_dropdown;
3131
pub mod main_panel;
3232
pub mod modal;
3333
pub mod number_column_style;
34+
pub mod number_series_style;
3435
pub mod plugin_selector;
3536
pub mod portal;
3637
pub mod render_warning;
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
use web_sys::{HtmlInputElement, InputEvent};
14+
use yew::prelude::*;
15+
16+
use super::modal::{ModalLink, SetModalLink};
17+
use super::style::LocalStyle;
18+
use crate::components::form::select_enum_field::SelectEnumField;
19+
use crate::config::*;
20+
use crate::css;
21+
use crate::utils::WeakScope;
22+
23+
#[derive(Properties)]
24+
pub struct NumberSeriesStyleProps {
25+
pub config: Option<NumberSeriesStyleConfig>,
26+
pub default_config: NumberSeriesStyleDefaultConfig,
27+
28+
#[prop_or_default]
29+
pub on_change: Callback<ColumnConfigValueUpdate>,
30+
31+
#[prop_or_default]
32+
weak_link: WeakScope<NumberSeriesStyle>,
33+
}
34+
35+
impl ModalLink<NumberSeriesStyle> for NumberSeriesStyleProps {
36+
fn weak_link(&self) -> &'_ WeakScope<NumberSeriesStyle> {
37+
&self.weak_link
38+
}
39+
}
40+
41+
impl PartialEq for NumberSeriesStyleProps {
42+
fn eq(&self, other: &Self) -> bool {
43+
self.config == other.config && self.default_config == other.default_config
44+
}
45+
}
46+
47+
pub enum NumberSeriesStyleMsg {
48+
ChartTypeChanged(Option<ChartType>),
49+
StackChanged(Option<bool>),
50+
}
51+
52+
/// Form control for the per-column `chart_type` + `stack` picker. Rendered
53+
/// inside the column-settings sidebar when the active plugin returns a
54+
/// `NumberSeriesStyleDefaultConfig` from its `column_style_controls` hook.
55+
pub struct NumberSeriesStyle {
56+
config: NumberSeriesStyleConfig,
57+
}
58+
59+
impl Component for NumberSeriesStyle {
60+
type Message = NumberSeriesStyleMsg;
61+
type Properties = NumberSeriesStyleProps;
62+
63+
fn create(ctx: &Context<Self>) -> Self {
64+
ctx.set_modal_link();
65+
Self {
66+
config: ctx.props().config.clone().unwrap_or_default(),
67+
}
68+
}
69+
70+
fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
71+
let new_config = ctx.props().config.clone().unwrap_or_default();
72+
if self.config != new_config {
73+
self.config = new_config;
74+
true
75+
} else {
76+
false
77+
}
78+
}
79+
80+
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
81+
match msg {
82+
NumberSeriesStyleMsg::ChartTypeChanged(val) => {
83+
self.config.chart_type = val.unwrap_or_default();
84+
// Hiding the stack checkbox on Line/Scatter also clears any
85+
// lingering override so the JSON stays empty by default.
86+
if !self.config.chart_type.supports_stack() {
87+
self.config.stack = None;
88+
}
89+
self.dispatch_config(ctx);
90+
true
91+
},
92+
NumberSeriesStyleMsg::StackChanged(val) => {
93+
self.config.stack = val;
94+
self.dispatch_config(ctx);
95+
true
96+
},
97+
}
98+
}
99+
100+
fn view(&self, ctx: &Context<Self>) -> Html {
101+
let chart_type_changed = ctx.link().callback(NumberSeriesStyleMsg::ChartTypeChanged);
102+
103+
let stack_controls = if self.config.chart_type.supports_stack() {
104+
// Default: bar/area stack. `None` == inherit the default.
105+
let checked = self.config.stack.unwrap_or(true);
106+
let oninput = ctx.link().callback(move |e: InputEvent| {
107+
let input: HtmlInputElement = e.target_unchecked_into();
108+
let next = input.checked();
109+
// Persist explicit `false` overrides; the "stacked" default
110+
// round-trips as `None` to keep JSON empty.
111+
NumberSeriesStyleMsg::StackChanged(if next { None } else { Some(false) })
112+
});
113+
html! {
114+
<div class="row">
115+
<label id="stack-label" />
116+
<input type="checkbox" id="stack-checkbox" {checked} {oninput} />
117+
</div>
118+
}
119+
} else {
120+
html! {}
121+
};
122+
123+
html! {
124+
<>
125+
<LocalStyle href={css!("column-style")} />
126+
<div id="column-style-container" class="number-series-style-container">
127+
<SelectEnumField<ChartType>
128+
label="chart-type"
129+
on_change={chart_type_changed}
130+
current_value={self.config.chart_type}
131+
/>
132+
{ stack_controls }
133+
</div>
134+
</>
135+
}
136+
}
137+
}
138+
139+
impl NumberSeriesStyle {
140+
/// Dispatch the current config as an update. When the config matches
141+
/// the default (Bar + no stack override), send `None` so the field is
142+
/// omitted entirely from the serialized `ColumnConfigValues`.
143+
fn dispatch_config(&self, ctx: &Context<Self>) {
144+
let update = Some(self.config.clone()).filter(|c| c != &NumberSeriesStyleConfig::default());
145+
ctx.props()
146+
.on_change
147+
.emit(ColumnConfigValueUpdate::NumberSeriesStyle(update));
148+
}
149+
}

rust/perspective-viewer/src/rust/config/columns_config.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use ts_rs::TS;
1717

1818
use super::{
1919
CustomNumberFormatConfig, DatetimeColumnStyleConfig, DatetimeColumnStyleDefaultConfig,
20-
NumberColumnStyleConfig, NumberColumnStyleDefaultConfig, StringColumnStyleConfig,
21-
StringColumnStyleDefaultConfig,
20+
NumberColumnStyleConfig, NumberColumnStyleDefaultConfig, NumberSeriesStyleConfig,
21+
NumberSeriesStyleDefaultConfig, StringColumnStyleConfig, StringColumnStyleDefaultConfig,
2222
};
2323

2424
fn is_zero(x: &u32) -> bool {
@@ -42,6 +42,12 @@ pub struct ColumnConfigValues {
4242
#[serde(flatten)]
4343
pub datagrid_datetime_style: DatetimeColumnStyleConfig,
4444

45+
/// Per-column render-glyph config consumed by the Y Bar plugin's
46+
/// `columns_config["<agg>"].chart_type` router. Flattened at the
47+
/// JSON boundary; default `{ chart_type: Bar }` serializes empty.
48+
#[serde(flatten)]
49+
pub number_series_style: NumberSeriesStyleConfig,
50+
4551
#[serde(default)]
4652
#[serde(skip_serializing_if = "Option::is_none")]
4753
pub number_format: Option<CustomNumberFormatConfig>,
@@ -59,6 +65,7 @@ pub enum ColumnConfigValueUpdate {
5965
Symbols(Option<HashMap<String, String>>),
6066
CustomNumberStringFormat(Option<CustomNumberFormatConfig>),
6167
AggregateDepth(u32),
68+
NumberSeriesStyle(Option<NumberSeriesStyleConfig>),
6269
}
6370

6471
impl ColumnConfigValues {
@@ -88,6 +95,10 @@ impl ColumnConfigValues {
8895
aggregate_depth,
8996
..self
9097
},
98+
ColumnConfigValueUpdate::NumberSeriesStyle(update) => Self {
99+
number_series_style: update.unwrap_or_default(),
100+
..self
101+
},
91102
}
92103
}
93104

@@ -108,6 +119,10 @@ pub struct ColumnStyleOpts {
108119
pub datagrid_datetime_style: Option<DatetimeColumnStyleDefaultConfig>,
109120
pub symbols: Option<KeyValueOpts>,
110121
pub number_string_format: Option<bool>,
122+
123+
/// Y-series render-glyph picker (Chart Type). Populated by the Y Bar
124+
/// plugin for numeric aggregate columns.
125+
pub number_series_style: Option<NumberSeriesStyleDefaultConfig>,
111126
}
112127

113128
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]

rust/perspective-viewer/src/rust/config/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod columns_config;
1717
mod datetime_column_style;
1818
mod kvpair;
1919
mod number_column_style;
20+
mod number_series_style;
2021
mod number_string_format;
2122
mod string_column_style;
2223
pub mod view_config;
@@ -25,6 +26,7 @@ mod viewer_config;
2526
pub use columns_config::*;
2627
pub use datetime_column_style::*;
2728
pub use number_column_style::*;
29+
pub use number_series_style::*;
2830
pub use number_string_format::*;
2931
pub use string_column_style::*;
3032
pub use view_config::*;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
use serde::{Deserialize, Serialize};
14+
use strum::{Display, EnumIter};
15+
use ts_rs::TS;
16+
17+
/// Render glyph for a numeric aggregate in the Y Bar plugin. Serialized as
18+
/// a lowercase string to match the plugin's runtime lookup (which does a
19+
/// case-insensitive `chart_type` match against the same literal set).
20+
#[derive(
21+
Clone, Copy, Debug, Default, Deserialize, Display, EnumIter, Eq, PartialEq, Serialize, TS,
22+
)]
23+
pub enum ChartType {
24+
#[default]
25+
#[serde(rename = "bar")]
26+
Bar,
27+
28+
#[serde(rename = "line")]
29+
Line,
30+
31+
#[serde(rename = "scatter")]
32+
Scatter,
33+
34+
#[serde(rename = "area")]
35+
Area,
36+
}
37+
38+
impl ChartType {
39+
pub fn is_default(&self) -> bool {
40+
*self == Self::Bar
41+
}
42+
43+
/// Glyphs for which `stack` is a meaningful option. Line/Scatter never
44+
/// stack; Bar/Area stack by default but can be opted out per column.
45+
pub fn supports_stack(&self) -> bool {
46+
matches!(self, Self::Bar | Self::Area)
47+
}
48+
}
49+
50+
/// Per-column render-style config for numeric aggregates in series charts
51+
/// (currently Y Bar). Flattened into `ColumnConfigValues`, so the JSON
52+
/// shape at the viewer boundary is `{ "chart_type": "line", "stack": false }`.
53+
///
54+
/// Default `Bar` + `None` stack serializes as an empty object so plugins
55+
/// that never touch these fields don't pay any JSON overhead.
56+
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, TS)]
57+
pub struct NumberSeriesStyleConfig {
58+
#[serde(default)]
59+
#[serde(skip_serializing_if = "ChartType::is_default")]
60+
pub chart_type: ChartType,
61+
62+
/// Stack override. `None` means "use the glyph default"
63+
/// (Bar/Area stack, Line/Scatter don't). `Some(false)` on a Bar/Area
64+
/// forces non-stacking.
65+
#[serde(default)]
66+
#[serde(skip_serializing_if = "Option::is_none")]
67+
pub stack: Option<bool>,
68+
}
69+
70+
/// Defaults-only shape returned by `plugin.column_style_controls`. Presence
71+
/// of `Some(...)` on `ColumnStyleOpts.number_series_style` is the signal
72+
/// for the sidebar to render the chart-type picker.
73+
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
74+
pub struct NumberSeriesStyleDefaultConfig {
75+
pub chart_type: ChartType,
76+
#[serde(default)]
77+
pub stack: Option<bool>,
78+
}

0 commit comments

Comments
 (0)