MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SeriesBuilder.cpp
Go to the documentation of this file.
1#include "SeriesBuilder.hpp"
2
4
6
7// =============================================================================
8// Internal helpers
9// =============================================================================
10
11namespace {
12
14
15 /**
16 * @brief Collect all series from the container matching any role in @p mapping.
17 */
18 std::vector<std::span<const double>> series_for_mapping(
19 const Kakshya::PlotContainer& container,
20 const Series::AxisMapping& mapping)
21 {
22 std::vector<std::span<const double>> result;
23 for (const auto& role : mapping.roles) {
24 auto s = series_by_role(container, role);
25 result.insert(result.end(), s.begin(), s.end());
26 }
27 return result;
28 }
29
30 /**
31 * @brief Flatten all AxisMappings into a single merged AxisRange.
32 */
33 AxisRange merge_axis(std::vector<Series::AxisMapping>& mappings)
34 {
35 if (mappings.empty())
36 return {};
37 if (mappings.size() == 1)
38 return mappings[0].range;
39
40 AxisRange merged = mappings[0].range;
41 for (size_t i = 1; i < mappings.size(); ++i) {
42 const auto& r = mappings[i].range;
43 merged.min = std::min(merged.min, r.min);
44 merged.max = std::max(merged.max, r.max);
45 if (r.auto_scaling)
46 merged.auto_scaling = true;
47 if (!merged.scale_predicate && r.scale_predicate)
48 merged.scale_predicate = r.scale_predicate;
49 }
50 return merged;
51 }
52
53 AxisRange merge_axis_const(const std::vector<Series::AxisMapping>& mappings)
54 {
55 auto copy = mappings;
56 return merge_axis(copy);
57 }
58
59 /**
60 * @brief Resolve color for a series index within a mapping.
61 */
62 glm::vec3 resolve_color(
63 const Series::AxisMapping& mapping,
64 const std::vector<glm::vec3>& global_palette,
65 size_t series_index)
66 {
67 if (!mapping.palette.empty())
68 return palette_color(mapping.palette, series_index);
69 return palette_color(global_palette, series_index);
70 }
71
72 std::vector<float> uniform_x(size_t n, const AxisRange& x_range)
73 {
74 std::vector<float> xs(n);
75 if (n == 1) {
76 xs[0] = x_range.to_ndc((x_range.min + x_range.max) * 0.5F);
77 return xs;
78 }
79 for (size_t i = 0; i < n; ++i) {
80 const float t = static_cast<float>(i) / static_cast<float>(n - 1);
81 xs[i] = x_range.to_ndc(x_range.min + t * (x_range.max - x_range.min));
82 }
83 return xs;
84 }
85
86 /** Vertex field offsets — derived from VertexLayout, never hardcoded.
87 * All three vertex types share the 60-byte layout:
88 * offset 0 — position vec3 (12)
89 * offset 12 — color vec3 (12)
90 * offset 24 — scalar float (4) [size / thickness / weight]
91 * offset 28 — uv vec2 (8)
92 * offset 36 — normal vec3 (12)
93 * offset 48 — tangent vec3 (12)
94 **/
95
96 constexpr uint32_t k_stride = 60;
97 constexpr uint32_t k_off_pos = 0;
98 constexpr uint32_t k_off_color = 12;
99 constexpr uint32_t k_off_scalar = 24;
100
101 constexpr glm::vec3 k_default_normal { 0.F, 0.F, 1.F };
102 constexpr glm::vec3 k_default_tangent { 1.F, 0.F, 0.F };
103 constexpr glm::vec2 k_default_uv { 0.F, 0.F };
104
105 void write_vertex(
106 std::vector<uint8_t>& out,
107 glm::vec3 pos,
108 glm::vec3 color,
109 float scalar)
110 {
111 const size_t base = out.size();
112 out.resize(base + k_stride, 0);
113 uint8_t* v = out.data() + base;
114 std::memcpy(v + k_off_pos, &pos, 12);
115 std::memcpy(v + k_off_color, &color, 12);
116 std::memcpy(v + k_off_scalar, &scalar, 4);
117 std::memcpy(v + 28, &k_default_uv, 8);
118 std::memcpy(v + 36, &k_default_normal, 12);
119 std::memcpy(v + 48, &k_default_tangent, 12);
120 }
121
122 /**
123 * @brief Map a spec's geometry from NDC into its plot bounds, when it has any.
124 *
125 * Every encoding emits positions in [-1, 1]; this scales x and y into
126 * @c plot_bounds, the region the ticks, background and legend are laid out
127 * against. Vertex scalars such as line thickness are left as written.
128 */
129 SeriesSpec fitted(SeriesSpec spec)
130 {
131 if (!spec.plot_bounds)
132 return spec;
133
134 const Kinesis::AABB2D bounds = *spec.plot_bounds;
135 spec.fn = [inner = std::move(spec.fn), bounds](
136 const std::shared_ptr<Kakshya::PlotContainer>& container,
137 std::vector<uint8_t>& out,
138 Element& element) mutable {
139 inner(container, out, element);
140
141 const size_t count = out.size() / k_stride;
142 for (size_t i = 0; i < count; ++i) {
143 float position[2];
144 uint8_t* vertex = out.data() + i * k_stride;
145 std::memcpy(position, vertex, sizeof(position));
146 position[0] = bounds.min.x + (position[0] + 1.F) * 0.5F * bounds.width();
147 position[1] = bounds.min.y + (position[1] + 1.F) * 0.5F * bounds.height();
148 std::memcpy(vertex, position, sizeof(position));
149 }
150 };
151 return spec;
152 }
153
154} // namespace
155
156// =============================================================================
157// Series adornment resolution
158// =============================================================================
159
160std::vector<TickLabelsSpec> Series::resolved_tick_labels() const
161{
162 std::vector<TickLabelsSpec> resolved;
163 if (!m_plot_bounds)
164 return resolved;
165
166 resolved.reserve(m_ticks.size());
167
168 for (const auto& req : m_ticks) {
169 AxisRange range {};
170 if (req.range) {
171 range = *req.range;
172 } else {
173 switch (req.axis) {
174 case TickAxis::X:
175 range = merge_axis_const(m_x);
176 break;
177 case TickAxis::Y:
178 range = merge_axis_const(m_y);
179 break;
181 default:
182 range = {};
183 break;
184 }
185 }
186
187 resolved.push_back(TickLabelsSpec {
189 .range = std::move(range),
190 .count = req.count,
191 .edge = req.edge,
192 .color = req.color,
193 .decimal_places = req.decimal_places,
194 .label_h = req.label_h,
195 .label_w = req.label_w,
196 .name_prefix = req.name_prefix,
197 });
198 }
199
200 return resolved;
201}
202
203// =============================================================================
204// WaveformBuilder::done
205// =============================================================================
206
208{
209 auto x_mappings = m_state.x_mappings();
210 auto y_mappings = m_state.y_mappings();
211 auto z_mappings = m_state.z_mappings();
212 const auto global_palette = m_state.palette();
213 const float thickness = m_thickness;
214
215 return fitted(SeriesSpec {
216 .fn = [x_mappings, y_mappings, z_mappings, global_palette, thickness](
217 const std::shared_ptr<Kakshya::PlotContainer>& container,
218 std::vector<uint8_t>& out,
219 Element&) mutable {
220 if (!container)
221 return;
222
223 container->process_default();
224
225 struct SeriesEntry {
226 std::span<const double> data;
227 size_t mapping_index;
228 size_t index_within_mapping;
229 };
230
231 std::vector<SeriesEntry> y_entries;
232 for (size_t mi = 0; mi < y_mappings.size(); ++mi) {
233 auto series = series_for_mapping(*container, y_mappings[mi]);
234 for (size_t si = 0; si < series.size(); ++si)
235 y_entries.push_back({ .data = series[si], .mapping_index = mi, .index_within_mapping = si });
236 }
237 if (y_entries.empty())
238 return;
239
240 AxisRange x_range = merge_axis(x_mappings);
241 AxisRange z_range = z_mappings.empty() ? AxisRange {} : merge_axis(z_mappings);
242
243 std::vector<std::span<const double>> all_x, all_z;
244 for (auto& m : x_mappings) {
245 auto s = series_for_mapping(*container, m);
246 all_x.insert(all_x.end(), s.begin(), s.end());
247 }
248 for (auto& m : z_mappings) {
249 auto s = series_for_mapping(*container, m);
250 all_z.insert(all_z.end(), s.begin(), s.end());
251 }
252
253 apply_auto_scale(x_range, all_x.empty() ? std::vector<std::span<const double>> { y_entries[0].data } : all_x);
254 apply_auto_scale(z_range, all_z);
255
256 out.clear();
257
258 for (size_t ei = 0; ei < y_entries.size(); ++ei) {
259 const auto& entry = y_entries[ei];
260 const auto& ys = entry.data;
261 const size_t n = ys.size();
262 if (n == 0)
263 continue;
264
265 const glm::vec3 color = resolve_color(
266 y_mappings[entry.mapping_index], global_palette, entry.index_within_mapping);
267
268 auto y_range = y_mappings[entry.mapping_index].range;
269 apply_auto_scale(y_range, { ys });
270
271 const bool has_x = ei < all_x.size() && all_x[ei].size() == n;
272 const bool has_z = ei < all_z.size() && all_z[ei].size() == n;
273
274 const std::vector<float> xs = has_x
275 ? [&] {
276 std::vector<float> v;
277 v.reserve(n);
278 for (double val : all_x[ei])
279 v.push_back(x_range.to_ndc(static_cast<float>(val)));
280 return v;
281 }()
282 : uniform_x(n, x_range);
283
284 for (size_t i = 0; i < n; ++i) {
285 write_vertex(out,
286 { xs[i],
287 y_range.to_ndc(static_cast<float>(ys[i])),
288 has_z ? z_range.to_ndc(static_cast<float>(all_z[ei][i])) : 0.F },
289 color,
290 thickness);
291 }
292
293 if (ei + 1 < y_entries.size()) {
294 const glm::vec3 sep_pos {
295 xs[n - 1],
296 y_range.to_ndc(static_cast<float>(ys[n - 1])),
297 has_z ? z_range.to_ndc(static_cast<float>(all_z[ei][n - 1])) : 0.F,
298 };
299 write_vertex(out, sep_pos, color, 0.F);
300 write_vertex(out, sep_pos, color, 0.F);
301 }
302 } },
304 .capacity_for = [](uint64_t n) { return (n + 2) * k_stride; },
305 .background_fn = m_state.has_background()
306 ? std::optional<GeometryFn<float>> { background(m_state.background_bounds(), m_state.background_color()) }
307 : std::nullopt,
308 .plot_bounds = m_state.plot_bounds(),
309 .labels = m_state.labels(),
310 .tick_labels = m_state.resolved_tick_labels(),
311 .legend = m_state.legend_spec(),
312 });
313}
314
315// =============================================================================
316// ScatterBuilder::done
317// =============================================================================
318
320{
321 auto x_mappings = m_state.x_mappings();
322 auto y_mappings = m_state.y_mappings();
323 auto z_mappings = m_state.z_mappings();
324 const auto global_palette = m_state.palette();
325 const float point_size = m_point_size;
326
327 return fitted(SeriesSpec {
328 .fn = [x_mappings, y_mappings, z_mappings, global_palette, point_size](
329 const std::shared_ptr<Kakshya::PlotContainer>& container,
330 std::vector<uint8_t>& out,
331 Element&) mutable {
332 if (!container)
333 return;
334
335 container->process_default();
336
337 std::vector<std::span<const double>> all_x, all_z;
338 for (auto& m : x_mappings) {
339 auto s = series_for_mapping(*container, m);
340 all_x.insert(all_x.end(), s.begin(), s.end());
341 }
342 for (auto& m : z_mappings) {
343 auto s = series_for_mapping(*container, m);
344 all_z.insert(all_z.end(), s.begin(), s.end());
345 }
346
347 AxisRange x_range = merge_axis(x_mappings);
348 AxisRange z_range = z_mappings.empty() ? AxisRange {} : merge_axis(z_mappings);
349 apply_auto_scale(x_range, all_x);
350 apply_auto_scale(z_range, all_z);
351
352 out.clear();
353
354 size_t global_idx = 0;
355 for (auto& y_mapping : y_mappings) {
356 auto y_series = series_for_mapping(*container, y_mapping);
357 auto y_range = y_mapping.range;
358 apply_auto_scale(y_range, y_series);
359
360 for (size_t si = 0; si < y_series.size(); ++si, ++global_idx) {
361 const auto& ys = y_series[si];
362 const size_t n = ys.size();
363 if (n == 0)
364 continue;
365
366 const glm::vec3 color = resolve_color(y_mapping, global_palette, si);
367 const size_t x_idx = std::min(global_idx, all_x.empty() ? size_t(0) : all_x.size() - 1);
368 const bool has_x = !all_x.empty() && all_x[x_idx].size() == n;
369 const bool has_z = global_idx < all_z.size() && all_z[global_idx].size() == n;
370
371 for (size_t i = 0; i < n; ++i) {
372 const float px = has_x
373 ? x_range.to_ndc(static_cast<float>(all_x[x_idx][i]))
374 : x_range.to_ndc(x_range.min
375 + static_cast<float>(i) / static_cast<float>(std::max<size_t>(n - 1, 1))
376 * (x_range.max - x_range.min));
377
378 write_vertex(out,
379 { px,
380 y_range.to_ndc(static_cast<float>(ys[i])),
381 has_z ? z_range.to_ndc(static_cast<float>(all_z[global_idx][i])) : 0.F },
382 color,
383 point_size);
384 }
385 }
386 } },
388 .capacity_for = [](uint64_t n) { return n * k_stride; },
389 .background_fn = m_state.has_background()
390 ? std::optional<GeometryFn<float>> { background(m_state.background_bounds(), m_state.background_color()) }
391 : std::nullopt,
392 .plot_bounds = m_state.plot_bounds(),
393 .labels = m_state.labels(),
394 .tick_labels = m_state.resolved_tick_labels(),
395 .legend = m_state.legend_spec(),
396 });
397}
398
399// =============================================================================
400// BarsBuilder::done
401// =============================================================================
402
404{
405 auto x_mappings = m_state.x_mappings();
406 auto y_mappings = m_state.y_mappings();
407 const auto global_palette = m_state.palette();
408
409 return fitted(SeriesSpec {
410 .fn = [x_mappings, y_mappings, global_palette](
411 const std::shared_ptr<Kakshya::PlotContainer>& container,
412 std::vector<uint8_t>& out,
413 Element&) mutable {
414 if (!container)
415 return;
416
417 container->process_default();
418
419 AxisRange x_range = x_mappings.empty() ? AxisRange {} : merge_axis(x_mappings);
420 out.clear();
421
422 for (auto& y_mapping : y_mappings) {
423 auto y_series = series_for_mapping(*container, y_mapping);
424 if (y_series.empty())
425 continue;
426
427 auto y_range = y_mapping.range;
428 apply_auto_scale(y_range, y_series);
429
430 for (size_t si = 0; si < y_series.size(); ++si) {
431 const auto& series = y_series[si];
432 const size_t n = series.size();
433 if (n == 0)
434 continue;
435
436 const glm::vec3 color = resolve_color(y_mapping, global_palette, si);
437 const float bar_w = (x_range.max - x_range.min) / static_cast<float>(n);
438 const float y_base = y_range.to_ndc(0.F);
439
440 for (size_t i = 0; i < n; ++i) {
441 const float x_left = x_range.to_ndc(x_range.min + static_cast<float>(i) * bar_w);
442 const float x_right = x_range.to_ndc(x_range.min + static_cast<float>(i + 1) * bar_w);
443 const float y_top = y_range.to_ndc(static_cast<float>(series[i]));
444
445 write_vertex(out, { x_left, y_base, 0.F }, color, 0.F);
446 write_vertex(out, { x_right, y_base, 0.F }, color, 0.F);
447 write_vertex(out, { x_left, y_top, 0.F }, color, 0.F);
448 write_vertex(out, { x_right, y_base, 0.F }, color, 0.F);
449 write_vertex(out, { x_right, y_top, 0.F }, color, 0.F);
450 write_vertex(out, { x_left, y_top, 0.F }, color, 0.F);
451 }
452 }
453 } },
455 .capacity_for = [](uint64_t n) { return n * 6 * k_stride; },
456 .background_fn = m_state.has_background()
457 ? std::optional<GeometryFn<float>> { background(m_state.background_bounds(), m_state.background_color()) }
458 : std::nullopt,
459 .plot_bounds = m_state.plot_bounds(),
460 .labels = m_state.labels(),
461 .tick_labels = m_state.resolved_tick_labels(),
462 .legend = m_state.legend_spec(),
463 });
464}
465
467{
468 auto x_mappings = m_state.x_mappings();
469 auto y_mappings = m_state.y_mappings();
470 const auto global_palette = m_state.palette();
471 const float baseline_data = m_baseline;
472
473 return fitted(SeriesSpec {
474 .fn = [x_mappings, y_mappings, global_palette, baseline_data](
475 const std::shared_ptr<Kakshya::PlotContainer>& container,
476 std::vector<uint8_t>& out,
477 Element&) mutable {
478 if (!container)
479 return;
480
481 container->process_default();
482
483 struct SeriesEntry {
484 std::span<const double> data;
485 size_t mapping_index;
486 size_t index_within_mapping;
487 };
488 std::vector<SeriesEntry> y_entries;
489 for (size_t mi = 0; mi < y_mappings.size(); ++mi) {
490 auto sv = series_for_mapping(*container, y_mappings[mi]);
491 for (size_t si = 0; si < sv.size(); ++si)
492 y_entries.push_back({ .data = sv[si], .mapping_index = mi, .index_within_mapping = si });
493 }
494 if (y_entries.empty())
495 return;
496
497 AxisRange x_range = merge_axis(x_mappings);
498 AxisRange y_range = merge_axis(y_mappings);
499
500 std::vector<std::span<const double>> all_y;
501 all_y.reserve(y_entries.size());
502 for (const auto& e : y_entries)
503 all_y.push_back(e.data);
504 apply_auto_scale(y_range, all_y);
505
506 std::vector<std::span<const double>> all_x;
507 if (!x_mappings.empty()) {
508 all_x = series_for_mapping(*container, x_mappings[0]);
509 apply_auto_scale(x_range, all_x);
510 } else {
511 x_range = AxisRange {}.range(-1.F, 1.F);
512 }
513
514 const float baseline_ndc = y_range.to_ndc(baseline_data);
515
516 out.clear();
517
518 for (size_t ei = 0; ei < y_entries.size(); ++ei) {
519 const auto& entry = y_entries[ei];
520 const size_t n = entry.data.size();
521 if (n == 0)
522 continue;
523
524 const glm::vec3 color = resolve_color(
525 y_mappings[entry.mapping_index], global_palette, entry.index_within_mapping);
526
527 const size_t x_idx = std::min(ei, all_x.empty() ? size_t(0) : all_x.size() - 1);
528 const bool has_x = !all_x.empty() && all_x[x_idx].size() == n;
529
530 const size_t pre_size = out.size();
531
532 for (size_t i = 0; i < n; ++i) {
533 const float px = has_x
534 ? x_range.to_ndc(static_cast<float>(all_x[x_idx][i]))
535 : x_range.to_ndc(x_range.min
536 + static_cast<float>(i) / static_cast<float>(std::max<size_t>(n - 1, 1))
537 * (x_range.max - x_range.min));
538
539 const float py = y_range.to_ndc(static_cast<float>(entry.data[i]));
540
541 write_vertex(out, { px, py, 0.F }, color, 1.F);
542 write_vertex(out, { px, baseline_ndc, 0.F }, color, 0.F);
543 }
544
545 if (ei > 0 && out.size() > pre_size) {
546 const std::vector<uint8_t> head(
547 out.begin() + static_cast<ptrdiff_t>(pre_size),
548 out.begin() + static_cast<ptrdiff_t>(pre_size + k_stride));
549 out.insert(out.begin() + static_cast<ptrdiff_t>(pre_size), head.begin(), head.end());
550 }
551
552 if (ei + 1 < y_entries.size() && out.size() > pre_size) {
553 const std::vector<uint8_t> tail(out.end() - static_cast<ptrdiff_t>(k_stride), out.end());
554 out.insert(out.end(), tail.begin(), tail.end());
555 }
556 } },
558 .capacity_for = [](uint64_t n) { return n * 2 * k_stride + 128; },
559 .background_fn = m_state.has_background()
560 ? std::optional<GeometryFn<float>> { background(
562 : std::nullopt,
563 .plot_bounds = m_state.plot_bounds(),
564 .labels = m_state.labels(),
565 .tick_labels = m_state.resolved_tick_labels(),
566 .legend = m_state.legend_spec(),
567 });
568}
569
570} // namespace MayaFlux::Portal::Forma::Plot
glm::vec3 color
Definition Decoder.cpp:97
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
uint32_t scalar
std::vector< float > * out
size_t count
uint32_t py
uint32_t px
ScatterBuilder & point_size(float s)
Point size in pixels.
const std::vector< LabelSpec > & labels() const
std::vector< TickLabelsSpec > resolved_tick_labels() const
Resolve pending fluent tick requests into concrete tick label specs.
Kinesis::AABB2D background_bounds() const
const std::vector< AxisMapping > & z_mappings() const
const std::vector< AxisMapping > & x_mappings() const
const std::optional< LegendSpec > & legend_spec() const
const std::optional< Kinesis::AABB2D > & plot_bounds() const
const std::vector< glm::vec3 > & palette() const
std::optional< Kinesis::AABB2D > m_plot_bounds
std::vector< TickRequest > m_ticks
const std::vector< AxisMapping > & y_mappings() const
WaveformBuilder & thickness(float t)
Line thickness in pixels.
glm::vec3 palette_color(const std::vector< glm::vec3 > &palette, size_t index) noexcept
Resolve a per-series color from a palette.
Definition PlotSpec.cpp:76
Series series()
Begin a Series chain.
Definition Plot.hpp:108
void apply_auto_scale(AxisRange &range, const std::vector< std::span< const double > > &series)
Apply auto-scaling to an AxisRange from a set of series.
Definition PlotSpec.cpp:53
std::vector< std::span< const double > > series_by_role(const Kakshya::PlotContainer &container, Kakshya::DataDimension::Role role)
Collect all series from processed_data whose DataDimension role matches role.
Definition PlotSpec.cpp:12
GeometryFn< float > background(Kinesis::AABB2D bounds, glm::vec3 color, const std::shared_ptr< Core::VKImage > &texture)
TRIANGLE_STRIP background quad for a plot area.
Definition PlotSpec.cpp:88
CopiedText copy(std::string_view text, const LayoutResult &layout, size_t a, size_t b)
Copy a byte range: the text it spans, and the quads that render it.
Role
Semantic role of the dimension.
Definition NDData.hpp:204
A bounded, renderable region on a window surface.
Definition Element.hpp:61
AxisRange & range(float lo, float hi)
Set the explicit [min, max] domain.
Definition AxisRange.hpp:39
float to_ndc(float v) const noexcept
Map a value into [-1, 1] NDC within this range.
Definition AxisRange.hpp:96
Scalar domain extent for one plot axis.
Definition AxisRange.hpp:22
Forma::GeometryFn< std::shared_ptr< Kakshya::PlotContainer > > fn
Config for generating numeric tick labels on one plot edge.
Definition PlotSpec.hpp:75