CYD-UI
A C++ library for building native graphic user interfaces
Loading...
Searching...
No Matches
plot_axis.cppm
Go to the documentation of this file.
1//
2// Created by castle on 8/11/24.
3//
4
5module;
7
9
10import cydui;
11
12import reflect;
13import fabric.linalg;
14import fabric.logging;
15
16using la = with_precision<double>;
17using as_no_unit = quantify::quantity_t<quantify::no_unit, double>;
18// using la = with_precision<quantity_t<no_unit, double>>;
19// using la = with_precision<cydui::dimensions::screen_measure>;
20
21namespace charts {
22 constexpr la::scalar DEFAULT_AXIS_MIN_VALUE = -1.0;
23 constexpr la::scalar DEFAULT_AXIS_MAX_VALUE = +1.0;
24 constexpr la::scalar DEFAULT_AXIS_STEP = 0.1;
25
26
27 export COMPONENT(PlotAxis, {
28 la::vec<2> axis_direction{0, 0};
29 la::vec<2> label_direction{0, 0};
30 la::scalar label_offset{0};
31 la::vec<2> label_orientation{0, 0};
33 bool major_ticks_show = false;
34 bool minor_ticks_show = false;
35 unsigned int minor_ticks_count = 4;
36 std::string title = "";
38 la::scalar title_offset{0};
39 std::function<cydui::components::component_holder_t(la::scalar)> label_component;
40 std::function<void(la::scalar)> move_callback;
41 std::function<void()> reset_callback;
42 }) {
43 ON_REDRAW {
45 std::vector<cydui::components::component_holder_t> result{};
46
47 // labels
48 auto start = props.step * std::floor((props.min / props.step));
49 auto num_steps = (props.max - start) / props.step;
50 for (la::scalar step_i = 0; step_i < num_steps; step_i = step_i + la::scalar{1}) {
51 la::scalar X = start + step_i * props.step;
52 if (X < props.min || X > props.max) {
53 continue;
54 }
55 auto pos = props.axis_direction * ((X - props.min) / (props.max - props.min));
56 result.push_back(props.label_component(X));
57
58 auto label = result.back().get_components()[0];
59 auto label_dim = label->get_dimensional_relations();
60 if (props.axis_direction[0] > la::scalar{0}) {
61 label_dim.x = $width * screen_measure{pos[0]} - (label_dim.width / 2_px) +
62 (props.label_direction[0] * props.label_offset);
63 } else if (props.axis_direction[0] < la::scalar{0}) {
64 label_dim.x = $width * screen_measure{pos[0]} - (label_dim.width / screen_measure{2}) +
65 (props.label_direction[0] * props.label_offset) + $width;
66 } else {
67 if (props.label_direction[0] > 0) {
68 label_dim.x = $width * pos[0] - (label_dim.width / 2) +
69 (props.label_direction[0] * props.label_offset);
70 } else {
71 label_dim.x = $width * pos[0] - (label_dim.width / 2) +
72 (props.label_direction[0] * props.label_offset) + $width;
73 }
74 }
75 if (props.axis_direction[1] > 0) {
76 label_dim.y = $height * pos[1] + (label_dim.height / 2) +
77 (props.label_direction[1] * props.label_offset);
78 } else if (props.axis_direction[1] < 0) {
79 label_dim.y = $height * pos[1] + (label_dim.height / 2) +
80 (props.label_direction[1] * props.label_offset) + $height;
81 } else {
82 if (props.label_direction[1] > 0) {
83 label_dim.y = $height * pos[1] + (label_dim.height / 2) +
84 (props.label_direction[1] * props.label_offset);
85 } else {
86 label_dim.y = $height * pos[1] + (label_dim.height / 2) +
87 (props.label_direction[1] * props.label_offset) + $height;
88 }
89 }
90 }
91 return result;
92 } // namespace charts
93
94 FRAGMENT {
95 auto offset_x = 0_px;
96 auto offset_y = 0_px;
97
98 if (props.axis_direction[0] < 0) {
99 offset_x = $width;
100 } else if (props.axis_direction[0] == 0) {
101 if (props.label_direction[0] <= 0) {
102 offset_x = $width;
103 }
104 }
105
106 if (props.axis_direction[1] < 0) {
107 offset_y = $height;
108 } else if (props.axis_direction[1] == 0) {
109 if (props.label_direction[1] <= 0) {
110 offset_y = $height;
111 }
112 }
113
114 // draw axis_line
115 auto x1 = 0_px;
116 auto y1 = 0_px;
117 auto x2 =
118 as_no_unit{props.axis_direction[0]} * ($height / as_no_unit{props.axis_direction[1]});
119 auto y2 =
120 as_no_unit{props.axis_direction[1]} * ($width / as_no_unit{props.axis_direction[0]});
121 if (y2 > $height) {
122 y2 = $height;
123 } else if (y2 < -$height) {
124 y2 = -$height;
125 } else if (x2 > $width) {
126 x2 = $width;
127 } else if (x2 < -$width) {
128 x2 = -$width;
129 } else {
130 fragment.append(
131 vg::line{}.x1(x1).y1(y1).x2($width).y2($height).stroke_width(2).stroke("#FFFFFF"_color)
132 );
133 }
134 fragment.append(vg::line{}
135 .x1(x1 + offset_x)
136 .y1(y1 + offset_y)
137 .x2(x2 + offset_x)
138 .y2(y2 + offset_y)
139 .stroke_width(2)
140 .stroke("#777777"_color));
141 const auto line_length = std::sqrt(la::vec<2>{(x2 - x1).value, (y2 - y1).value}.mag2());
142
143 // draw ticks
144 auto start = props.step * std::floor((props.min / props.step));
145 if (props.major_ticks_show) {
146 for (la::scalar X = start; X < props.max; X += props.step) {
147 auto pos = props.axis_direction * ((X - props.min) / (props.max - props.min));
148
149 x1 = cydui::dimensions::screen_measure{line_length * pos[0]} + offset_x;
150 y1 = cydui::dimensions::screen_measure{line_length * pos[1]} + offset_y;
151 if (X >= props.min && X <= props.max) {
152 fragment.append(vg::line{}
153 .x1(x1)
154 .y1(y1)
155 .x2(
156 x1 +
158 props.label_direction[0] * props.label_offset * 0.25
159 }
160 )
161 .y2(
162 y1 +
164 props.label_direction[1] * props.label_offset * 0.25
165 }
166 )
167 // .stroke_width(1).stroke("#555555"_color));
168 .stroke_width(2)
169 .stroke("#777777"_color));
170 }
171
172 if (props.minor_ticks_show) {
173 la::scalar minor_step = props.step / (props.minor_ticks_count + 1.0);
174 for (la::scalar Y = minor_step; Y <= props.step - minor_step && (X + Y) < props.max;
175 Y += minor_step) {
176 pos = props.axis_direction * (((X + Y) - props.min) / (props.max - props.min));
177
178 x1 = cydui::dimensions::screen_measure{line_length * pos[0]} + offset_x;
179 y1 = cydui::dimensions::screen_measure{line_length * pos[1]} + offset_y;
180 if ((X + Y) >= props.min && (X + Y) <= props.max) {
181 fragment.append(vg::line{}
182 .x1(x1)
183 .y1(y1)
184 .x2(
185 x1 +
187 props.label_direction[0] * props.label_offset * 0.15
188 }
189 )
190 .y2(
191 y1 +
193 props.label_direction[1] * props.label_offset * 0.15
194 }
195 )
196 // .stroke_width(1).stroke("#555555"_color));
197 .stroke_width(1)
198 .stroke("#777777"_color));
199 }
200 }
201 }
202 }
203 }
204
205 // draw title
206 if (!props.title.empty()) {
207 auto title =
208 vg::text{props.title}.font_family("Helvetica").font_size(14).fill("#EEEEEE"_color);
209 auto footprint = title.get_footprint();
210 title.pivot_x(footprint.w / 2);
211 title.pivot_y(footprint.h / 2);
212
213 double ratio;
214 int text_offset_x;
215 int text_offset_y = footprint.h.value_as_base_unit() / 2;
216 switch (props.title_align) {
218 ratio = 0.0;
219 text_offset_x = 0;
220 break;
222 ratio = 0.5;
223 text_offset_x = footprint.w.value_as_base_unit() / 2;
224 break;
226 ratio = 1.0;
227 text_offset_x = footprint.w.value_as_base_unit();
228 break;
229 }
230 auto pos = (props.axis_direction * ratio * line_length) +
231 (props.label_direction * props.title_offset);
232 auto angle =
233 std::atan2(props.axis_direction[1], props.axis_direction[0]) * 180.0 / std::numbers::pi;
234
235 title.x(cydui::dimensions::screen_measure{pos[0] - text_offset_x} + offset_x);
236 title.y(cydui::dimensions::screen_measure{pos[1] + text_offset_y} + offset_y);
237 title.rotate(angle);
238 fragment.append(title);
239 }
240 }
241
242 ON_SCROLL {
243 if (dy.value > 0) {
244 props.move_callback(0.1);
245 state.parent()->mark_dirty();
246 } else if (dy.value < 0) {
247 props.move_callback(-0.1);
248 state.parent()->mark_dirty();
249 }
250 }
251
253 if (button == cydui::Button::WHEEL) {
254 props.reset_callback();
255 state.parent()->mark_dirty();
256 }
257 }
258 };
259
260 export COMPONENT(
261 NumericAxisLabel, { la::scalar value = 0; };
262 static cydui::components::component_holder_t builder(const la::scalar value) {
263 return NumericAxisLabel{{value}};
264 }
265 ) {
266 ON_REDRAW {
267 const auto label_footprint = label_fragment().get_footprint();
268 $width = cydui::dimensions::screen_measure{(double)label_footprint.w.value_as_base_unit()};
269 $height = cydui::dimensions::screen_measure{(double)label_footprint.h.value_as_base_unit()};
270 return {};
271 }
272
273 FRAGMENT {
274 fragment.append(label_fragment());
275 }
276
277 private:
278 vg::text label_fragment() const {
279 return vg::text{std::format("{:.1f}", props.value)}
280 .font_size(14)
281 .font_family("Helvetica")
282 .fill("#CCCCCC"_color);
283 }
284 };
285
286 export COMPONENT(
287 PiRatioAxisLabel, { la::scalar value = 0; };
288 static cydui::components::component_holder_t builder(const la::scalar value) {
289 return PiRatioAxisLabel{{value}};
290 }
291 ) {
292 ON_REDRAW {
293 const auto label_footprint = label_fragment().get_footprint();
294 $width = cydui::dimensions::screen_measure{(double)label_footprint.w.value_as_base_unit()};
295 $height = cydui::dimensions::screen_measure{(double)label_footprint.h.value_as_base_unit()};
296 return {};
297 }
298
299 FRAGMENT {
300 fragment.append(label_fragment());
301 }
302
303 private:
304 vg::text label_fragment() const {
305 auto pi_ratio = props.value / std::numbers::pi;
306 return vg::text{std::format("{:.2f}π", pi_ratio)}
307 .font_size(14)
308 .font_family("Helvetica")
309 .fill("#CCCCCC"_color);
310 }
311 };
312
313 export template <class C>
314 struct axis_t {
315 explicit axis_t(
316 C& plot,
317 const la::vec<2> axis_direction,
318 const la::vec<2> label_direction,
319 const la::vec<2> label_orientation,
320 bool default_show,
321 la::scalar default_label_offset = 30,
322 la::scalar default_title_offset = 45
323
324 )
325 : show_(default_show),
326 label_offset_(default_label_offset),
327 title_offset_(default_title_offset),
328 ref_(&plot),
329 axis_direction_(axis_direction),
330 label_direction_(label_direction),
331 label_orientation_(label_orientation) {
332 initial_min_value_ = min_value_;
333 initial_max_value_ = max_value_;
334 initial_step_ = step_;
335 }
336
337 C& show(const bool show_axis) {
338 show_ = show_axis;
339 return *ref_;
340 }
341
342 C& bounds(const la::scalar min, const la::scalar max, const la::scalar step) {
343 auto_scale_ = false;
344 auto_scale_bound_zero_ = false;
345 min_value_ = min;
346 max_value_ = max;
347 step_ = step;
348 initial_min_value_ = min_value_;
349 initial_max_value_ = max_value_;
350 initial_step_ = step_;
351 return *ref_;
352 }
353
354 C& bounds_auto(const bool bind_zero = false) {
355 auto_scale_ = true;
356 auto_scale_bound_zero_ = bind_zero;
357 return *ref_;
358 }
359
360 C& major_ticks_show(const bool show_ticks) {
361 major_ticks_show_ = show_ticks;
362 return *ref_;
363 }
364
365 C& minor_ticks_show(const bool show_ticks) {
366 minor_ticks_show_ = show_ticks;
367 return *ref_;
368 }
369
370 C& minor_ticks_count(const unsigned int tick_count) {
371 minor_ticks_count_ = tick_count;
372 return *ref_;
373 }
374
375 C& title(const std::string& title_str) {
376 title_ = title_str;
377 return *ref_;
378 }
379
381 title_align_ = align;
382 return *ref_;
383 }
384
385 C& title_offset(const la::scalar offset) {
386 title_offset_ = offset;
387 return *ref_;
388 }
389
390
392 const std::function<cydui::components::component_holder_t(la::scalar)>& builder
393 ) {
394 label_builder_ = [&](const la::scalar value) { return builder(value); };
395 return *ref_;
396 }
397
398 template <typename L>
400 label_builder_ = [&](const la::scalar value) { return L{{value}}; };
401 return *ref_;
402 }
403
404 private:
405 axis_t& operator=(const axis_t& rhl) {
406 show_ = rhl.show_;
407 auto_scale_ = rhl.auto_scale_;
408 auto_scale_bound_zero_ = rhl.auto_scale_bound_zero_;
409 min_value_ = rhl.min_value_;
410 max_value_ = rhl.max_value_;
411 step_ = rhl.step_;
412 initial_min_value_ = rhl.initial_min_value_;
413 initial_max_value_ = rhl.initial_max_value_;
414 initial_step_ = rhl.initial_step_;
415 axis_direction_ = rhl.axis_direction_;
416 label_direction_ = rhl.label_direction_;
417 label_orientation_ = rhl.label_orientation_;
418 label_builder_ = rhl.label_builder_;
419 label_offset_ = rhl.label_offset_;
420 title_ = rhl.title_;
421 title_align_ = rhl.title_align_;
422 title_offset_ = rhl.title_offset_;
423 return *this;
424 }
425
426 PlotAxis build_component(auto&& x, auto&& y, auto&& w, auto&& h) {
427 PlotAxis pa{
428 {.axis_direction = axis_direction_,
429 .label_direction = label_direction_,
430 .label_offset = label_offset_,
431 .label_orientation = label_orientation_,
432 .min = min_value_,
433 .max = max_value_,
434 .step = step_,
435 .major_ticks_show = major_ticks_show_,
436 .minor_ticks_show = minor_ticks_show_,
437 .minor_ticks_count = minor_ticks_count_,
438 .title = title_,
439 .title_align = title_align_,
440 .title_offset = title_offset_,
441 .label_component = label_builder_,
442 .move_callback =
443 [&](const la::scalar dx) {
444 min_value_ += dx;
445 max_value_ += dx;
446 },
447 .reset_callback =
448 [&] {
449 min_value_ = initial_min_value_;
450 max_value_ = initial_max_value_;
451 step_ = initial_step_;
452 }}
453 };
454 pa.x(x).y(y).width(w).height(h);
455 return pa;
456 }
457
458 friend C;
459 friend typename C::event_handler_t;
460 template <class>
461 friend struct grid_t;
462 template <class>
463 friend struct view_t;
464
465 private:
466 bool show_{true};
467 bool auto_scale_{true};
468 bool auto_scale_bound_zero_{false};
469 la::scalar min_value_{DEFAULT_AXIS_MIN_VALUE};
470 la::scalar max_value_{DEFAULT_AXIS_MAX_VALUE};
471 la::scalar step_{DEFAULT_AXIS_STEP};
472 la::scalar initial_min_value_{DEFAULT_AXIS_MIN_VALUE};
473 la::scalar initial_max_value_{DEFAULT_AXIS_MAX_VALUE};
474 la::scalar initial_step_{DEFAULT_AXIS_STEP};
475 bool major_ticks_show_{true};
476 bool minor_ticks_show_{false};
477 unsigned int minor_ticks_count_{3};
478 la::scalar label_offset_{30};
479 std::string title_{""};
481 la::scalar title_offset_{45};
482
483 private:
484 C* ref_;
485 la::vec<2> axis_direction_{0, 0};
486 la::vec<2> label_direction_{0, 0};
487 la::vec<2> label_orientation_{0, 0};
488 std::function<cydui::components::component_builder_t(la::scalar)> label_builder_{
489 [](const la::scalar value) { return NumericAxisLabel{{value}}; }
490 };
491 };
492
493 export template <class C>
495}
#define ON_BUTTON_RELEASE
#define FRAGMENT
#define ON_SCROLL
#define COMPONENT(NAME,...)
#define ON_REDRAW
quantify::quantity_t< screen::pixel, double > screen_measure
Definition _types.cppm:21
axis_t(C &) -> axis_t< C >
constexpr la::scalar DEFAULT_AXIS_MIN_VALUE
constexpr la::scalar DEFAULT_AXIS_MAX_VALUE
constexpr la::scalar DEFAULT_AXIS_STEP
quantify::quantity_t< screen::pixel, double > screen_measure
Definition _types.cppm:21
text_anchor_e
with_precision< double > la
Definition plot.cppm:19
quantify::quantity_t< quantify::no_unit, double > as_no_unit
C & bounds(const la::scalar min, const la::scalar max, const la::scalar step)
friend struct view_t
friend struct grid_t
C & minor_ticks_count(const unsigned int tick_count)
C & bounds_auto(const bool bind_zero=false)
C & title_offset(const la::scalar offset)
C & title(const std::string &title_str)
axis_t(C &plot, const la::vec< 2 > axis_direction, const la::vec< 2 > label_direction, const la::vec< 2 > label_orientation, bool default_show, la::scalar default_label_offset=30, la::scalar default_title_offset=45)
C & title_align(const vg::text_anchor_e align)
C & minor_ticks_show(const bool show_ticks)
C & show(const bool show_axis)
C & major_ticks_show(const bool show_ticks)
C & label_component(const std::function< cydui::components::component_holder_t(la::scalar)> &builder)