plot
Header-only C++ SVG charts — heatmap, line, scatter — GR-style API + Solarized themes
Loading...
Searching...
No Matches
density.hpp
1/* Copyright (c) 2026 Steven Varga, Toronto, ON, Canada
2 * MIT License — see LICENSE
3 *
4 * plot::density(values, opts...) — a deferred view (plot::view) drawing a
5 * Gaussian kernel-density estimate as a smooth polyline over an x-grid. The
6 * bandwidth is plot::bandwidth{h}; the default is Silverman's rule of thumb
7 * h = 1.06 * sigma * n^(-1/5).
8 *
9 * Built on the #5 view pattern + impl::render_panel_into. Dependency-free.
10 */
11#ifndef PLOT_DENSITY_HPP
12#define PLOT_DENSITY_HPP
13
14#include <string>
15#include <vector>
16#include <tuple>
17#include <utility>
18#include <algorithm>
19#include <cmath>
20#include <cstddef>
21#include <cstdint>
22
23#include "tags.hpp"
24#include "meta.hpp"
25#include "attributes.hpp"
26#include "canvas.hpp"
27#include "theme.hpp"
28#include "gr.hpp"
29#include "view.hpp"
30
31namespace plot {
32 // KDE bandwidth override (h <= 0 ⇒ fall back to Silverman's rule).
33 struct bandwidth { using value_type = tag::bandwidth_t; double value; };
34}
35
36namespace plot::impl {
37 template <class... opt_t>
38 double bandwidth_of(const std::vector<double>& v, const opt_t&... opts){
39 using bw_t = typename arg::tpos<tag::bandwidth_t, opt_t...>;
40 if constexpr( bw_t::present ){
41 auto tuple = std::forward_as_tuple(opts...);
42 double h = std::get<bw_t::position>(tuple).value;
43 if( h > 0 ) return h;
44 }
45 // Silverman: 1.06 * sigma * n^(-1/5).
46 const std::size_t n = v.size();
47 if( n < 2 ) return 1.0;
48 double mean = 0; for(double d : v) mean += d; mean /= double(n);
49 double var = 0; for(double d : v) var += (d-mean)*(d-mean); var /= double(n-1);
50 double sigma = std::sqrt(var > 0 ? var : 1.0);
51 double h = 1.06 * sigma * std::pow(double(n), -1.0/5.0);
52 return h > 0 ? h : 1.0;
53 }
54
55 template <class... opt_t>
56 struct density_view {
57 using value_type = tag::view_t;
58 std::vector<double> values;
59 std::tuple<opt_t...> opts;
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{640}, std::size_t{400}, 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 bw = impl::bandwidth_of(values, o...);
70
71 auto [vmn, vmx] = impl::minmax_of(values);
72 // pad the grid by 3 bandwidths so the tails are visible.
73 double xlo = vmn - 3.0*bw, xhi = vmx + 3.0*bw;
74 if( xhi <= xlo ) xhi = xlo + 1.0;
75
76 const std::size_t G = 200;
77 std::vector<double> gx(G), gy(G);
78 constexpr double two_pi = 6.283185307179586;
79 const double inv = 1.0 / (std::sqrt(two_pi) * bw * double(values.size() ? values.size() : 1));
80 double dmax = 0.0;
81 for(std::size_t g=0; g<G; ++g){
82 double xq = xlo + (xhi - xlo) * double(g)/double(G-1);
83 double acc = 0.0;
84 for(double v : values){
85 double u = (xq - v)/bw;
86 acc += std::exp(-0.5*u*u);
87 }
88 double dens = acc * inv;
89 gx[g] = xq; gy[g] = dens;
90 if( dens > dmax ) dmax = dens;
91 }
92 if( dmax <= 0.0 ) dmax = 1.0;
93
94 impl::scale_t sx = impl::make_scale(xlo, xhi, false, 10.0);
95 impl::scale_t sy = impl::make_scale(0.0, dmax, false, 10.0);
96 impl::render_panel_into(cv, x, y,
97 static_cast<std::size_t>(w), static_cast<std::size_t>(h),
98 th, sx, sy, title, xl, yl, {},
99 [&](impl::canvas_t& c, auto px, auto py){
100 using attribute_t = plot::attribute::element_t;
101 std::vector<float> xpx, ypx;
102 for(std::size_t g=0; g<G; ++g){ xpx.push_back(px(gx[g])); ypx.push_back(py(gy[g])); }
103 attribute_t a;
104 a.color = plot::attribute::color_t{ th.series.empty()? th.fg : th.series[0] };
105 a.stroke = plot::attribute::stroke_t{1.0f, 1.8f, {}, {}, {}};
106 c.poly_line(xpx, ypx, a);
107 });
108 }, opts);
109 }
110 };
111}
112
113namespace plot {
114 template <class... opt_t>
115 impl::density_view<opt_t...> density(std::vector<double> values, opt_t... opts){
116 return impl::density_view<opt_t...>{ std::move(values), std::make_tuple(opts...) };
117 }
118}
119#endif