-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathfastchart_vector.c
More file actions
234 lines (215 loc) · 10.4 KB
/
Copy pathfastchart_vector.c
File metadata and controls
234 lines (215 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
/*
+----------------------------------------------------------------------+
| Copyright (c) 2025-2026, Ilia Alshanetsky |
| Copyright (c) 2025-2026, Advanced Internet Designs Inc. |
+----------------------------------------------------------------------+
| This source file is subject to the BSD 3-Clause license that is |
| bundled with this package in the file LICENSE. |
+----------------------------------------------------------------------+
| Author: Ilia Alshanetsky <ilia@ilia.ws> |
+----------------------------------------------------------------------+
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <math.h>
#include <stdio.h>
#include <string.h>
#include "php.h"
#include "Zend/zend_exceptions.h"
#include "php_fastchart.h"
#include "fastchart_palette.h"
#include "fastchart_target.h"
#include "fastchart_axis.h"
#include "fastchart_text.h"
/* Vector field: each datum draws an arrow at (x,y) pointing in
* (dx,dy). Arrow length is proportional to vector magnitude
* relative to mag_max, capped at one grid cell so dense fields
* don't overlap. */
int fastchart_vector_render_to_target(fastchart_vector_obj *self, fastchart_target_t *t)
{
fastchart_palette pal;
fastchart_palette_init(t, (int)self->theme, &pal);
fastchart_palette_apply_overrides(t, (fastchart_obj *)self, &pal);
int W, H;
fastchart_target_get_dims(t, &W, &H);
/* Canvas background via the shared helper so setTransparentBackground /
* setBackgroundImage / setPlotRect compositing are honored instead of an
* unconditional opaque fill. */
fastchart_paint_canvas_bg(t, (fastchart_obj *)self, &pal);
if (self->vector_count <= 0) {
zend_throw_error(NULL,
"FastChart\\VectorChart::draw() requires setVectors() with at least one entry");
return -1;
}
int title_h = 0;
const char *title_font = fastchart_resolve_font((fastchart_obj *)self, FC_FONT_TITLE);
double base_size = self->font_size > 0 ? self->font_size : FASTCHART_DEFAULT_FONT_SIZE;
double title_size = fastchart_resolve_font_size(
(fastchart_obj *)self, FC_FONT_TITLE, base_size * 1.4);
if (self->title && ZSTR_LEN(self->title) > 0 && title_font) {
fastchart_text_measure(t, title_font, title_size, ZSTR_VAL(self->title),
NULL, &title_h, NULL, 0);
}
/* Data bounds for (x,y) anchor. */
double xmin = self->vectors[0].x, xmax = self->vectors[0].x;
double ymin = self->vectors[0].y, ymax = self->vectors[0].y;
for (int i = 1; i < self->vector_count; i++) {
if (self->vectors[i].x < xmin) xmin = self->vectors[i].x;
if (self->vectors[i].x > xmax) xmax = self->vectors[i].x;
if (self->vectors[i].y < ymin) ymin = self->vectors[i].y;
if (self->vectors[i].y > ymax) ymax = self->vectors[i].y;
}
if (xmax <= xmin) xmax = xmin + 1.0;
if (ymax <= ymin) ymax = ymin + 1.0;
fastchart_value_range xr, yr;
fastchart_value_range_compute(xmin, xmax, 5, &xr);
fastchart_value_range_compute(ymin, ymax, 5, &yr);
fastchart_rect plot;
fastchart_compute_layout((fastchart_obj *)self, t, 1, 1, NULL, 0, &plot);
/* Plot bg + frame + ticks (basic). */
fastchart_target_rect(t, plot.x0, plot.y0,
plot.x1 - plot.x0 + 1, plot.y1 - plot.y0 + 1,
pal.plot_bg, 1, 0);
/* Tick labels use the AXIS font role (setAxisFont), matching the shared
* axis renderer, and are suppressed in thumbnail mode like every other
* chart. VectorChart hand-rolls its axis so it must honor both here. */
int draw_labels = !((fastchart_obj *)self)->thumbnail_mode;
int x_vis = ((fastchart_obj *)self)->x_axis_visible;
int y_vis = ((fastchart_obj *)self)->y_axis_visible;
const char *font = fastchart_resolve_font((fastchart_obj *)self, FC_FONT_AXIS);
double size = fastchart_resolve_font_size(
(fastchart_obj *)self, FC_FONT_AXIS, base_size);
/* Light grid. Each axis's gridlines + labels are gated on its
* visibility flag, matching the shared axis drawers (setXAxisVisible /
* setYAxisVisible suppress the whole axis, gridlines included). */
if (y_vis) {
for (int i = 0; i < yr.n_ticks; i++) {
double frac = fastchart_normalize_finite(yr.ticks[i], yr.min,
yr.max);
int y = plot.y1 - (int)(frac * (plot.y1 - plot.y0));
fastchart_target_line(t, plot.x0, y, plot.x1, y,
pal.grid, 1, FASTCHART_DASH_SOLID);
if (font && draw_labels) {
char buf[24];
snprintf(buf, sizeof(buf), "%g", yr.ticks[i]);
fastchart_text_draw(t, font, size, pal.text,
plot.x0 - 6, y + (int)(size * 0.4),
FASTCHART_ALIGN_RIGHT, buf, NULL, 0);
}
}
}
if (x_vis) {
for (int i = 0; i < xr.n_ticks; i++) {
double frac = fastchart_normalize_finite(xr.ticks[i], xr.min,
xr.max);
int x = plot.x0 + (int)(frac * (plot.x1 - plot.x0));
fastchart_target_line(t, x, plot.y0, x, plot.y1,
pal.grid, 1, FASTCHART_DASH_SOLID);
if (font && draw_labels) {
char buf[24];
snprintf(buf, sizeof(buf), "%g", xr.ticks[i]);
fastchart_text_draw(t, font, size, pal.text,
x, plot.y1 + (int)(size * 1.4),
FASTCHART_ALIGN_CENTER, buf, NULL, 0);
}
}
}
fastchart_target_rect(t, plot.x0, plot.y0,
plot.x1 - plot.x0 + 1, plot.y1 - plot.y0 + 1,
pal.border, 0, 1);
/* Max arrow length: cap at the smaller plot-axis tick spacing
* so dense fields don't overlap. */
double tick_dx = (plot.x1 - plot.x0) / (double)(xr.n_ticks > 1 ? xr.n_ticks - 1 : 1);
double tick_dy = (plot.y1 - plot.y0) / (double)(yr.n_ticks > 1 ? yr.n_ticks - 1 : 1);
double cap_px = (tick_dx < tick_dy ? tick_dx : tick_dy) * 0.9;
if (cap_px < 8) cap_px = 8;
double mag_max = self->mag_max > 0 ? self->mag_max : 1.0;
/* Inset the anchor-placement rect by cap_px on each side so an
* arrow at the extreme data corner pointing outward still fits
* inside the plot frame. The grid lines + axis ticks computed
* above still use the full plot rect (they mark data range, not
* arrow positions). */
int anchor_x0 = plot.x0 + (int)cap_px;
int anchor_x1 = plot.x1 - (int)cap_px;
int anchor_y0 = plot.y0 + (int)cap_px;
int anchor_y1 = plot.y1 - (int)cap_px;
if (anchor_x1 <= anchor_x0) { anchor_x0 = plot.x0; anchor_x1 = plot.x1; }
if (anchor_y1 <= anchor_y0) { anchor_y0 = plot.y0; anchor_y1 = plot.y1; }
/* Ramp colors if both endpoints set. */
int have_ramp = (self->color_low_rgb >= 0 && self->color_high_rgb >= 0);
int lr = 0, lg = 0, lb = 0, hr = 0, hg = 0, hb = 0;
if (have_ramp) {
lr = (self->color_low_rgb >> 16) & 0xFF;
lg = (self->color_low_rgb >> 8) & 0xFF;
lb = (self->color_low_rgb ) & 0xFF;
hr = (self->color_high_rgb >> 16) & 0xFF;
hg = (self->color_high_rgb >> 8) & 0xFF;
hb = (self->color_high_rgb ) & 0xFF;
}
double mag_range = self->mag_max - self->mag_min;
if (mag_range <= 0) mag_range = 1.0;
/* Render every arrow. */
for (int i = 0; i < self->vector_count; i++) {
double mag = sqrt(self->vectors[i].dx * self->vectors[i].dx
+ self->vectors[i].dy * self->vectors[i].dy);
double scale = mag_max > 0 ? mag / mag_max : 0.0;
double len = scale * cap_px;
if (len < 1.5) continue;
/* Anchor arrows against the displayed (nice) axis range so they
* line up with the grid ticks and labels, not the raw data
* min/max. */
double axu = fastchart_normalize_finite(self->vectors[i].x,
xr.min, xr.max);
double ayu = fastchart_normalize_finite(self->vectors[i].y,
yr.min, yr.max);
/* A finite-but-extreme coordinate can land far outside a small
* fallback axis range, so axu/ayu overflow the pixel span and
* the (int) cast is UB. Drop NaN/Inf, clamp the rest to [0,1]. */
if (!isfinite(axu) || !isfinite(ayu)) continue;
if (axu < 0.0) axu = 0.0; else if (axu > 1.0) axu = 1.0;
if (ayu < 0.0) ayu = 0.0; else if (ayu > 1.0) ayu = 1.0;
int ax = anchor_x0 + (int)(axu * (anchor_x1 - anchor_x0));
int ay = anchor_y1 - (int)(ayu * (anchor_y1 - anchor_y0));
double ang = atan2(self->vectors[i].dy, self->vectors[i].dx);
int tx = ax + (int)(cos(ang) * len);
int ty = ay - (int)(sin(ang) * len); /* screen-Y is inverted */
int color;
if (have_ramp) {
double frac = (mag - self->mag_min) / mag_range;
if (frac < 0) frac = 0;
if (frac > 1) frac = 1;
int rr = lr + (int)((hr - lr) * frac);
int gg = lg + (int)((hg - lg) * frac);
int bb = lb + (int)((hb - lb) * frac);
color = fastchart_target_color_rgb(
t, (rr << 16) | (gg << 8) | bb);
} else if (self->vectors[i].color_rgb >= 0) {
color = fastchart_target_color_rgb(t, self->vectors[i].color_rgb);
} else {
color = pal.series[0];
}
fastchart_target_line(t, ax, ay, tx, ty, color, 2, FASTCHART_DASH_SOLID);
/* Arrowhead: small triangle at the tip. */
double head_len = len * 0.3;
if (head_len < 4) head_len = 4;
double head_ang = 0.4; /* ~23 degrees */
int hx1 = tx - (int)(cos(ang - head_ang) * head_len);
int hy1 = ty + (int)(sin(ang - head_ang) * head_len);
int hx2 = tx - (int)(cos(ang + head_ang) * head_len);
int hy2 = ty + (int)(sin(ang + head_ang) * head_len);
fastchart_point_t tri[3] = {
{ tx, ty },
{ hx1, hy1 },
{ hx2, hy2 },
};
fastchart_target_polygon(t, tri, 3, color, 1, 0);
}
if (self->title && ZSTR_LEN(self->title) > 0 && title_font && title_h > 0) {
fastchart_text_draw(t, title_font, title_size, pal.text,
W / 2, 12 + title_h, FASTCHART_ALIGN_CENTER,
ZSTR_VAL(self->title), NULL, 0);
}
fastchart_draw_text_annotations(t, (fastchart_obj *)self, &pal);
return 0;
}