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 crate::config::*;
17use crate::ui::{IntlLabel, ModalLink, SelectEnumField, SetModalLink};
18use crate::utils::WeakScope;
19
20#[derive(Properties)]
21pub struct NumberSeriesStyleProps {
22    pub config: Option<NumberSeriesStyleConfig>,
23    pub default_config: NumberSeriesStyleDefaultConfig,
24
25    #[prop_or_default]
26    pub on_change: Callback<ColumnConfigFieldUpdate>,
27
28    #[prop_or_default]
29    pub keys: Vec<String>,
30
31    #[prop_or_default]
32    weak_link: WeakScope<NumberSeriesStyle>,
33}
34
35impl ModalLink<NumberSeriesStyle> for NumberSeriesStyleProps {
36    fn weak_link(&self) -> &'_ WeakScope<NumberSeriesStyle> {
37        &self.weak_link
38    }
39}
40
41impl PartialEq for NumberSeriesStyleProps {
42    fn eq(&self, other: &Self) -> bool {
43        self.config == other.config && self.default_config == other.default_config
44    }
45}
46
47pub 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/// `ControlSpec::NumberSeriesStyle` from its `column_config_schema` hook.
55pub struct NumberSeriesStyle {
56    config: NumberSeriesStyleConfig,
57}
58
59impl 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                    <IntlLabel name="stack" />
116                    <input type="checkbox" id="stack-checkbox" {checked} {oninput} />
117                </div>
118            }
119        } else {
120            html! {}
121        };
122
123        html! {
124            <>
125                <div id="column-style-container" class="number-series-style-container">
126                    <SelectEnumField<ChartType>
127                        label="chart-type"
128                        on_change={chart_type_changed}
129                        current_value={self.config.chart_type}
130                    />
131                    { stack_controls }
132                </div>
133            </>
134        }
135    }
136}
137
138impl NumberSeriesStyle {
139    /// Dispatch the current config as an update. The default (Bar + no
140    /// stack override) round-trips as an empty JSON object via
141    /// `skip_serializing_if`, which means a field-level reset for this
142    /// schema field.
143    fn dispatch_config(&self, ctx: &Context<Self>) {
144        let value = if self.config == NumberSeriesStyleConfig::default() {
145            serde_json::Map::new()
146        } else {
147            match serde_json::to_value(&self.config) {
148                Ok(serde_json::Value::Object(m)) => m,
149                _ => serde_json::Map::new(),
150            }
151        };
152        ctx.props().on_change.emit(ColumnConfigFieldUpdate {
153            keys: ctx.props().keys.clone(),
154            value,
155        });
156    }
157}