izzi
SVG SUBSET C++ API
Loading...
Searching...
No Matches
a60-svg-graphs-line.h
Go to the documentation of this file.
1// izzi line graphs -*- mode: C++ -*-
2
3// Copyright (c) 2025, Benjamin De Kosnik <b.dekosnik@gmail.com>
4
5// This file is part of the alpha60 library. This library is free
6// software; you can redistribute it and/or modify it under the terms
7// of the GNU General Public License as published by the Free Software
8// Foundation; either version 3, or (at your option) any later
9// version.
10
11// This library is distributed in the hope that it will be useful, but
12// WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14// General Public License for more details.
15
16#ifndef izzi_SVG_GRAPHS_LINE_H
17#define izzi_SVG_GRAPHS_LINE_H 1
18
19#include <set>
20
21#include "izzi-json-basics.h"
24#include "a60-svg-markers.h"
25
26
27/// Polyline/line creation options.
28/// 1: use one line with css dasharray and markers mid, end points
29/// 2: use two lines one with css dasharray, one with path tooltips
32
33namespace svg {
34
35
36/**
37 Line Graphs / Line Charts.
38
39 Some Example:
40 https://www.highcharts.com/demo/highcharts/accessible-line
41
42 Outline:
43
44 input has 2 columns: x, y
45 - how many x, what is range, what is delta
46 - how many y, what is range, what is delta
47
48 plot as grid/matrix system given above.
49
50 line: points, linestyle
51
52 x axis: title, tick mark spacing, tick mark style
53 y axis: title, tick mark spacing, tick mark style
54*/
55
56/// Per-graph constants, metadata, text.
58{
59 // Margins/Spaces
60 static constexpr uint marginx = 100;
61 static constexpr uint marginy = 100;
62 static constexpr uint xticdigits = 1; // sig digits xaxis
63 static constexpr uint yticdigits = 10; // number y tic labels
64
65 /// Glyph type sizes.
66 static constexpr uint ttitlesz = 16; // title large bold
67 static constexpr uint th1sz = 12; // h1
68 static constexpr uint tpsz = 10; // text, paragraph,
69 static constexpr uint tticsz = 7; // tic text
70
71 // Labels.
72 string title; // graph/chart title
73 string xlabel; // x axis label
74 string ylabel;
75 string xticu; // x axis tick mark units postfix
76 string yticu;
77
78 style lstyle; // line style
79 stroke_style sstyle; // stroke style, if any.
80};
81
82
83/// Return set of paths corresponding to marker shapes with tooltips.
84string
85make_line_graph_markers_tips(const vrange& points, const vrange& cpoints,
86 const graph_rstate& gstate, const double radius)
87{
88 string ret;
89 for (uint i = 0; i < points.size(); i++)
90 {
91 auto [ vx, vy ] = points[i];
92 auto [ cx, cy ] = cpoints[i];
93
94 // Generate displayed tooltip text....
95 string tipstr(gstate.title);
96 tipstr += k::newline;
97 tipstr += std::to_string(static_cast<uint>(vy));
98 tipstr += '%';
99 tipstr += k::comma;
100 tipstr += k::space;
101 tipstr += std::to_string(static_cast<uint>(vx));
102 tipstr += "ms";
103
104 const string& linecap = gstate.sstyle.linecap;
105 const bool roundp = linecap == "round" || linecap == "circle";
106 const bool squarep = linecap == "square";
107 const bool trianglep = linecap == "triangle";
108
109 // Markers default to closed paths that are filled with no stroke.
110 // Setting visible to vector | echo induces outline behavior.
111 style styl = gstate.lstyle;
112 styl._M_fill_opacity = 1;
113 if (gstate.is_visible(select::echo))
115
116
117 // Circle Centered.
118 // svg::circle_element c = make_circle(cpoints[i], gstate.lstyle, r);
119 if (roundp)
120 {
122 circle_element::data dc = { cx, cy, radius };
123 c.start_element();
124 c.add_data(dc);
125 c.add_style(styl);
127 c.add_title(tipstr);
128 c.add_raw(string { circle_element::pair_finish_tag } + k::newline);
129 ret += c.str();
130 }
131
132 // Square Centered.
133 // svg::rect_element r = (cpoints[i], gstate.lstyle, {2 * r, 2 * r});
134 if (squarep)
135 {
136 rect_element r;
137 rect_element::data dr = { cx - radius, cy - radius,
138 2 * radius, 2 * radius };
139 r.start_element();
140 r.add_data(dr);
141 r.add_style(styl);
143 r.add_title(tipstr);
144 r.add_raw(string { rect_element::pair_finish_tag } + k::newline);
145 ret += r.str();
146 }
147
148 // Triangle Centered.
149 // svg::path_element t = (cpoints[i], gstate.lstyle, {2 * r, 2 * r});
150 if (trianglep)
151 {
152 path_element p = make_path_triangle(cpoints[i], styl, radius, 120,
153 false);
154 p.add_title(tipstr);
155 p.add_raw(string { path_element::pair_finish_tag } + k::newline);
156 ret += p.str();
157 }
158
159 // Throw if marker style not supported.
160 if (!roundp && !squarep && !trianglep)
161 {
162 string m("make_line_graph_markers_tips:: ");
163 m += "linecap value invalid or missing, currently set to: ";
164 m += linecap;
165 m += k::newline;
166 throw std::runtime_error(m);
167 }
168 }
169 return ret;
170}
171
172
173/// Axis Labels
174/// Axis X/Y Ticmarks
175/// X line increments
176svg_element
178 const vrange& points,
179 const graph_rstate& gstate,
180 const double xscalein = 1, const double yscalein = 1,
181 const typography typo = k::apercu_typo)
182{
183 using namespace std;
184 svg_element lanno(gstate.title, "line graph annotation", aplate, false);
185
186 // Locate graph area on plate area.
187 auto [ pwidth, pheight ] = aplate;
188 double gwidth = pwidth - (2 * gstate.marginx);
189 double gheight = pheight - (2 * gstate.marginy);
190 const double chartyo = pheight - gstate.marginy;
191 const double chartxo = gstate.marginx;
192 const double chartxe = pwidth - gstate.marginx;
193
194 // Base typo for annotations.
195 typography anntypo = typo;
196 anntypo._M_style = k::wcaglg_style;
198
199 // Axes and Labels
200 if (gstate.is_visible(select::axis))
201 {
202 lanno.add_raw(group_element::start_group("axes-" + gstate.title));
203
204 // Add axis labels.
205 point_2t xlabelp = make_tuple(pwidth / 2, chartyo + (gstate.marginy / 2));
206 styled_text(lanno, gstate.xlabel, xlabelp, anntypo);
207
208 point_2t ylabelp = make_tuple(chartxo / 2, pheight / 2);
209 styled_text(lanno, gstate.ylabel, ylabelp, anntypo);
210
211 // Add axis lines.
212 line_element lx = make_line({chartxo, chartyo}, {chartxe, chartyo},
213 gstate.lstyle);
214 line_element ly = make_line({chartxo, chartyo}, {chartxo, gstate.marginy},
215 gstate.lstyle);
216 lanno.add_element(lx);
217 lanno.add_element(ly);
218
220 }
221
222 // Base typo for tic labels.
223 // NB: Assume pointsx/pointsy are monotonically increasing.
226
227 // Separate tic label values for each (x, y) axis, find ranges for each.
228 auto [ maxx, maxy ] = max_vrange(points, gstate.xticdigits, xscalein, yscalein);
229 auto minx = 0;
230 auto miny = 0;
231
232 const double xrange(maxx - minx);
233 const double xscale(gwidth / xrange);
234 const double yrange(maxy - miny);
235 const double yscale(gheight / yrange);
236
237 // Derive the number of tick marks.
238
239 // Use a multiple of 5 to make natural counting easier.
240 // Start with an assumption of 20 tic marks for the x axis.
241 double xtickn(xrange * 2); // .5 sec
242 if (xtickn < 10)
243 xtickn = 10;
244 if (xtickn > 26)
245 xtickn = xrange;
246
247 // X axis is seconds, xtickn minimum delta is 0.1 sec.
248 double xdelta = std::max(xrange / xtickn, 0.1);
249
250 // Round up to significant digits, so if xdelta is 0.18 round to 0.2.
251 xdelta = std::round(xdelta * gstate.xticdigits * 10) / (gstate.xticdigits * 10);
252
253 // Y axis is simpler, 0, 10, 20, ..., 80, 90, 100 in percent.
254 const double ydelta = yrange / gstate.yticdigits;
255
256 // Generate tic marks
257 const double ygo = gstate.marginy + gheight + graph_rstate::th1sz;
258 if (gstate.is_visible(select::ticks))
259 {
260 // X tic labels
261 lanno.add_raw(group_element::start_group("tic-x-" + gstate.title));
262 for (double x = minx; x < maxx; x += xdelta)
263 {
264 const double xto = chartxo + (x * xscale);
265 ostringstream oss;
266 oss << fixed << setprecision(gstate.xticdigits) << x;
267 const string sxui = oss.str() + gstate.xticu;
268 styled_text(lanno, sxui, {xto, ygo}, anntypo);
269 }
271
272 // Y tic labels
273 // Positions for left and right y-axis tic labels.
274 lanno.add_raw(group_element::start_group("tic-y-" + gstate.title));
275 const double yticspacer = graph_rstate::th1sz * 2;
276 const double xgol = gstate.marginx - yticspacer; // left
277 const double xgor = gstate.marginx + gwidth + yticspacer; // right
278 const double starty = miny != 0 ? miny : miny + ydelta; // skip zero label
279 for (double y = starty; y < maxy + ydelta; y += ydelta)
280 {
281 const double yto = chartyo - (y * yscale);
282 const string syui = std::to_string(static_cast<uint>(y)) + gstate.yticu;
283 styled_text(lanno, syui, {xgol, yto}, anntypo);
284 styled_text(lanno, syui, {xgor, yto}, anntypo);
285 }
287 }
288
289 // Horizontal lines linking left and right y-axis tic label value to each other,
290 // perhaps with magnification-ready micro text.
291 if (gstate.is_visible(select::linex))
292 {
293 lanno.add_raw(group_element::start_group("tic-y-lines-" + gstate.title));
294
295 style hlstyl = gstate.lstyle;
297
298 anntypo._M_size = 3;
300 for (double y = miny + ydelta; y < maxy + ydelta; y += ydelta)
301 {
302 // Base line layer.
303 const double yto = chartyo - (y * yscale);
304 line_element lxe = make_line({chartxo + graph_rstate::th1sz, yto},
305 {chartxe - graph_rstate::th1sz, yto}, hlstyl);
306 lanno.add_element(lxe);
307
308 // Add y-axis tic numbers along line for use when magnified.
309 if (gstate.is_visible(select::alt))
310 {
311 // Skip first and last as covered by either Y-axes tic marks.
312 for (double x = minx + xdelta; x < maxx - xdelta; x += xdelta)
313 {
314 const double xto = chartxo + (x * xscale);
315 const string syui = std::to_string(static_cast<uint>(y)) + gstate.yticu;
316 styled_text(lanno, syui, {xto, yto}, anntypo);
317 }
318 }
319 }
320
322 }
323
324 return lanno;
325}
326
327
328/// Returns a svg_element with the chart and labels
329/// Assume:
330/// vgrange x axis is monotonically increasing
331svg_element
332make_line_graph(const svg::area<> aplate, const vrange& points,
333 const graph_rstate& gstate,
334 const point_2t xrange, const point_2t yrange)
335{
336 using namespace std;
337
338 auto [ minx, maxx ] = xrange;
339 auto [ miny, maxy ] = yrange;
340
341 // Locate graph area on plate area.
342 // aplate is total plate area with margins, aka
343 // pwidth = marginx + gwidth + marginx
344 // pheight = marginy + gheight + marginy
345 auto [ pwidth, pheight ] = aplate;
346 double gwidth = pwidth - (2 * gstate.marginx);
347 double gheight = pheight - (2 * gstate.marginy);
348 const double chartyo = pheight - gstate.marginy;
349
350 // Transform data points to scaled cartasian points in graph area.
351 vrange cpoints;
352 for (uint i = 0; i < points.size(); i++)
353 {
354 const point_2t& pt = points[i];
355 auto [ vx, vy ] = pt;
356
357 // At bottom of graph.
358 const double xlen = scale_value_on_range(vx, minx, maxx, 0, gwidth);
359 double x = gstate.marginx + xlen;
360
361 // Y axis grows up from chartyo.
362 const double ylen = scale_value_on_range(vy, miny, maxy, 0, gheight);
363 double y = chartyo - ylen;
364
365 cpoints.push_back(make_tuple(x, y));
366 }
367
368 // Plot path of points on cartesian plane.
369 svg_element lgraph(gstate.title, "line graph", aplate, false);
370
371 // Grouped tooltips have to be the last, aka top layer of SVG to work (?).
372 //constexpr ushort line_strategy = line_1_polyline;
373 constexpr ushort line_strategy = line_2_polyline_tooltips;
374
375 if (gstate.is_visible(select::vector))
376 {
377 if constexpr(line_strategy == line_1_polyline)
378 {
379 // Use polylines and markerspoints
380 polyline_element pl1 = make_polyline(cpoints, gstate.lstyle,
381 gstate.sstyle);
382 lgraph.add_element(pl1);
383 }
384 if constexpr(line_strategy == line_2_polyline_tooltips)
385 {
386 // Use polyline base and set of marker paths with orignal values
387 // as tooltips on top.
388 lgraph.add_raw(group_element::start_group("polyline-" + gstate.title));
389 polyline_element pl1 = make_polyline(cpoints, gstate.lstyle,
390 gstate.sstyle);
391 lgraph.add_element(pl1);
393
394 lgraph.add_raw(group_element::start_group("values-" + gstate.title));
395 string markers = make_line_graph_markers_tips(points, cpoints,
396 gstate, 3);
397 lgraph.add_raw(markers);
399 }
400 }
401
402 return lgraph;
403}
404
405} // namepace svg
406
407#endif
constexpr svg::ushort line_2_polyline_tooltips(200)
constexpr svg::ushort line_1_polyline(100)
Polyline/line creation options. 1: use one line with css dasharray and markers mid,...
void add_data(const data &d)
Either serialize immediately (as below), or create data structure that adds data to data_vec and then...
void add_style(const style &sty)
void add_data(const data &d, string trans="")
static constexpr const char * pair_finish_tag
static string finish_group()
void add_title(const string &t)
static string start_group(const string name="")
static constexpr const char * pair_finish_tag
static constexpr const char * pair_finish_tag
void add_raw(const string &raw)
string str() const
void add_element(const element_base &e)
static constexpr string finish_tag_hard
line_element make_line(const point_2t origin, const point_2t end, style s, const string dasharray="")
Line primitive.
string make_line_graph_markers_tips(const vrange &points, const vrange &cpoints, const graph_rstate &gstate, const double radius)
Return set of paths corresponding to marker shapes with tooltips.
@ pt
Point where 1 pixel x 1/72 dpi x 96 PPI = .26 mm.
unsigned short ushort
Base integer type: positive and negative, signed integral value.
Definition a60-svg.h:56
svg_element make_line_graph_annotations(const area<> aplate, const vrange &points, const graph_rstate &gstate, const double xscalein=1, const double yscalein=1, const typography typo=k::apercu_typo)
Axis Labels Axis X/Y Ticmarks X line increments.
double scale_value_on_range(const ssize_type value, const ssize_type min, const ssize_type max, const ssize_type nfloor, const ssize_type nceil)
Scale value from min to max on range (nfloor, nceil).
Definition a60-svg.h:216
void styled_text(svg_element &obj, const string text, const point_2t origin, const typography typo)
Text at.
@ alt
alternate use specified in situ
@ ticks
ticks, markers
@ vector
svg path, circle, rectangle, etc.
@ linex
horizontal lines
@ echo
b & w outline version of vector
svg_element make_line_graph(const svg::area<> aplate, const vrange &points, const graph_rstate &gstate, const point_2t xrange, const point_2t yrange)
Returns a svg_element with the chart and labels Assume: vgrange x axis is monotonically increasing.
path_element make_path_triangle(const point_2t origin, const style styl, const double r=4, const double angle=120, const bool selfclosingtagp=true)
Center a triangle at this point.
std::vector< point_2t > vrange
Definition a60-svg.h:86
point_2t max_vrange(vspace &xpoints, vspace &ypoints, const uint pown)
For each dimension of vrnage, find min/max and return (xmax, ymax) NB: Assumes zero is min.
Definition a60-svg.h:147
unsigned int uint
Definition a60-svg.h:57
polyline_element make_polyline(const vrange &points, const style s, const stroke_style sstyle={ })
Polyline primitive.
std::tuple< space_type, space_type > point_2t
Point (x,y) in 2D space.
Definition a60-svg.h:65
Per-graph constants, metadata, text.
static constexpr uint marginy
static constexpr uint yticdigits
static constexpr uint xticdigits
static constexpr uint th1sz
static constexpr uint marginx
static constexpr uint tticsz
static constexpr uint ttitlesz
Glyph type sizes.
static constexpr uint tpsz
render_state_base(const select m=select::none)
bool is_visible(const select v) const
Additional path/line/polyline stroke styles.
Datum consolidating style preferences.
color_qi _M_fill_color
color_qi _M_stroke_color
void set_colors(const svg::color &klr)
Convenience function to set all colors at once.