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            NumberForegroundMode::LabelBar => html! {
263                <>
264                    <div class="row">
265                        <ColorRangeSelector ..self.color_props("label-bar-color", Fg, false, ctx) />
266                    </div>
267                    <NumberField ..self.max_value_props(Fg, ctx) />
268                </>
269            },
270        };
271
272        let bg_controls = match self.bg_mode {
273            NumberBackgroundMode::Disabled => html! {},
274            NumberBackgroundMode::Color => html! {
275                <div class="row">
276                    <ColorRangeSelector ..self.color_props("bg-color", Bg,false, ctx) />
277                </div>
278            },
279            NumberBackgroundMode::Gradient => html! {
280                <>
281                    <div class="row">
282                        <ColorRangeSelector ..self.color_props("gradient-color", Bg, true, ctx) />
283                    </div>
284                    <NumberField ..self.max_value_props(Bg, ctx) />
285                </>
286            },
287            NumberBackgroundMode::Pulse => html! {
288                <div class="row">
289                    <ColorRangeSelector ..self.color_props("pulse-color", Bg, true, ctx) />
290                </div>
291            },
292        };
293
294        html! {
295            <>
296                <LocalStyle href={css!("column-style")} />
297                <div id="column-style-container" class="number-column-style-container">
298                    <SelectEnumField<NumberForegroundMode>
299                        label="foreground"
300                        on_change={fg_mode_changed}
301                        current_value={self.fg_mode}
302                    />
303                    { fg_controls }
304                    <SelectEnumField<NumberBackgroundMode>
305                        label="background"
306                        on_change={bg_mode_changed}
307                        current_value={self.bg_mode}
308                    />
309                    { bg_controls }
310                </div>
311            </>
312        }
313    }
314}
315
316impl NumberColumnStyle {
317    /// When this config has changed, we must signal the wrapper element.
318    fn dispatch_config(&self, ctx: &Context<Self>) {
319        let mut config = self.config.clone();
320        match &self.config {
321            NumberColumnStyleConfig {
322                pos_fg_color: Some(pos_color),
323                neg_fg_color: Some(neg_color),
324                ..
325            } if *pos_color == self.default_config.pos_fg_color
326                && *neg_color == self.default_config.neg_fg_color =>
327            {
328                config.pos_fg_color = None;
329                config.neg_fg_color = None;
330            },
331            _ => {},
332        };
333
334        match &self.config {
335            NumberColumnStyleConfig {
336                pos_bg_color: Some(pos_color),
337                neg_bg_color: Some(neg_color),
338                ..
339            } if *pos_color == self.default_config.pos_bg_color
340                && *neg_color == self.default_config.neg_bg_color =>
341            {
342                config.pos_bg_color = None;
343                config.neg_bg_color = None;
344            },
345            _ => {},
346        };
347
348        let update = Some(config).filter(|config| config != &NumberColumnStyleConfig::default());
349
350        ctx.props()
351            .on_change
352            .emit(ColumnConfigValueUpdate::DatagridNumberStyle(update));
353    }
354
355    fn color_props(
356        &self,
357        id: &str,
358        side: Side,
359        is_gradient: bool,
360        ctx: &Context<Self>,
361    ) -> ColorRangeProps {
362        let on_pos_color = ctx
363            .link()
364            .callback(move |x| NumberColumnStyleMsg::PosColorChanged(side, x));
365        let on_neg_color = ctx
366            .link()
367            .callback(move |x| NumberColumnStyleMsg::NegColorChanged(side, x));
368
369        let default_config = self.default_config.clone();
370
371        props!(ColorRangeProps {
372            id: id.to_string(),
373            is_gradient,
374            pos_color: if side == Fg {
375                &self.pos_fg_color
376            } else {
377                &self.pos_bg_color
378            }
379            .to_owned(),
380            neg_color: if side == Fg {
381                &self.neg_fg_color
382            } else {
383                &self.neg_bg_color
384            }
385            .to_owned(),
386            on_pos_color,
387            on_neg_color,
388            on_reset: ctx.link().batch_callback(move |_| if side == Fg {
389                vec![
390                    NumberColumnStyleMsg::PosColorChanged(
391                        side,
392                        default_config.pos_fg_color.clone(),
393                    ),
394                    NumberColumnStyleMsg::NegColorChanged(
395                        side,
396                        default_config.neg_fg_color.clone(),
397                    ),
398                ]
399            } else {
400                vec![
401                    NumberColumnStyleMsg::PosColorChanged(
402                        side,
403                        default_config.pos_bg_color.clone(),
404                    ),
405                    NumberColumnStyleMsg::NegColorChanged(
406                        side,
407                        default_config.neg_bg_color.clone(),
408                    ),
409                ]
410            }),
411            is_modified: if side == Fg {
412                self.pos_fg_color != self.default_config.pos_fg_color
413                    || self.neg_fg_color != self.default_config.neg_fg_color
414            } else {
415                self.pos_bg_color != self.default_config.pos_bg_color
416                    || self.neg_bg_color != self.default_config.neg_bg_color
417            },
418        })
419    }
420
421    fn max_value_props(&self, side: Side, ctx: &Context<Self>) -> NumberFieldProps {
422        let on_change = ctx
423            .link()
424            .callback(move |x| NumberColumnStyleMsg::GradientChanged(side, x));
425
426        let value = if side == Fg {
427            self.fg_gradient.unwrap_or_default()
428        } else {
429            self.bg_gradient.unwrap_or_default()
430        };
431
432        props!(NumberFieldProps {
433            default: value,
434            current_value: value,
435            label: "max-value",
436            on_change
437        })
438    }
439
440    fn reset(
441        config: &NumberColumnStyleConfig,
442        default_config: &NumberColumnStyleDefaultConfig,
443    ) -> Self {
444        let mut config = config.clone();
445        let fg_gradient = config.fg_gradient;
446        let bg_gradient = config.bg_gradient;
447
448        let pos_fg_color = config
449            .pos_fg_color
450            .as_ref()
451            .unwrap_or(&default_config.pos_fg_color)
452            .to_owned();
453
454        let neg_fg_color = config
455            .neg_fg_color
456            .as_ref()
457            .unwrap_or(&default_config.neg_fg_color)
458            .to_owned();
459
460        let pos_bg_color = config
461            .pos_bg_color
462            .as_ref()
463            .unwrap_or(&default_config.pos_bg_color)
464            .to_owned();
465
466        let neg_bg_color = config
467            .neg_bg_color
468            .as_ref()
469            .unwrap_or(&default_config.neg_bg_color)
470            .to_owned();
471
472        config.pos_fg_color = Some(pos_fg_color.to_owned());
473        config.neg_fg_color = Some(neg_fg_color.to_owned());
474        let fg_mode = config.number_fg_mode;
475        config.pos_bg_color = Some(pos_bg_color.to_owned());
476        config.neg_bg_color = Some(neg_bg_color.to_owned());
477        let bg_mode = config.number_bg_mode;
478        Self {
479            config,
480            default_config: default_config.clone(),
481            fg_mode,
482            bg_mode,
483            pos_fg_color,
484            neg_fg_color,
485            pos_bg_color,
486            neg_bg_color,
487            fg_gradient,
488            bg_gradient,
489        }
490    }
491}