plot
Header-only C++ SVG charts — heatmap, line, scatter — GR-style API + Solarized themes
Loading...
Searching...
No Matches
pie.hpp
1/* Copyright (c) 2026 Steven Varga, Toronto, ON, Canada
2 * MIT License — see LICENSE
3 *
4 * plot::pie(labels, values, opts...) and plot::donut(labels, values, opts...) —
5 * deferred views (plot::view) drawing a pie / donut. donut defaults to a 0.55
6 * inner-radius hole; adjust either with plot::hole{r} (r in (0,1)). Each slice is
7 * a FILLED <polygon> wedge: the arc is segmented into short chords, centre→arc→
8 * centre for a pie, or outer-arc→inner-arc for a donut ring.
9 * Slices are filled with the theme colour. Slice colours cycle theme.series; a
10 * label is placed at the slice's mid-angle.
11 *
12 * Built on the #5 view pattern (draws straight into the cell). Dependency-free.
13 */
14#ifndef PLOT_PIE_HPP
15#define PLOT_PIE_HPP
16
17#include <string>
18#include <vector>
19#include <tuple>
20#include <utility>
21#include <algorithm>
22#include <cmath>
23#include <cstddef>
24#include <cstdint>
25
26#include "tags.hpp"
27#include "meta.hpp"
28#include "attributes.hpp"
29#include "canvas.hpp"
30#include "theme.hpp"
31#include "gr.hpp"
32#include "view.hpp"
33
34namespace plot {
35 // inner-radius hole fraction in (0,1); 0 (or absent) ⇒ a full pie. Set by
36 // plot::donut (default 0.55) and accepted by plot::pie too.
37 struct hole { using value_type = tag::hole_t; double value; };
38}
39
40namespace plot::impl {
41 template <class... opt_t>
42 double hole_of(double fallback, const opt_t&... opts){
43 using d_t = typename arg::tpos<tag::hole_t, opt_t...>;
44 if constexpr( d_t::present ){
45 auto tuple = std::forward_as_tuple(opts...);
46 double r = std::get<d_t::position>(tuple).value;
47 if( r < 0 ) r = 0; else if( r > 0.95 ) r = 0.95;
48 return r;
49 }
50 return fallback;
51 }
52
53 template <class... opt_t>
54 struct pie_view {
55 using value_type = tag::view_t;
56 std::vector<std::string> labels;
57 std::vector<double> values;
58 std::tuple<opt_t...> opts;
59 double default_inner = 0.0; // plot::pie ⇒ 0 (full); plot::donut ⇒ 0.55
60
61 std::pair<std::size_t,std::size_t> natural() const {
62 return std::apply([](const auto&... o){
63 return impl::natural_size(std::size_t{500}, std::size_t{500}, o...); }, opts);
64 }
65 void draw_into(canvas_t& cv, float x, float y, float w, float h) const {
66 std::apply([&](const auto&... o){
67 const theme_t& th = impl::resolve_theme(o...);
68 auto [title, xl, yl] = impl::texts(o...);
69 const double inner = impl::hole_of(default_inner, o...);
70 using attribute_t = plot::attribute::element_t;
71
72 const std::size_t n = std::min(labels.size(), values.size());
73 double total = 0.0; for(std::size_t i=0;i<n;++i) if(values[i]>0) total += values[i];
74 if( total <= 0.0 ) total = 1.0;
75
76 cv.group(static_cast<std::size_t>(x), static_cast<std::size_t>(y), attribute_t{},
77 [&](){
78 { attribute_t bg; bg.color = plot::attribute::color_t{ th.bg };
79 cv.rect(0,0, w, h, 0,0, bg); }
80 float top = title.empty() ? 8.0f : 26.0f;
81 if( !title.empty() ){
82 attribute_t a; a.color = plot::attribute::color_t{ th.fg };
83 a.font = plot::attribute::font_t{"Arial, sans-serif", "bold", 13u};
84 a.align = plot::attribute::align_t::center;
85 cv.text(title, std::size_t(w/2), std::size_t(16), a);
86 }
87 float cx = w*0.5f, cy = top + (h - top)*0.5f;
88 float Ro = std::max(8.0f, std::min(w, h - top)*0.5f - 30.0f);
89 float Ri = float(inner) * Ro;
90
91 double a0 = -1.5707963267948966; // start at 12 o'clock
92 const double TWO_PI = 6.283185307179586;
93 for(std::size_t i=0;i<n;++i){
94 double frac = (values[i] > 0 ? values[i] : 0.0) / total;
95 double a1 = a0 + frac*TWO_PI;
96 // segment the arc into ~enough chords for smoothness.
97 std::size_t segs = std::max<std::size_t>(2,
98 static_cast<std::size_t>(std::ceil(frac*64.0)));
99 std::vector<float> X, Y;
100 // outer arc a0 -> a1
101 for(std::size_t s=0;s<=segs;++s){
102 double a = a0 + (a1-a0)*double(s)/double(segs);
103 X.push_back(cx + Ro*float(std::cos(a)));
104 Y.push_back(cy + Ro*float(std::sin(a)));
105 }
106 if( Ri > 0.5f ){
107 // inner arc a1 -> a0 (reverse) for a ring wedge.
108 for(std::size_t s=0;s<=segs;++s){
109 double a = a1 + (a0-a1)*double(s)/double(segs);
110 X.push_back(cx + Ri*float(std::cos(a)));
111 Y.push_back(cy + Ri*float(std::sin(a)));
112 }
113 } else {
114 X.push_back(cx); Y.push_back(cy); // pie apex
115 }
116 // close the wedge.
117 X.push_back(X.front()); Y.push_back(Y.front());
118
119 std::uint32_t col = th.series.empty()? th.fg
120 : th.series[i % th.series.size()];
121 attribute_t pa; pa.color = plot::attribute::color_t{ col };
122 // subtle separator stroke in the background colour so
123 // adjacent filled slices read as distinct wedges.
124 pa.stroke = plot::attribute::stroke_t{1.0f, 1.0f, {}, {}, {}};
125 cv.polygon(X, Y, pa);
126
127 // label at the slice mid-angle, just outside the wedge.
128 double am = 0.5*(a0+a1);
129 float lr = Ro + 12.0f;
130 attribute_t ta; ta.color = plot::attribute::color_t{ th.fg };
131 ta.font = plot::attribute::font_t{"Arial, sans-serif", "normal", 10u};
132 ta.align = plot::attribute::align_t::center;
133 float lx = cx + lr*float(std::cos(am));
134 float ly = cy + lr*float(std::sin(am));
135 cv.text(labels[i], std::size_t(lx<0?0:lx), std::size_t(ly+3), ta);
136 a0 = a1;
137 }
138 });
139 }, opts);
140 }
141 };
142}
143
144namespace plot {
145 template <class... opt_t>
146 impl::pie_view<opt_t...> pie(std::vector<std::string> labels,
147 std::vector<double> values, opt_t... opts){
148 return impl::pie_view<opt_t...>{ std::move(labels), std::move(values),
149 std::make_tuple(opts...) }; // default_inner = 0 → full pie
150 }
151
152 // donut: a pie with a default inner-radius hole (0.55); adjust with plot::hole{}.
153 template <class... opt_t>
154 impl::pie_view<opt_t...> donut(std::vector<std::string> labels,
155 std::vector<double> values, opt_t... opts){
156 return impl::pie_view<opt_t...>{ std::move(labels), std::move(values),
157 std::make_tuple(opts...), 0.55 };
158 }
159}
160#endif