Skip to main content

perspective_viewer/components/
number_column_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 perspective_client::config::Scalar;
14use yew::prelude::*;
15use yew::*;
16
17use super::form::color_range_selector::*;
18use super::form::number_field::NumberFieldProps;
19use super::modal::*;
20use super::style::LocalStyle;
21use crate::components::form::number_field::NumberField;
22use crate::components::form::select_enum_field::SelectEnumField;
23use crate::config::*;
24use crate::session::Session;
25use crate::utils::*;
26use crate::*;
27
28#[derive(PartialEq, Eq, Copy, Clone, Debug)]
29pub enum Side {
30    Fg,
31    Bg,
32}
33
34use Side::*;
35
36/// A `ColumnStyle` component is mounted to the window anchored at the screen
37/// position of `elem`.
38///
39/// It needs two input configs, the current configuration object and a default
40/// version without `Option<>`
41#[derive(Properties)]
42pub struct NumberColumnStyleProps {
43    #[cfg_attr(test, prop_or_default)]
44    pub config: Option<NumberColumnStyleConfig>,
45
46    #[cfg_attr(test, prop_or_default)]
47    pub default_config: NumberColumnStyleDefaultConfig,
48
49    #[prop_or_default]
50    pub on_change: Callback<ColumnConfigValueUpdate>,
51
52    #[prop_or_default]
53    pub weak_link: WeakScope<NumberColumnStyle>,
54
55    #[prop_or_default]
56    pub column_name: Option<String>,
57
58    // State
59    pub session: Session,
60}
61
62impl ModalLink<NumberColumnStyle> for NumberColumnStyleProps {
63    fn weak_link(&self) -> &'_ WeakScope<NumberColumnStyle> {
64        &self.weak_link
65    }
66}
67
68impl PartialEq for NumberColumnStyleProps {
69    fn eq(&self, other: &Self) -> bool {
70        self.config == other.config
71            && self.default_config == other.default_config
72            && self.column_name == other.column_name
73    }
74}
75
76fn scalar_to_f64(scalar: &Scalar) -> f64 {
77    match scalar {
78        Scalar::Float(x) => *x,
79        Scalar::String(x) => x.parse::<f64>().unwrap_or_default(),
80        Scalar::Bool(x) => {
81            if *x {
82                1.0
83            } else {
84                0.0
85            }
86        },
87        Scalar::Null => 0.0,
88    }
89}
90
91fn set_default_gradient(session: &Session, ctx: &Context<NumberColumnStyle>) {
92    if let Some(column_name) = ctx.props().column_name.clone() {
93        let session = session.clone();
94        ctx.link().send_future(async move {
95            let view = session.get_view().unwrap();
96            let min_max = view.get_min_max(column_name).await.unwrap();
97            let abs_max = scalar_to_f64(&min_max.0)
98                .abs()
99                .max(scalar_to_f64(&min_max.1).abs());
100
101            let gradient = (abs_max * 100.).round() / 100.;
102            NumberColumnStyleMsg::DefaultGradientChanged(gradient)
103        });
104    }
105}
106
107#[derive(Debug)]
108pub enum NumberColumnStyleMsg {
109    PosColorChanged(Side, String),
110    NegColorChanged(Side, String),
111    NumberForeModeChanged(NumberForegroundMode),
112    NumberBackModeChanged(NumberBackgroundMode),
113    GradientChanged(Side, Option<f64>),
114    DefaultGradientChanged(f64),
115}
116
117/// A column style form control for `number` columns.
118pub struct NumberColumnStyle {
119    config: NumberColumnStyleConfig,
120    default_config: NumberColumnStyleDefaultConfig,
121    fg_mode: NumberForegroundMode,
122    bg_mode: NumberBackgroundMode,
123    pos_fg_color: String,
124    neg_fg_color: String,
125    pos_bg_color: String,
126    neg_bg_color: String,
127    fg_gradient: Option<f64>,
128    bg_gradient: Option<f64>,
129}
130
131impl Component for NumberColumnStyle {
132    type Message = NumberColumnStyleMsg;
133    type Properties = NumberColumnStyleProps;
134
135    fn create(ctx: &Context<Self>) -> Self {
136        ctx.set_modal_link();
137        set_default_gradient(&ctx.props().session, ctx);
138        Self::reset(
139            &ctx.props().config.clone().unwrap_or_default(),
140            &ctx.props().default_config.clone(),
141        )
142    }
143
144    fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
145        let mut new = Self::reset(
146            &ctx.props().config.clone().unwrap_or_default(),
147            &ctx.props().default_config.clone(),
148        );
149
150        set_default_gradient(&ctx.props().session, ctx);
151        std::mem::swap(self, &mut new);
152        true
153    }
154
155    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
156        match msg {
157            NumberColumnStyleMsg::PosColorChanged(side, val) => {
158                if side == Fg {
159                    self.pos_fg_color = val;
160                    self.config.pos_fg_color = Some(self.pos_fg_color.to_owned());
161                } else {
162                    self.pos_bg_color = val;
163                    self.config.pos_bg_color = Some(self.pos_bg_color.to_owned());
164                }
165
166                self.dispatch_config(ctx);
167                true
168            },
169            NumberColumnStyleMsg::NegColorChanged(side, val) => {
170                if side == Fg {
171                    self.neg_fg_color = val;
172                    self.config.neg_fg_color = Some(self.neg_fg_color.to_owned());
173                } else {
174                    self.neg_bg_color = val;
175                    self.config.neg_bg_color = Some(self.neg_bg_color.to_owned());
176                }
177
178                self.dispatch_config(ctx);
179                true
180            },
181            NumberColumnStyleMsg::NumberForeModeChanged(val) => {
182                self.fg_mode = val;
183                self.config.number_fg_mode = val;
184                if self.fg_mode.needs_gradient() {
185                    self.config.fg_gradient = Some(self.fg_gradient.unwrap());
186                } else {
187                    self.config.fg_gradient = None;
188                }
189
190                self.dispatch_config(ctx);
191                true
192            },
193            NumberColumnStyleMsg::NumberBackModeChanged(val) => {
194                self.bg_mode = val;
195                self.config.number_bg_mode = val;
196                if self.bg_mode.needs_gradient() {
197                    self.config.bg_gradient = Some(self.bg_gradient.unwrap());
198                } else {
199                    self.config.bg_gradient = None;
200                }
201
202                self.dispatch_config(ctx);
203                true
204            },
205            NumberColumnStyleMsg::GradientChanged(side, gradient) => {
206                match (side, gradient) {
207                    (Fg, Some(x)) => {
208                        self.fg_gradient = Some(x);
209                        self.config.fg_gradient = Some(x);
210                    },
211                    (Fg, None) => {
212                        self.fg_gradient = Some(self.default_config.fg_gradient);
213                        self.config.fg_gradient = None;
214                    },
215                    (Bg, Some(x)) => {
216                        self.bg_gradient = Some(x);
217                        self.config.bg_gradient = Some(x);
218                    },
219                    (Bg, None) => {
220                        self.bg_gradient = Some(self.default_config.bg_gradient);
221                        self.config.bg_gradient = None;
222                    },
223                };
224
225                self.dispatch_config(ctx);
226                false
227            },
228            NumberColumnStyleMsg::DefaultGradientChanged(gradient) => {
229                self.fg_gradient.get_or_insert(gradient);
230                self.bg_gradient.get_or_insert(gradient);
231                self.default_config.fg_gradient = gradient;
232                self.default_config.bg_gradient = gradient;
233                true
234            },
235        }
236    }
237
238    fn view(&self, ctx: &Context<Self>) -> Html {
239        let fg_mode_changed = ctx.link().callback(|x: Option<_>| {
240            NumberColumnStyleMsg::NumberForeModeChanged(x.unwrap_or_default())
241        });
242
243        let bg_mode_changed = ctx.link().callback(|x: Option<_>| {
244            NumberColumnStyleMsg::NumberBackModeChanged(x.unwrap_or_default())
245        });
246
247        let fg_controls = match self.fg_mode {
248            NumberForegroundMode::Disabled => html! {},
249            NumberForegroundMode::Color => html! {
250                <div class="row">
251                    <ColorRangeSelector ..self.color_props("fg-color", Fg, false, ctx) />
252                </div>
253            },
254            NumberForegroundMode::Bar => html! {
255                <>
256                    <div class="row">
257                        <ColorRangeSelector ..self.color_props("bar-color", Fg, false, ctx) />
258                    </div>
259                    <NumberField ..self.max_value_props(Fg, ctx) />
260                </>
261            },
262        };
263
264        let bg_controls = match self.bg_mode {
265            NumberBackgroundMode::Disabled => html! {},
266            NumberBackgroundMode::Color => html! {
267                <div class="row">
268                    <ColorRangeSelector ..self.color_props("bg-color", Bg,false, ctx) />
269                </div>
270            },
271            NumberBackgroundMode::Gradient => html! {
272                <>
273                    <div class="row">
274                        <ColorRangeSelector ..self.color_props("gradient-color", Bg, true, ctx) />
275                    </div>
276                    <NumberField ..self.max_value_props(Bg, ctx) />
277                </>
278            },
279            NumberBackgroundMode::Pulse => html! {
280                <div class="row">
281                    <ColorRangeSelector ..self.color_props("pulse-color", Bg, true, ctx) />
282                </div>
283            },
284        };
285
286        html! {
287            <>
288                <LocalStyle href={css!("column-style")} />
289                <div id="column-style-container" class="number-column-style-container">
290                    <SelectEnumField<NumberForegroundMode>
291                        label="foreground"
292                        on_change={fg_mode_changed}
293                        current_value={self.fg_mode}
294                    />
295                    { fg_controls }
296                    <SelectEnumField<NumberBackgroundMode>
297                        label="background"
298                        on_change={bg_mode_changed}
299                        current_value={self.bg_mode}
300                    />
301                    { bg_controls }
302                </div>
303            </>
304        }
305    }
306}
307
308impl NumberColumnStyle {
309    /// When this config has changed, we must signal the wrapper element.
310    fn dispatch_config(&self, ctx: &Context<Self>) {
311        let mut config = self.config.clone();
312        match &self.config {
313            NumberColumnStyleConfig {
314                pos_fg_color: Some(pos_color),
315                neg_fg_color: Some(neg_color),
316                ..
317            } if *pos_color == self.default_config.pos_fg_color
318                && *neg_color == self.default_config.neg_fg_color =>
319            {
320                config.pos_fg_color = None;
321                config.neg_fg_color = None;
322            },
323            _ => {},
324        };
325
326        match &self.config {
327            NumberColumnStyleConfig {
328                pos_bg_color: Some(pos_color),
329                neg_bg_color: Some(neg_color),
330                ..
331            } if *pos_color == self.default_config.pos_bg_color
332                && *neg_color == self.default_config.neg_bg_color =>
333            {
334                config.pos_bg_color = None;
335                config.neg_bg_color = None;
336            },
337            _ => {},
338        };
339
340        let update = Some(config).filter(|config| config != &NumberColumnStyleConfig::default());
341
342        ctx.props()
343            .on_change
344            .emit(ColumnConfigValueUpdate::DatagridNumberStyle(update));
345    }
346
347    fn color_props(
348        &self,
349        id: &str,
350        side: Side,
351        is_gradient: bool,
352        ctx: &Context<Self>,
353    ) -> ColorRangeProps {
354        let on_pos_color = ctx
355            .link()
356            .callback(move |x| NumberColumnStyleMsg::PosColorChanged(side, x));
357        let on_neg_color = ctx
358            .link()
359            .callback(move |x| NumberColumnStyleMsg::NegColorChanged(side, x));
360
361        let default_config = self.default_config.clone();
362
363        props!(ColorRangeProps {
364            id: id.to_string(),
365            is_gradient,
366            pos_color: if side == Fg {
367                &self.pos_fg_color
368            } else {
369                &self.pos_bg_color
370            }
371            .to_owned(),
372            neg_color: if side == Fg {
373                &self.neg_fg_color
374            } else {
375                &self.neg_bg_color
376            }
377            .to_owned(),
378            on_pos_color,
379            on_neg_color,
380            on_reset: ctx.link().batch_callback(move |_| if side == Fg {
381                vec![
382                    NumberColumnStyleMsg::PosColorChanged(
383                        side,
384                        default_config.pos_fg_color.clone(),
385                    ),
386                    NumberColumnStyleMsg::NegColorChanged(
387                        side,
388                        default_config.neg_fg_color.clone(),
389                    ),
390                ]
391            } else {
392                vec![
393                    NumberColumnStyleMsg::PosColorChanged(
394                        side,
395                        default_config.pos_bg_color.clone(),
396                    ),
397                    NumberColumnStyleMsg::NegColorChanged(
398                        side,
399                        default_config.neg_bg_color.clone(),
400                    ),
401                ]
402            }),
403            is_modified: if side == Fg {
404                self.pos_fg_color != self.default_config.pos_fg_color
405                    || self.neg_fg_color != self.default_config.neg_fg_color
406            } else {
407                self.pos_bg_color != self.default_config.pos_bg_color
408                    || self.neg_bg_color != self.default_config.neg_bg_color
409            },
410        })
411    }
412
413    fn max_value_props(&self, side: Side, ctx: &Context<Self>) -> NumberFieldProps {
414        let on_change = ctx
415            .link()
416            .callback(move |x| NumberColumnStyleMsg::GradientChanged(side, x));
417
418        let value = if side == Fg {
419            self.fg_gradient.unwrap_or_default()
420        } else {
421            self.bg_gradient.unwrap_or_default()
422        };
423
424        props!(NumberFieldProps {
425            default: value,
426            current_value: value,
427            label: "max-value",
428            on_change
429        })
430    }
431
432    fn reset(
433        config: &NumberColumnStyleConfig,
434        default_config: &NumberColumnStyleDefaultConfig,
435    ) -> Self {
436        let mut config = config.clone();
437        let fg_gradient = config.fg_gradient;
438        let bg_gradient = config.bg_gradient;
439
440        let pos_fg_color = config
441            .pos_fg_color
442            .as_ref()
443            .unwrap_or(&default_config.pos_fg_color)
444            .to_owned();
445
446        let neg_fg_color = config
447            .neg_fg_color
448            .as_ref()
449            .unwrap_or(&default_config.neg_fg_color)
450            .to_owned();
451
452        let pos_bg_color = config
453            .pos_bg_color
454            .as_ref()
455            .unwrap_or(&default_config.pos_bg_color)
456            .to_owned();
457
458        let neg_bg_color = config
459            .neg_bg_color
460            .as_ref()
461            .unwrap_or(&default_config.neg_bg_color)
462            .to_owned();
463
464        config.pos_fg_color = Some(pos_fg_color.to_owned());
465        config.neg_fg_color = Some(neg_fg_color.to_owned());
466        let fg_mode = config.number_fg_mode;
467        config.pos_bg_color = Some(pos_bg_color.to_owned());
468        config.neg_bg_color = Some(neg_bg_color.to_owned());
469        let bg_mode = config.number_bg_mode;
470        Self {
471            config,
472            default_config: default_config.clone(),
473            fg_mode,
474            bg_mode,
475            pos_fg_color,
476            neg_fg_color,
477            pos_bg_color,
478            neg_bg_color,
479            fg_gradient,
480            bg_gradient,
481        }
482    }
483}