plot
Header-only C++ SVG charts — heatmap, line, scatter — GR-style API + Solarized themes
Loading...
Searching...
No Matches
canvas.hpp
1/* Copyright (c) 2026 Steven Varga, Toronto, ON, Canada
2 * MIT License — see LICENSE
3 *
4 * The SVG canvas: opens an <svg> document on construction, writes primitives
5 * (rect/text/line/poly_line/circle/group) via std::format, and closes it on
6 * destruction. Ported from plot:: into plot::; the original matrix and
7 * formatting third-party dependencies are removed (std::format only).
8 * Dependency-free.
9 */
10#ifndef PLOT_CANVAS_HPP
11#define PLOT_CANVAS_HPP
12
13#include <string>
14#include <string_view>
15#include <fstream>
16#include <ostream>
17#include <array>
18#include <vector>
19#include <variant>
20#include <optional>
21#include <functional>
22#include <format>
23#include <stdexcept>
24#include <cstddef>
25
26#include "attributes.hpp"
27#include "utils.hpp"
28
29namespace plot::impl {
30 struct canvas_t {
31 using color_t = plot::attribute::color_t;
32 using font_t = plot::attribute::font_t;
33 using stroke_t = plot::attribute::stroke_t;
34 using opacity_t = plot::attribute::opacity_t;
35 using id_t = plot::attribute::id_t;
36 using position_t = plot::attribute::position_t;
37 using attribute_t = plot::attribute::element_t;
38 using align_t = plot::attribute::align_t;
39
40 // stream-backed: render into any std::ostream (file or in-memory)
41 canvas_t(std::ostream& os, std::size_t x, std::size_t y, const std::array<float,4>& margin);
42 ~canvas_t();
43
44 void group (std::variant<std::size_t, align_t> x, std::variant<std::size_t, align_t> y,
45 const attribute_t& attr, std::function<void()> const& call );
46 // translate + uniform scale group: emits transform="translate(x y) scale(s)".
47 // Used by plot::grid to fit a view's natural layout into its cell rect.
48 void group_scaled (float x, float y, float scale, std::function<void()> const& call );
49 void rect(float x, float y, float width, float height, float rx, float ry, const attribute_t& attr );
50 void circle(float cx, float cy, float radius, const attribute_t& attr );
51 void line(float x1, float y1, float x2, float y2, const attribute_t& attr );
52 void poly_line(const std::vector<float>& x, const std::vector<float>& y, const attribute_t& attr );
53 void polygon(const std::vector<float>& x, const std::vector<float>& y, const attribute_t& attr );
54 static std::pair<std::size_t,std::size_t>
55 bounding_box( const std::vector<std::string>& x_axis, double angle_x,
56 const std::vector<std::string>& y_axis, double angle_y, std::size_t font_size );
57 void text( const std::string& txt,
58 std::variant<std::size_t, align_t> x, std::variant<std::size_t, align_t> y, const attribute_t& attr );
59
60 std::optional<float> grid_x, grid_y;
61
62 private:
63 // std::format requires a consteval format string, but the SVG templates
64 // below are runtime std::string members — so route every call through
65 // std::vformat, which accepts a runtime format string.
66 template <class... Args>
67 static std::string fmt(std::string_view f, Args... args){
68 return std::vformat(f, std::make_format_args(args...));
69 }
70 void align( std::optional<align_t> alignment );
71 float align_x( std::variant<std::size_t, align_t> x );
72 float align_y( std::variant<std::size_t, align_t> y, const std::optional<font_t>& font);
73
74 std::size_t width, height;
75 float font_size = 0;
76 std::array<float,4> margin;
77 static constexpr double rad = 0.017453292519943295;
78 std::ostream& os;
79
80 const std::string gr_begin_ = "<g {}>\n";
81 const std::string gr_end_ = "</g>\n";
82 const std::string rotate_ = " rotate({} {} {})";
83 const std::string translate_ = "translate({} {})";
84 const std::string translate_scale_ = "translate({:.3f} {:.3f}) scale({:.5f})";
85 const std::string transform_ = " transform=\"{}\"";
86
87 const std::string svg_start_ = "<svg viewBox=\"{} {} {} {}\" xmlns=\"http://www.w3.org/2000/svg\">\n";
88 const std::string svg_end_ = "</svg>\n";
89 const std::string href_begin_ = "<a href=\"{}\">";
90 const std::string href_end_ = "</a>\n";
91
92 const std::string text_anchor_ = " text-anchor=\"{}\"";
93 const std::string font_ = " font-size=\"{}\" font-family=\"{}\" font-weight=\"{}\"";
94 const std::string fill_ = " fill=\"#{:06X}\"";
95 const std::string stroke_attr_ = " stroke=\"#{:06X}\" stroke-width=\"{:.2f}\"";
96 const std::array<std::string, 5> align_horizontal = {"start", "middle", "end", "", ""};
97
98 const std::string rect_ = "<rect x=\"{:.2f}\" y=\"{:.2f}\" width=\"{:.2f}\" height=\"{:.2f}\" rx=\"{:.2f}\" ry=\"{:.2f}\" {}>{}</rect>\n";
99 const std::string line_ = "<line x1=\"{:.2f}\" y1=\"{:.2f}\" x2=\"{:.2f}\" y2=\"{:.2f}\"{}/>\n";
100 const std::string polyline_ = "<polyline points=\"{}\" fill=\"none\"{}/>\n";
101 const std::string polygon_ = "<polygon points=\"{}\"{}>{}</polygon>\n";
102 const std::string circle_ = "<circle cx=\"{:.2f}\" cy=\"{:.2f}\" r=\"{:.2f}\" {}>{}</circle>\n";
103 const std::string text_ = "<text x=\"{:.2f}\" y=\"{:.2f}\"{}>{}</text>\n";
104 const std::string title_ = "<title>{}</title>";
105 };
106}
107
108namespace plot::impl {
109 template <class Derived >
110 struct io_t {
111 void ostream( impl::canvas_t& ) const {
112 }
113
114 friend impl::canvas_t& operator<<(impl::canvas_t& cs, const io_t<Derived>& attr){
115 const Derived& derived = static_cast<const Derived&>(attr);
116 derived.ostream( cs );
117 return cs;
118 }
119 };
120}
121
122inline plot::impl::canvas_t::canvas_t( std::ostream& os, std::size_t width, std::size_t height, const std::array<float,4>& margin )
123 : width(width), height(height), margin(margin), os(os) {
124
125 os << fmt( svg_start_, 0,0, width,height);
126}
127
128inline plot::impl::canvas_t::~canvas_t(){
129 os << svg_end_;
130}
131
132
133inline void plot::impl::canvas_t::group (std::variant<std::size_t, align_t> x, std::variant<std::size_t, align_t> y,
134 const attribute_t& attr, std::function<void()> const& call ){
135 float _x = align_x( x ); float _y = align_y( y, attr.font );
136
137 std::string _attr = fmt(transform_, fmt(translate_, _x, _y));
138 if( attr.font ) _attr += fmt(font_, attr.font->size, attr.font->family, attr.font->weight);
139 if( attr.color ) _attr += fmt(fill_, static_cast<unsigned>(attr.color.value()) );
140
141 os << fmt(gr_begin_, _attr); // group has properties set
142 call();
143 os << gr_end_;
144}
145
146inline void plot::impl::canvas_t::group_scaled (float x, float y, float scale,
147 std::function<void()> const& call ){
148 std::string _attr = fmt(transform_, fmt(translate_scale_, x, y, scale));
149 os << fmt(gr_begin_, _attr);
150 call();
151 os << gr_end_;
152}
153
154inline std::pair<std::size_t,std::size_t>
155plot::impl::canvas_t::bounding_box( const std::vector<std::string>&, double,
156 const std::vector<std::string>&, double, std::size_t ){
157
158 return std::make_pair(std::size_t{0}, std::size_t{0});
159}
160
161inline void plot::impl::canvas_t::rect(float x, float y, float width, float height, float rx, float ry,
162 const attribute_t& attr){
163 std::string _attr, _lbl;
164 if( attr.color ) _attr += fmt(fill_, static_cast<unsigned>(attr.color.value()) );
165 if( attr.label ) _lbl = fmt(title_, util::html_escape(attr.label.value()));
166
167 if( attr.href ) os << fmt(href_begin_, attr.href.value());
168 os << fmt(rect_, x, y, width, height, rx, ry, _attr, _lbl );
169 if( attr.href ) os << fmt(href_end_);
170}
171
172inline void plot::impl::canvas_t::circle(float cx, float cy, float radius, const attribute_t& attr){
173 std::string _attr, _lbl;
174 if( attr.color ) _attr += fmt(fill_, static_cast<unsigned>(attr.color.value()) );
175 if( attr.stroke ) _attr += fmt(stroke_attr_, attr.color ? static_cast<unsigned>(attr.color.value()) : 0u, attr.stroke->width);
176 if( attr.label ) _lbl = fmt(title_, util::html_escape(attr.label.value()));
177
178 if( attr.href ) os << fmt(href_begin_, attr.href.value());
179 os << fmt(circle_, cx, cy, radius, _attr, _lbl );
180 if( attr.href ) os << fmt(href_end_);
181}
182
183inline void plot::impl::canvas_t::line(float x1, float y1, float x2, float y2, const attribute_t& attr){
184 const unsigned color = attr.color ? static_cast<unsigned>(attr.color.value()) : 0u;
185 const float w = attr.stroke ? attr.stroke->width : 1.0f;
186 os << fmt(line_, x1, y1, x2, y2, fmt(stroke_attr_, color, w));
187}
188
189inline void plot::impl::canvas_t::poly_line(const std::vector<float>& x, const std::vector<float>& y,
190 const attribute_t& attr){
191 const unsigned color = attr.color ? static_cast<unsigned>(attr.color.value()) : 0u;
192 const float w = attr.stroke ? attr.stroke->width : 1.0f;
193 std::string points;
194 const std::size_t n = x.size() < y.size() ? x.size() : y.size();
195 for(std::size_t i=0; i<n; i++){
196 if(i) points += ' ';
197 points += std::format("{:.2f},{:.2f}", x[i], y[i]);
198 }
199 os << fmt(polyline_, points, fmt(stroke_attr_, color, w));
200}
201
202inline void plot::impl::canvas_t::polygon(const std::vector<float>& x, const std::vector<float>& y,
203 const attribute_t& attr){
204 std::string points;
205 const std::size_t n = x.size() < y.size() ? x.size() : y.size();
206 for(std::size_t i=0; i<n; i++){
207 if(i) points += ' ';
208 points += std::format("{:.2f},{:.2f}", x[i], y[i]);
209 }
210
211 std::string _attr, _lbl;
212 if( attr.color ) _attr += fmt(fill_, static_cast<unsigned>(attr.color.value()) );
213 if( attr.stroke ) _attr += fmt(stroke_attr_, attr.color ? static_cast<unsigned>(attr.color.value()) : 0u, attr.stroke->width);
214 if( attr.label ) _lbl = fmt(title_, util::html_escape(attr.label.value()));
215
216 if( attr.href ) os << fmt(href_begin_, attr.href.value());
217 os << fmt(polygon_, points, _attr, _lbl );
218 if( attr.href ) os << fmt(href_end_);
219}
220
221inline void plot::impl::canvas_t::text( const std::string& txt,
222 std::variant<std::size_t, align_t> x, std::variant<std::size_t, align_t> y, const attribute_t& attr ){
223 float _x = align_x( x ); float _y = align_y( y, attr.font );
224 int i = std::holds_alternative<std::size_t>( x ) ? 0 : static_cast<int>(std::get<align_t>(x));
225
226 std::string _attr;
227 if( attr.font ) _attr += fmt(font_, attr.font->size, attr.font->family, attr.font->weight);
228 if( attr.color ) _attr += fmt(fill_, static_cast<unsigned>(attr.color.value()) );
229 if( attr.rotate ) _attr += fmt(transform_, fmt(rotate_, attr.rotate->value, _x, _y));
230 _attr += fmt(text_anchor_, align_horizontal[i]);
231
232 os << fmt(text_, _x, _y, _attr, util::html_escape(txt) );
233}
234
235inline void plot::impl::canvas_t::align( std::optional<align_t> x ) {
236 if( !x ) return;
237 os << fmt(text_anchor_, align_horizontal[ static_cast<int>(x.value()) ]);
238}
239
240inline float plot::impl::canvas_t::align_x( std::variant<std::size_t, align_t> x ) {
241 if( std::holds_alternative<std::size_t>( x ) )
242 return static_cast<float>(std::get<std::size_t>( x ));
243 switch( std::get<align_t>(x) ){
244 case align_t::left: return std::get<0>(margin);
245 case align_t::right: return width - std::get<2>(margin);
246 case align_t::center: return width / 2 - std::get<0>(margin);
247 default: throw std::runtime_error("fixme: #169");
248 }
249 return std::get<0>(margin);
250}
251
252inline float plot::impl::canvas_t::align_y( std::variant<std::size_t, align_t> y, const std::optional<font_t>& font ) {
253 if( std::holds_alternative<std::size_t>( y ) )
254 return static_cast<float>(std::get<std::size_t>( y ));
255 switch( std::get<align_t>(y) ){
256 case align_t::top: return std::get<1>(margin) + (font ? font->size : 0);
257 case align_t::bottom: return height - std::get<3>(margin);
258 case align_t::center: return height / 2 - std::get<1>(margin);
259 default:
260 throw std::runtime_error("fixme: 182");
261 break;
262 }
263 return std::get<1>(margin);
264}
265#endif