Skip to main content

perspective_viewer/components/
number_series_style.rs

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
13use web_sys::{HtmlInputElement, InputEvent};
14use yew::prelude::*;
15
16use super::modal::{ModalLink, SetModalLink};
17use crate::components::form::select_enum_field::SelectEnumField;
18use crate::config::*;
19use crate::utils::WeakScope;
20
21#[derive(Properties)]
22pub struct NumberSeriesStyleProps {
23    pub config: Option<NumberSeriesStyleConfig>,
24    pub default_config: NumberSeriesStyleDefaultConfig,
25
26    #[prop_or_default]
27    pub on_change: Callback<ColumnConfigFieldUpdate>,
28
29    #[prop_or_default]
30    pub keys: Vec<String>,
31
32    #[prop_or_default]
33    weak_link: WeakScope<NumberSeriesStyle>,
34}
35
36impl ModalLink<NumberSeriesStyle> for NumberSeriesStyleProps {
37    fn weak_link(&self) -> &'_ WeakScope<NumberSeriesStyle> {
38        &self.weak_link
39    }
40}
41
42impl PartialEq for NumberSeriesStyleProps {
43    fn eq(&self, other: &Self) -> bool {
44        self.config == other.config && self.default_config == other.default_config
45    }
46}
47
48pub enum NumberSeriesStyleMsg {
49    ChartTypeChanged(Option<ChartType>),
50    StackChanged(Option<bool>),
51}
52
53/// Form control for the per-column `chart_type` + `stack` picker. Rendered
54/// inside the column-settings sidebar when the active plugin returns a
55/// `ControlSpec::NumberSeriesStyle` from its `column_config_schema` hook.
56pub struct NumberSeriesStyle {
57    config: NumberSeriesStyleConfig,
58}
59
60impl Component for NumberSeriesStyle {
61    type Message = NumberSeriesStyleMsg;
62    type Properties = NumberSeriesStyleProps;
63
64    fn create(ctx: &Context<Self>) -> Self {
65        ctx.set_modal_link();
66        Self {
67            config: ctx.props().config.clone().unwrap_or_default(),
68        }
69    }
70
71    fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
72        let new_config = ctx.props().config.clone().unwrap_or_default();
73        if self.config != new_config {
74            self.config = new_config;
75            true
76        } else {
77            false
78        }
79    }
80
81    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
82        match msg {
83            NumberSeriesStyleMsg::ChartTypeChanged(val) => {
84                self.config.chart_type = val.unwrap_or_default();
85                // Hiding the stack checkbox on Line/Scatter also clears any
86                // lingering override so the JSON stays empty by default.
87                if !self.config.chart_type.supports_stack() {
88                    self.config.stack = None;
89                }
90                self.dispatch_config(ctx);
91                true
92            },
93            NumberSeriesStyleMsg::StackChanged(val) => {
94                self.config.stack = val;
95                self.dispatch_config(ctx);
96                true
97            },
98        }
99    }
100
101    fn view(&self, ctx: &Context<Self>) -> Html {
102        let chart_type_changed = ctx.link().callback(NumberSeriesStyleMsg::ChartTypeChanged);
103
104        let stack_controls = if self.config.chart_type.supports_stack() {
105            // Default: bar/area stack. `None` == inherit the default.
106            let checked = self.config.stack.unwrap_or(true);
107            let oninput = ctx.link().callback(move |e: InputEvent| {
108                let input: HtmlInputElement = e.target_unchecked_into();
109                let next = input.checked();
110                // Persist explicit `false` overrides; the "stacked" default
111                // round-trips as `None` to keep JSON empty.
112                NumberSeriesStyleMsg::StackChanged(if next { None } else { Some(false) })
113            });
114            html! {
115                <div class="row">
116                    <label id="stack-label" />
117                    <input type="checkbox" id="stack-checkbox" {checked} {oninput} />
118                </div>
119            }
120        } else {
121            html! {}
122        };
123
124        html! {
125            <>
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
139impl NumberSeriesStyle {
140    /// Dispatch the current config as an update. The default (Bar + no
141    /// stack override) round-trips as an empty JSON object via
142    /// `skip_serializing_if`, which means a field-level reset for this
143    /// schema field.
144    fn dispatch_config(&self, ctx: &Context<Self>) {
145        let value = if self.config == NumberSeriesStyleConfig::default() {
146            serde_json::Map::new()
147        } else {
148            match serde_json::to_value(&self.config) {
149                Ok(serde_json::Value::Object(m)) => m,
150                _ => serde_json::Map::new(),
151            }
152        };
153        ctx.props().on_change.emit(ColumnConfigFieldUpdate {
154            keys: ctx.props().keys.clone(),
155            value,
156        });
157    }
158}