plot
Header-only C++ SVG charts — heatmap, line, scatter — GR-style API + Solarized themes
Loading...
Searching...
No Matches
hexbin.hpp
1/* Copyright (c) 2026 Steven Varga, Toronto, ON, Canada
2 * MIT License — see LICENSE
3 *
4 * plot::hexbin(grid, opts...) — a deferred view (plot::view): a HEXAGONAL variant
5 * of the continuous heatmap. Each cell of a row-major plot::mat<double> renders
6 * as a flat-topped hexagon (a filled <polygon> of 6 vertices) coloured along
7 * theme.gradient by the cell's value normalised across the
8 * grid's [min,max]. Alternate rows are offset by half a hex width for the classic
9 * honeycomb tessellation.
10 *
11 * A separate header from heatmap.hpp (left untouched). Built on the #5 view
12 * pattern (draws straight into the cell). Dependency-free.
13 */
14#ifndef PLOT_HEXBIN_HPP
15#define PLOT_HEXBIN_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#include <limits>
26#include <type_traits>
27
28#include "tags.hpp"
29#include "meta.hpp"
30#include "attributes.hpp"
31#include "canvas.hpp"
32#include "theme.hpp"
33#include "gr.hpp"
34#include "heatmap.hpp" // plot::mat<T>
35#include "view.hpp"
36
37namespace plot {
38 // style marker selecting the hexagonal heatmap layout (e.g. heatmap(..,hex{})).
39 struct hex { using value_type = tag::hex_t; };
40}
41
42namespace plot::impl {
43 template <class... opt_t>
44 struct hexbin_view {
45 using value_type = tag::view_t;
46 std::vector<double> data; // owned, row-major
47 std::size_t rows = 0, cols = 0;
48 std::tuple<opt_t...> opts;
49
50 std::pair<std::size_t,std::size_t> natural() const {
51 return std::apply([](const auto&... o){
52 return impl::natural_size(std::size_t{560}, std::size_t{460}, o...); }, opts);
53 }
54 double at(std::size_t i, std::size_t j) const { return data[i*cols + j]; }
55
56 void draw_into(canvas_t& cv, float x, float y, float w, float h) const {
57 std::apply([&](const auto&... o){
58 const theme_t& th = impl::resolve_theme(o...);
59 auto [title, xl, yl] = impl::texts(o...);
60 using attribute_t = plot::attribute::element_t;
61
62 double lo = std::numeric_limits<double>::infinity();
63 double hi = -std::numeric_limits<double>::infinity();
64 for(double d : data){ if(d<lo) lo=d; if(d>hi) hi=d; }
65 if( !(lo<=hi) ){ lo=0; hi=1; }
66 const double span = (hi>lo) ? (hi-lo) : 1.0;
67
68 cv.group(static_cast<std::size_t>(x), static_cast<std::size_t>(y), attribute_t{},
69 [&](){
70 { attribute_t bg; bg.color = plot::attribute::color_t{ th.bg };
71 cv.rect(0,0, w, h, 0,0, bg); }
72 float top = title.empty() ? 8.0f : 26.0f;
73 if( !title.empty() ){
74 attribute_t a; a.color = plot::attribute::color_t{ th.fg };
75 a.font = plot::attribute::font_t{"Arial, sans-serif", "bold", 13u};
76 a.align = plot::attribute::align_t::center;
77 cv.text(title, std::size_t(w/2), std::size_t(16), a);
78 }
79 if( rows == 0 || cols == 0 ) return;
80
81 const float pad = 8.0f;
82 const float availW = w - 2*pad;
83 const float availH = (h - top) - 2*pad;
84 // pointy-top hex grid: horizontal pitch = sqrt(3)*r, with a
85 // half-pitch offset on odd rows; vertical pitch = 1.5*r.
86 // solve r from the available box for (cols + 0.5) columns and
87 // (rows*1.5 + 0.5) rows.
88 const float sqrt3 = 1.7320508075688772f;
89 float rW = availW / (sqrt3 * (float(cols) + 0.5f));
90 float rH = availH / (1.5f * float(rows) + 0.5f);
91 float r = std::max(2.0f, std::min(rW, rH));
92 float hpitch = sqrt3 * r;
93 float vpitch = 1.5f * r;
94 float ox = pad + hpitch*0.5f;
95 float oy = top + pad + r;
96
97 for(std::size_t i=0;i<rows;++i)
98 for(std::size_t j=0;j<cols;++j){
99 float cx = ox + float(j)*hpitch + ((i & 1) ? hpitch*0.5f : 0.0f);
100 float cy = oy + float(i)*vpitch;
101 double t = (at(i,j) - lo)/span;
102 std::uint32_t col = impl::gradient3(th.gradient, t);
103 // pointy-top hexagon: 6 vertices at 30°,90°,...,330°.
104 std::vector<float> X, Y;
105 for(int k=0;k<6;++k){
106 double a = (3.141592653589793/180.0)*(60.0*double(k) + 30.0);
107 X.push_back(cx + r*float(std::cos(a)));
108 Y.push_back(cy + r*float(std::sin(a)));
109 }
110 X.push_back(X.front()); Y.push_back(Y.front()); // close
111 attribute_t ha; ha.color = plot::attribute::color_t{ col };
112 ha.stroke = plot::attribute::stroke_t{1.0f, 1.0f, {}, {}, {}};
113 cv.polygon(X, Y, ha);
114 }
115 });
116 }, opts);
117 }
118 };
119}
120
121namespace plot {
122 template <class T, class... opt_t,
123 class = std::enable_if_t<std::is_arithmetic_v<T>>>
124 impl::hexbin_view<opt_t...> hexbin(const mat<T>& grid, opt_t... opts){
125 impl::hexbin_view<opt_t...> v{ {}, grid.rows, grid.cols, std::make_tuple(opts...) };
126 v.data.assign(grid.begin(), grid.end());
127 return v;
128 }
129}
130#endif