all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [RFC PATCH yew-widget-toolkit 0/3] implement pie chart widget
@ 2026-03-20 16:03 Dominik Csapak
  2026-03-20 16:03 ` [RFC PATCH yew-widget-toolkit 1/3] widget: button: add 'icon_style' property Dominik Csapak
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Dominik Csapak @ 2026-03-20 16:03 UTC (permalink / raw)
  To: yew-devel

I'm currently working on adding more dashboard elements to PDM, and I
wanted to add some gauge charts. While doing this, I made it more
generic and it's now a (more or less) fully featured Pie/Donut/Gauge chart.

In PDM we probably won't use it as a pie/donut chart for now, but it
wasn't too hard to implement the remaining features and wanted to get
some comments and opinions on it, so sending it as RFC for now.

(There are some things we could more neatly visualize with a pie/donut
chart if we want to, so I believe it makes sense to have such a widget)

Dominik Csapak (3):
  widget: button: add 'icon_style' property
  macros: widget: impl WidgetStyleBuilder for svgs
  add pie chart widget

 pwt-macros/src/widget.rs |  25 +-
 src/widget/button.rs     |   8 +-
 src/widget/charts/mod.rs |   4 +
 src/widget/charts/pie.rs | 547 +++++++++++++++++++++++++++++++++++++++
 src/widget/mod.rs        |   2 +
 5 files changed, 574 insertions(+), 12 deletions(-)
 create mode 100644 src/widget/charts/mod.rs
 create mode 100644 src/widget/charts/pie.rs

-- 
2.47.3





^ permalink raw reply	[flat|nested] 4+ messages in thread

* [RFC PATCH yew-widget-toolkit 1/3] widget: button: add 'icon_style' property
  2026-03-20 16:03 [RFC PATCH yew-widget-toolkit 0/3] implement pie chart widget Dominik Csapak
@ 2026-03-20 16:03 ` Dominik Csapak
  2026-03-20 16:03 ` [RFC PATCH yew-widget-toolkit 2/3] macros: widget: impl WidgetStyleBuilder for svgs Dominik Csapak
  2026-03-20 16:04 ` [RFC PATCH yew-widget-toolkit 3/3] add pie chart widget Dominik Csapak
  2 siblings, 0 replies; 4+ messages in thread
From: Dominik Csapak @ 2026-03-20 16:03 UTC (permalink / raw)
  To: yew-devel

so one can override the style of the icon, e.g. giving an explicit color
without having to create a CSS class for that.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/widget/button.rs | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/widget/button.rs b/src/widget/button.rs
index 584f923..85b9e9d 100644
--- a/src/widget/button.rs
+++ b/src/widget/button.rs
@@ -64,6 +64,11 @@ pub struct Button {
     #[prop_or_default]
     pub icon_class: Option<Classes>,
 
+    /// Icon CSS style.
+    #[prop_or_default]
+    #[builder(IntoPropValue, into_prop_value)]
+    pub icon_style: Option<AttrValue>,
+
     /// Html tabindex attribute.
     #[prop_or_default]
     #[builder(IntoPropValue, into_prop_value)]
@@ -236,10 +241,11 @@ impl Component for PwtButton {
         };
 
         if let Some(icon_class) = &props.icon_class {
+            let style = props.icon_style.clone();
             if !icon_class.is_empty() {
                 // Chromium fires onclick from nested elements, so we need to suppress that manually here
                 children.push(html!{
-                    <span onclick={suppress_onclick.clone()}><i role="none" class={icon_class.clone()}></i></span>
+                    <span onclick={suppress_onclick.clone()}><i role="none" {style} class={icon_class.clone()}></i></span>
                 });
             }
         }
-- 
2.47.3





^ permalink raw reply	[flat|nested] 4+ messages in thread

* [RFC PATCH yew-widget-toolkit 2/3] macros: widget: impl WidgetStyleBuilder for svgs
  2026-03-20 16:03 [RFC PATCH yew-widget-toolkit 0/3] implement pie chart widget Dominik Csapak
  2026-03-20 16:03 ` [RFC PATCH yew-widget-toolkit 1/3] widget: button: add 'icon_style' property Dominik Csapak
@ 2026-03-20 16:03 ` Dominik Csapak
  2026-03-20 16:04 ` [RFC PATCH yew-widget-toolkit 3/3] add pie chart widget Dominik Csapak
  2 siblings, 0 replies; 4+ messages in thread
From: Dominik Csapak @ 2026-03-20 16:03 UTC (permalink / raw)
  To: yew-devel

svg elements such as Circle, Group, etc. have a style property that can
be set, so implement this for them.

padding/margin/border can be set on such elements too but have no
effect, so don't implement them for svg elements.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 pwt-macros/src/widget.rs | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/pwt-macros/src/widget.rs b/pwt-macros/src/widget.rs
index 08f2c1c..7e9ade2 100644
--- a/pwt-macros/src/widget.rs
+++ b/pwt-macros/src/widget.rs
@@ -218,24 +218,27 @@ fn derive_widget(setup: &WidgetSetup, widget: DeriveInput) -> Result<proc_macro2
         }
     });
 
-    if !setup.is_svg {
-        output.extend(quote!{
-            impl #impl_generics #pwt::props::AsClassesMut for #ident #ty_generics #where_clause {
-                fn as_classes_mut(&mut self) -> &mut ::yew::Classes {
-                    &mut self.std_props.class
-                }
+    output.extend(quote! {
+        impl #impl_generics #pwt::props::AsClassesMut for #ident #ty_generics #where_clause {
+            fn as_classes_mut(&mut self) -> &mut ::yew::Classes {
+                &mut self.std_props.class
             }
+        }
 
-            impl #impl_generics #pwt::props::AsCssStylesMut for #ident #ty_generics #where_clause {
-                fn as_css_styles_mut(&mut self) -> &mut #pwt::props::CssStyles {
-                    &mut self.std_props.styles
-                }
+        impl #impl_generics #pwt::props::AsCssStylesMut for #ident #ty_generics #where_clause {
+            fn as_css_styles_mut(&mut self) -> &mut #pwt::props::CssStyles {
+                &mut self.std_props.styles
             }
+        }
+
+        impl #impl_generics #pwt::props::WidgetStyleBuilder for #ident #ty_generics #where_clause {}
+    });
 
+    if !setup.is_svg {
+        output.extend(quote! {
             impl #impl_generics #pwt::props::CssMarginBuilder for #ident #ty_generics #where_clause {}
             impl #impl_generics #pwt::props::CssPaddingBuilder for #ident #ty_generics #where_clause {}
             impl #impl_generics #pwt::props::CssBorderBuilder for #ident #ty_generics #where_clause {}
-            impl #impl_generics #pwt::props::WidgetStyleBuilder for #ident #ty_generics #where_clause {}
         });
     }
 
-- 
2.47.3





^ permalink raw reply	[flat|nested] 4+ messages in thread

* [RFC PATCH yew-widget-toolkit 3/3] add pie chart widget
  2026-03-20 16:03 [RFC PATCH yew-widget-toolkit 0/3] implement pie chart widget Dominik Csapak
  2026-03-20 16:03 ` [RFC PATCH yew-widget-toolkit 1/3] widget: button: add 'icon_style' property Dominik Csapak
  2026-03-20 16:03 ` [RFC PATCH yew-widget-toolkit 2/3] macros: widget: impl WidgetStyleBuilder for svgs Dominik Csapak
@ 2026-03-20 16:04 ` Dominik Csapak
  2 siblings, 0 replies; 4+ messages in thread
From: Dominik Csapak @ 2026-03-20 16:04 UTC (permalink / raw)
  To: yew-devel

and introduce the 'charts' module for that.

This widget has the following features:
* use as 'gauge' with a single value
* use as 'pie' or 'donut' chart when using multiple values
* customizable colors,tooltip and legend position
* start and end angles can be customized (within reasonable limits)
* segments can be hidden via the legend
* can show text in the middle (most useful for gauge charts)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/widget/charts/mod.rs |   4 +
 src/widget/charts/pie.rs | 547 +++++++++++++++++++++++++++++++++++++++
 src/widget/mod.rs        |   2 +
 3 files changed, 553 insertions(+)
 create mode 100644 src/widget/charts/mod.rs
 create mode 100644 src/widget/charts/pie.rs

diff --git a/src/widget/charts/mod.rs b/src/widget/charts/mod.rs
new file mode 100644
index 0000000..42491f7
--- /dev/null
+++ b/src/widget/charts/mod.rs
@@ -0,0 +1,4 @@
+//! Chart components
+
+mod pie;
+pub use pie::{LegendPosition, PieChart};
diff --git a/src/widget/charts/pie.rs b/src/widget/charts/pie.rs
new file mode 100644
index 0000000..bfd6080
--- /dev/null
+++ b/src/widget/charts/pie.rs
@@ -0,0 +1,547 @@
+//! Pie Chart
+//!
+//! Provides an element that can be used for a 'gauge' by supplying a single value,
+//! a 'pie' chart by providing multiple values with an 'thickness_ratio' of 1.0 or
+//! a 'donut' chart.
+//!
+//! Includes the following features
+//! * An optional legend
+//! * configuring colors
+//! * specifying start and end angles
+//! * animated highlighting of segments
+//! * hiding segments (via clicking on the legend)
+//! * rendering tooltips (with an optional custom renderer)
+//! * showing a text in the middle (useful for gauges/donut charts)
+
+use std::collections::HashSet;
+
+use yew::html::IntoPropValue;
+use yew::prelude::*;
+
+use crate::css;
+use crate::dom::align::align_to_xy;
+use crate::prelude::*;
+use crate::props::{IntoOptionalRenderFn, RenderFn};
+use crate::widget::canvas::{Canvas, Circle, Group, Text};
+use crate::widget::{Button, Container};
+
+use pwt_macros::{builder, widget};
+
+// default colors
+const DEFAULT_COLORS: &[&str] = &[
+    "var(--pwt-color-primary)",
+    "var(--pwt-color-secondary)",
+    "var(--pwt-color-tertiary)",
+];
+
+const GAUGE_COLORS: &[(f64, &str)] = &[
+    (0.9, "var(--pwt-color-error)"),
+    (0.7, "var(--pwt-color-warning)"),
+    (0.0, "var(--pwt-color-primary)"),
+];
+
+// base size for the viewBox (before recalculating due to the start/end angles)
+const SIZE: f64 = 110.0;
+
+#[derive(PartialEq, Eq, Clone, Copy, Debug)]
+/// The position of the legend relative to a chart.
+pub enum LegendPosition {
+    Hidden,
+    Start,
+    End,
+    Top,
+    Bottom,
+}
+
+#[widget(pwt=crate, comp=PwtPieChart, @element)]
+#[derive(Properties, Clone, PartialEq)]
+#[builder]
+/// Pie chart properties
+pub struct PieChart {
+    #[builder]
+    #[prop_or(0.3)]
+    /// The ratio of the ring to the inner radius of the pie chart, must be between 0.01 and 1.0.
+    ///
+    /// 1.0 results in a pie chart, values below in a donut shaped one.
+    thickness_ratio: f64,
+
+    // the values to show. if only one, it's assumed to be between 0.0 and 1.0
+    // and the remainder will be calculated. otherwise it's assumed they're relative
+    // values
+    values: Vec<(AttrValue, f64)>,
+
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or_default]
+    /// Text to display inside the chart. Note that the size and relative position of the text does
+    /// not change when the ratio changes or when the chart is clipped due to different start/end
+    /// angles. Most useful when using default gauge charts (or only slightly changed angles).
+    text: Option<AttrValue>,
+
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or_default]
+    /// The list of colors to use. Only used when giving multiple values.
+    ///
+    /// For the 'gauge' type chart, The 'primary', 'warning' and 'error' colors are used.
+    /// For the multi-value chart 'primary', 'secondary' and 'tertiary' colors are used.
+    ///
+    /// If there are not enough colors for the given amount of values, the colors will
+    /// be reused and mixed with an increasing amount of the 'surface' color.
+    colors: Option<Vec<AttrValue>>,
+
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or_default]
+    /// Allows highlighting the separate value segments. Default is true for charts with multiple
+    /// values and false for charts with a single value.
+    allow_highlight: Option<bool>,
+
+    #[builder_cb(IntoOptionalRenderFn, into_optional_render_fn, (AttrValue, f64, usize))]
+    #[prop_or_default]
+    /// Tooltip renderer to override the default one. The parameters are the title, the value and
+    /// the index into values.
+    render_tooltip: Option<RenderFn<(AttrValue, f64, usize)>>,
+
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or(true)]
+    /// Determine if tooltips are shown or not.
+    show_tooltip: bool,
+
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or_default]
+    /// Determines the position of the legend.
+    ///
+    /// Defaults:
+    /// for gauge type chart: 'Hidden'
+    /// for multi-value chart: 'End'
+    legend: Option<LegendPosition>,
+
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or(0.0)]
+    /// The starting angle of the chart in degrees, default is 0.0.
+    angle_start: f64,
+
+    #[builder(IntoPropValue, into_prop_value)]
+    #[prop_or(360.0)]
+    /// The ending angle of the chart in degrees, default is 360.0,
+    angle_end: f64,
+}
+
+impl PieChart {
+    /// Creates a new chart with a single value from 0.0 to 1.0 (smaller and larger values will
+    /// be clamped).
+    pub fn new(title: impl Into<AttrValue>, value: f64) -> Self {
+        yew::props!(Self {
+            values: vec![(title.into(), value.clamp(0.0, 1.0))]
+        })
+    }
+
+    /// Creates a new chart with multiple relative values.
+    pub fn with_values(values: Vec<(impl Into<AttrValue>, f64)>) -> Self {
+        yew::props!(Self {
+            values: values
+                .into_iter()
+                .map(|(title, value)| (title.into(), value))
+                .collect::<Vec<_>>()
+        })
+    }
+
+    fn segment_get_color(&self, index: usize, value: f64) -> String {
+        if self.values.len() == 1 {
+            for (threshold, color) in GAUGE_COLORS {
+                if value >= *threshold {
+                    return color.to_string();
+                }
+            }
+        }
+        let (base_color, cycle) = match &self.colors {
+            Some(colors) => {
+                let base = colors[index % colors.len()].to_string();
+                let cycle = index / colors.len();
+                (base, cycle)
+            }
+            None => {
+                let base = DEFAULT_COLORS[index % DEFAULT_COLORS.len()].to_string();
+                let cycle = index / DEFAULT_COLORS.len();
+                (base, cycle)
+            }
+        };
+        format!(
+            "color-mix(in hsl, {base_color} {}%, var(--pwt-color-surface))",
+            100.0 - (20.0 * cycle as f64)
+        )
+    }
+
+    fn effective_legend_position(&self) -> LegendPosition {
+        self.legend.unwrap_or({
+            if self.values.len() == 1 {
+                LegendPosition::Hidden
+            } else {
+                LegendPosition::End
+            }
+        })
+    }
+}
+
+struct Segment {
+    dasharray: String,
+    offset: f64,
+    highlight_offset: (f64, f64),
+    tooltip: Html,
+    color: String,
+    allow_highlight: bool,
+}
+
+pub enum Msg {
+    Highlight(Option<usize>),
+    MouseOver(Option<(i32, i32)>),
+    ToggleValue(usize),
+}
+
+pub struct PwtPieChart {
+    radius: f32,
+    stroke_width: f32,
+    start: f64,
+    max_len: f64,
+    circumference: f64,
+    highlight: Option<usize>,
+    mouse_pos: Option<(f64, f64)>,
+    tooltip_ref: NodeRef,
+    svg_ref: NodeRef,
+    hidden: HashSet<usize>,
+    segments: Vec<Segment>,
+    bottom_diff: f64,
+    left_diff: f64,
+    right_diff: f64,
+}
+
+impl PwtPieChart {
+    fn new(ctx: &Context<Self>) -> Self {
+        let mut this = Self {
+            radius: 0.0,
+            stroke_width: 0.0,
+            start: 0.0,
+            max_len: 0.0,
+            circumference: 0.0,
+            highlight: None,
+            mouse_pos: None,
+            tooltip_ref: Default::default(),
+            svg_ref: Default::default(),
+            hidden: HashSet::new(),
+            segments: Vec::new(),
+            bottom_diff: 0.0,
+            left_diff: 0.0,
+            right_diff: 0.0,
+        };
+        this.recalculate_draw_values(ctx);
+        this.recalculate_segments(ctx);
+        this
+    }
+
+    fn recalculate_draw_values(&mut self, ctx: &Context<Self>) {
+        let props = ctx.props();
+        let ratio = props.thickness_ratio.clamp(0.01, 1.0) as f32;
+        self.stroke_width = ratio * 50.0;
+        self.radius = 50.0 - (self.stroke_width / 2.0);
+        self.circumference = self.radius as f64 * std::f64::consts::TAU;
+        self.max_len = self.circumference * ((props.angle_end - props.angle_start) / 360.0);
+        self.start = self.circumference * (props.angle_start / 360.0);
+        self.left_diff = 0.0;
+        self.right_diff = 0.0;
+        self.bottom_diff = 0.0;
+
+        let outer_radius = (self.radius + self.stroke_width / 2.0) as f64;
+        let inner_radius = outer_radius - self.stroke_width as f64;
+        if props.angle_start > 0.0 && props.angle_end < 360.0 {
+            let start_cos = props.angle_start.to_radians().cos();
+            let end_cos = props.angle_end.to_radians().cos();
+            let cos = start_cos.max(end_cos);
+
+            let bottom_radius = if props.angle_start > 90.0 && props.angle_end < 270.0 {
+                inner_radius
+            } else {
+                outer_radius
+            };
+
+            self.bottom_diff = outer_radius - (cos * bottom_radius);
+        }
+
+        if props.angle_start > 90.0 {
+            let sin = props.angle_start.to_radians().sin();
+            self.left_diff = outer_radius - sin * outer_radius;
+        }
+
+        if props.angle_end < 270.0 {
+            let sin = props.angle_end.to_radians().sin();
+            self.right_diff = outer_radius + sin * outer_radius;
+        }
+    }
+
+    fn recalculate_segments(&mut self, ctx: &Context<Self>) {
+        let props = ctx.props();
+        let mut segments = Vec::with_capacity(props.values.len());
+
+        let sum = if props.values.len() == 1 {
+            1.0
+        } else {
+            props
+                .values
+                .iter()
+                .enumerate()
+                .filter_map(|(index, (_, value))| (!self.hidden.contains(&index)).then_some(value))
+                .sum()
+        };
+
+        let allow_highlight = if props.values.len() == 1 {
+            false
+        } else {
+            props.allow_highlight.unwrap_or(true)
+        };
+
+        let mut last_value = 0.0;
+        for (index, (title, value)) in props.values.iter().enumerate() {
+            let is_visible = !self.hidden.contains(&index);
+
+            let highlight_offset = if is_visible {
+                let percent = (last_value + value * 0.5) / sum;
+                let angle = ((props.angle_end - props.angle_start) * percent + props.angle_start)
+                    .to_radians();
+                (4.0 * angle.cos(), 4.0 * angle.sin())
+            } else {
+                (0.0, 0.0)
+            };
+            // even if this segment is not visible, add the circle but with size 0 so the animation
+            // has a start point and the segments don't overlap
+            let offset = -(self.start + last_value / sum * self.max_len);
+            let length = if is_visible {
+                // adding 0.01 here to reduce the seams without changing the size too much
+                value / sum * self.max_len + 0.01
+            } else {
+                0.0
+            };
+            let remainder = self.circumference;
+            let dasharray = format!("{length} {remainder}",);
+
+            if is_visible {
+                last_value += value;
+            }
+
+            let tooltip = if let Some(renderer) = &props.render_tooltip {
+                renderer.apply(&(title.clone(), *value, index))
+            } else {
+                let pct = (value / sum * 100.0).round();
+                format!("{}: {} ({pct}%)", title, value).into()
+            };
+
+            let color = props.segment_get_color(index, *value);
+
+            segments.push(Segment {
+                dasharray,
+                offset,
+                highlight_offset,
+                tooltip,
+                color,
+                allow_highlight,
+            });
+        }
+
+        self.segments = segments;
+    }
+
+    fn render_legend(&self, ctx: &Context<Self>) -> Option<Container> {
+        let props = ctx.props();
+        let direction = match props.effective_legend_position() {
+            LegendPosition::Hidden => return None,
+            LegendPosition::Start | LegendPosition::End => css::FlexDirection::Column,
+            LegendPosition::Top | LegendPosition::Bottom => css::FlexDirection::Row,
+        };
+        let legend = Container::new()
+            .class("pwt-gap-1")
+            .class(css::Display::Flex)
+            .class(direction)
+            .class(css::AlignItems::Stretch)
+            .children(self.segments.iter().enumerate().map(|(index, segment)| {
+                let visible = !self.hidden.contains(&index);
+                let opacity = if visible { 100 } else { 10 };
+                Button::new(props.values[index].0.to_string())
+                    .icon_class("fa fa-circle")
+                    .icon_style(format!("color: {}; opacity: {opacity}%;", segment.color))
+                    .on_activate(ctx.link().callback(move |_| Msg::ToggleValue(index)))
+                    .onpointerenter(ctx.link().callback(move |_| Msg::Highlight(Some(index))))
+                    .onpointerleave(ctx.link().callback(move |_| Msg::Highlight(None)))
+                    .into()
+            }));
+        Some(legend)
+    }
+
+    fn render_tooltip(&self, ctx: &Context<Self>) -> Option<Html> {
+        let props = ctx.props();
+        match (props.show_tooltip, self.mouse_pos, self.highlight) {
+            (true, Some(_), Some(index)) => Some(
+                Container::new()
+                    .attribute("role", "tooltip")
+                    .attribute("aria-live", "polite")
+                    .attribute("data-show", Some(""))
+                    .class("pwt-tooltip")
+                    .class("pwt-tooltip-rich")
+                    .with_child(self.segments[index].tooltip.clone())
+                    .into_html_with_ref(self.tooltip_ref.clone()),
+            ),
+            _ => None,
+        }
+    }
+}
+
+impl Component for PwtPieChart {
+    type Message = Msg;
+    type Properties = PieChart;
+
+    fn create(ctx: &Context<Self>) -> Self {
+        Self::new(ctx)
+    }
+
+    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
+        self.recalculate_draw_values(ctx);
+        if ctx.props().values != old_props.values {
+            self.hidden = HashSet::new();
+            self.mouse_pos = None;
+            self.highlight = None;
+        }
+        self.recalculate_segments(ctx);
+        true
+    }
+
+    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Msg::Highlight(highlight) => self.highlight = highlight,
+            Msg::MouseOver(pos) => {
+                if let Some((x, y)) = pos {
+                    self.mouse_pos = Some((x as f64, y as f64));
+                } else {
+                    self.mouse_pos = None;
+                }
+            }
+            Msg::ToggleValue(index) => {
+                if self.hidden.contains(&index) {
+                    self.hidden.remove(&index);
+                    self.recalculate_segments(ctx);
+                } else if self.hidden.len() + 1 < ctx.props().values.len() {
+                    self.hidden.insert(index);
+                    self.recalculate_segments(ctx);
+                }
+            }
+        }
+        true
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let props = ctx.props();
+
+        let mut group = Group::new().style("transform", "rotate(90deg)");
+
+        // background for the 'gauge' type chart
+        if props.values.len() == 1 {
+            group.add_child(
+                Circle::new()
+                    .fill("none")
+                    .r(self.radius)
+                    .stroke("var(--pwt-color-surface)")
+                    .stroke_width(self.stroke_width)
+                    .style(
+                        "stroke-dasharray",
+                        format!("{} {}", self.max_len, self.circumference),
+                    )
+                    .style("stroke-dashoffset", (-self.start).to_string())
+                    .style("transition", "0.3s"),
+            );
+        };
+
+        for (index, segment) in self.segments.iter().enumerate() {
+            let (x_off, y_off) = match self.highlight {
+                Some(idx) if idx == index && segment.allow_highlight => segment.highlight_offset,
+                _ => (0.0, 0.0),
+            };
+
+            group.add_child(
+                Circle::new()
+                    .fill("none")
+                    .r(self.radius)
+                    .stroke(segment.color.to_string())
+                    .stroke_width(self.stroke_width)
+                    .style("stroke-dasharray", segment.dasharray.clone())
+                    .style("stroke-dashoffset", segment.offset.to_string())
+                    .style("transition", "0.3s")
+                    .style("transform", format!("translate({x_off}px, {y_off}px)"))
+                    .onpointerenter(ctx.link().callback(move |_| Msg::Highlight(Some(index))))
+                    .onpointerleave(ctx.link().callback(|_| Msg::Highlight(None))),
+            );
+
+            // invisible copy of the original position so we don't flicker between positions when
+            // the mouse is positioned on parts of the segment that moves out of the way
+            group.add_child(
+                Circle::new()
+                    .fill("none")
+                    .r(self.radius)
+                    .stroke("transparent")
+                    .stroke_width(self.stroke_width)
+                    .style("stroke-dasharray", segment.dasharray.clone())
+                    .style("stroke-dashoffset", segment.offset.to_string())
+                    .onpointerenter(ctx.link().callback(move |_| Msg::Highlight(Some(index))))
+                    .onpointerleave(ctx.link().callback(|_| Msg::Highlight(None))),
+            );
+        }
+
+        let width = SIZE - (self.left_diff + self.right_diff);
+        let height = SIZE - self.bottom_diff;
+        let mut canvas = Canvas::new()
+            .onpointermove(ctx.link().callback(|event: PointerEvent| {
+                Msg::MouseOver(Some((event.client_x(), event.client_y())))
+            }))
+            .onpointerleave(ctx.link().callback(|_| Msg::MouseOver(None)))
+            .attribute(
+                "viewBox",
+                format!(
+                    "{} {} {} {}",
+                    -(width - self.left_diff + self.right_diff) / 2.0,
+                    -(height + self.bottom_diff) / 2.0,
+                    width,
+                    height
+                ),
+            )
+            .with_child(group);
+
+        canvas.add_optional_child(props.text.as_ref().map(|text| {
+            Text::new(text.to_string())
+                .dy(0)
+                .attribute("text-anchor", "middle")
+                .attribute("alignment-baseline", "central")
+        }));
+
+        Container::new()
+            .with_std_props(&props.std_props)
+            .listeners(&props.listeners)
+            .class(css::Display::Flex)
+            .class(css::AlignItems::Center)
+            .class(match props.effective_legend_position() {
+                LegendPosition::Hidden => css::FlexDirection::Column,
+                LegendPosition::Bottom => css::FlexDirection::Column,
+                LegendPosition::Top => css::FlexDirection::ColumnReverse,
+                LegendPosition::End => css::FlexDirection::Row,
+                LegendPosition::Start => css::FlexDirection::RowReverse,
+            })
+            .with_child(canvas.into_html_with_ref(self.svg_ref.clone()))
+            .with_optional_child(self.render_legend(ctx))
+            .with_optional_child(self.render_tooltip(ctx))
+            .into()
+    }
+
+    fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
+        if let Some((x, y)) = self.mouse_pos {
+            if let Some(tooltip_ref) = self.tooltip_ref.get() {
+                let _ = align_to_xy(
+                    tooltip_ref,
+                    (x + 20.0, y + 20.0),
+                    crate::dom::align::Point::TopStart,
+                );
+            }
+        }
+    }
+}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 0df2cbf..176e7c9 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -28,6 +28,8 @@ pub use catalog_loader::CatalogLoader;
 #[doc(hidden)]
 pub use catalog_loader::PwtCatalogLoader;
 
+pub mod charts;
+
 mod column;
 pub use column::Column;
 
-- 
2.47.3





^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-03-20 16:08 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-20 16:03 [RFC PATCH yew-widget-toolkit 0/3] implement pie chart widget Dominik Csapak
2026-03-20 16:03 ` [RFC PATCH yew-widget-toolkit 1/3] widget: button: add 'icon_style' property Dominik Csapak
2026-03-20 16:03 ` [RFC PATCH yew-widget-toolkit 2/3] macros: widget: impl WidgetStyleBuilder for svgs Dominik Csapak
2026-03-20 16:04 ` [RFC PATCH yew-widget-toolkit 3/3] add pie chart widget Dominik Csapak

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal