1 /* acre - A cairo-based library for creating plots and charts.
3 * Copyright © 2009 Carl Worth <cworth@cworth.org>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the
8 * License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
26 typedef struct _acre_data_point_2d {
29 } acre_data_point_2d_t;
34 acre_data_point_2d_t *points;
35 unsigned int points_size;
36 unsigned int num_points;
39 typedef struct _acre_axis {
51 unsigned int data_size;
52 unsigned int num_data;
54 /* Data for drawing. */
57 /* Total size including labels. */
61 /* Position and size of chart alone. */
65 /* Create a new, empty plot. */
71 acre = xmalloc (sizeof (acre_t));
75 acre->x_axis.label = NULL;
76 acre->x_axis.min = 0.0;
77 acre->x_axis.max = 0.0;
79 acre->y_axis.label = NULL;
80 acre->y_axis.min = 0.0;
81 acre->y_axis.max = 0.0;
92 acre_destroy (acre_t *acre)
97 free (acre->x_axis.label);
98 free (acre->y_axis.label);
100 for (i = 0; i < acre->num_data; i++)
101 acre_data_destroy (acre->data[i]);
109 acre_set_title (acre_t *acre, const char *title)
113 acre->title = strdup (title);
117 acre_set_x_axis_label (acre_t *acre, const char *label)
119 free (acre->x_axis.label);
121 acre->x_axis.label = strdup (label);
125 acre_set_y_axis_label (acre_t *acre, const char *label)
127 free (acre->y_axis.label);
129 acre->y_axis.label = strdup (label);
132 /* Add a dataset to the plot. The plot assumes ownership of the
133 * dataset so it is not necessary to call acre_data_destroy on it. */
135 acre_add_data (acre_t *acre, acre_data_t *data)
137 if (acre->num_data >= acre->data_size) {
138 acre->data_size *= 2;
139 if (acre->data_size == 0)
141 acre->data = xrealloc_ab (acre->data,
143 sizeof (acre_data_t *));
146 acre->data[acre->num_data] = data;
150 #define ACRE_FONT_FAMILY "sans"
151 #define ACRE_FONT_SIZE 12
152 #define ACRE_TITLE_FONT_SIZE 32
153 #define ACRE_PAD (ACRE_FONT_SIZE)
154 #define ACRE_TICK_SIZE 6
157 _draw_title_and_labels (acre_t *acre)
159 cairo_t *cr = acre->cr;
160 PangoFontDescription *acre_font, *title_font;
161 PangoLayout *title_layout, *x_axis_layout, *y_axis_layout;
162 int title_width, title_height;
163 int x_axis_width, x_axis_height;
164 int y_axis_width, y_axis_height;
165 PangoRectangle new_chart;
169 acre_font = pango_font_description_new ();
170 pango_font_description_set_family (acre_font, ACRE_FONT_FAMILY);
171 pango_font_description_set_absolute_size (acre_font,
172 ACRE_FONT_SIZE * PANGO_SCALE);
174 title_font = pango_font_description_new ();
175 pango_font_description_set_family (title_font, ACRE_FONT_FAMILY);
176 pango_font_description_set_absolute_size (title_font,
177 ACRE_TITLE_FONT_SIZE * PANGO_SCALE);
179 title_layout = pango_cairo_create_layout (cr);
180 pango_layout_set_font_description (title_layout, title_font);
181 pango_layout_set_text (title_layout, acre->title, -1);
182 pango_layout_set_alignment (title_layout, PANGO_ALIGN_CENTER);
184 x_axis_layout = pango_cairo_create_layout (cr);
185 pango_layout_set_font_description (x_axis_layout, acre_font);
186 pango_layout_set_text (x_axis_layout, acre->x_axis.label, -1);
187 pango_layout_set_alignment (x_axis_layout, PANGO_ALIGN_CENTER);
189 y_axis_layout = pango_cairo_create_layout (cr);
190 pango_layout_set_font_description (y_axis_layout, acre_font);
191 pango_layout_set_text (y_axis_layout, acre->y_axis.label, -1);
192 pango_layout_set_alignment (y_axis_layout, PANGO_ALIGN_CENTER);
194 /* Iterate with the layout of the title and axis labels until they
195 * are stable, (this requires iteration since we don't know what
196 * to set their widths to in advance due to the wrapping of the
197 * other elements). */
200 acre->chart.width = acre->width;
201 acre->chart.height = acre->height;
203 pango_layout_set_width (title_layout, acre->chart.width * PANGO_SCALE);
204 pango_layout_set_width (x_axis_layout, acre->chart.width * PANGO_SCALE);
205 pango_layout_set_width (y_axis_layout, acre->chart.height * PANGO_SCALE);
207 pango_layout_get_pixel_size (title_layout, &title_width, &title_height);
208 pango_layout_get_pixel_size (x_axis_layout, &x_axis_width, &x_axis_height);
209 pango_layout_get_pixel_size (y_axis_layout, &y_axis_width, &y_axis_height);
211 new_chart.x = ACRE_PAD + y_axis_height +
212 ACRE_PAD + ACRE_FONT_SIZE;
213 new_chart.width = acre->width - acre->chart.x - ACRE_PAD;
215 new_chart.y = ACRE_PAD + title_height + ACRE_PAD;
216 new_chart.height = acre->height - acre->chart.y - (ACRE_FONT_SIZE + ACRE_PAD + x_axis_height + ACRE_PAD);
218 if (new_chart.x == acre->chart.x &&
219 new_chart.y == acre->chart.y &&
220 new_chart.width == acre->chart.width &&
221 new_chart.height == acre->chart.height)
226 acre->chart.x = new_chart.x;
227 acre->chart.y = new_chart.y;
228 acre->chart.width = new_chart.width;
229 acre->chart.height = new_chart.height;
232 cairo_set_source_rgb (cr, 0, 0, 0);
234 cairo_move_to (cr, acre->chart.x, ACRE_PAD);
235 pango_cairo_show_layout (cr, title_layout);
239 cairo_translate (cr, ACRE_PAD, acre->chart.y + acre->chart.height);
240 cairo_rotate (cr, - M_PI / 2.0);
241 cairo_move_to (cr, 0, 0);
242 pango_cairo_show_layout (cr, y_axis_layout);
246 cairo_move_to (cr, acre->chart.x,
247 acre->chart.y + acre->chart.height +
248 ACRE_FONT_SIZE + ACRE_PAD);
249 pango_cairo_show_layout (cr, x_axis_layout);
254 /* For a given axis range, compute a step size (in data space) to
255 * generate a suitable number of ticks (5 or so). */
257 _step_for_range (double range)
259 double step, scale_factor;
261 /* We want roughly 5 major ticks for the chart. */
264 /* Normalize the step so we can easily snap it to a desirable
266 scale_factor = pow (10.0, floor (log10 (step)));
267 step /= scale_factor;
269 /* We want increments of 1, 2.5, 5, or 10 (times some power of
270 * 10). The threshold values between these are computed
271 * logarithmically. */
272 if (step < 3.535533905932738) {
273 if (step < 1.58113883008419)
278 if (step < 7.071067811865475)
284 /* Un-normalize and we now have the data value that we want to
286 return step * scale_factor;
289 /* Given an axis range, we can compute a desired data-space step
290 * amount for the major ticks (see _step_for_range). To get
291 * nice-looking pixel-snapped ticks we want to expand the range
294 _expand_range (double data_range, int pixel_size)
296 double step, pixel_step;
298 step = _step_for_range (data_range);
299 pixel_step = step * pixel_size / data_range;
301 /* We expand the range by the ratio of the pixel step to the floor
304 return data_range * pixel_step / floor (pixel_step);
308 _compute_axis_ranges (acre_t *acre)
312 double x_range, new_x_range;
313 double y_range, new_y_range;
315 /* First, simply find the extrema of the data. */
316 for (d = 0; d < acre->num_data; d++) {
317 data = acre->data[d];
318 for (i = 0; i < data->num_points; i++) {
319 if (data->points[i].x < acre->x_axis.min)
320 acre->x_axis.min = data->points[i].x;
321 if (data->points[i].x > acre->x_axis.max)
322 acre->x_axis.max = data->points[i].x;
324 if (data->points[i].y < acre->y_axis.min)
325 acre->y_axis.min = data->points[i].y;
326 if (data->points[i].y > acre->y_axis.max)
327 acre->y_axis.max = data->points[i].y;
331 /* Next, increase the axis ranges just enough so that the step
332 * sizes for the ticks will be integers.
334 x_range = acre->x_axis.max - acre->x_axis.min;
335 new_x_range = _expand_range (x_range, acre->chart.width);
337 y_range = acre->y_axis.max - acre->y_axis.min;
338 new_y_range = _expand_range (y_range, acre->chart.height);
340 /* And spread the increase out on either side of the range. */
341 acre->x_axis.min -= (new_x_range - x_range) / 2.0;
342 acre->x_axis.max += (new_x_range - x_range) / 2.0;
344 acre->y_axis.min -= (new_y_range - y_range) / 2.0;
345 acre->y_axis.max += (new_y_range - y_range) / 2.0;
348 /* Setup a transformation in acre->cr such that data values plotted
349 * will appear where they should within the chart.
352 _set_transform_to_data_space (acre_t *acre)
354 cairo_t *cr = acre->cr;
358 acre->chart.y + acre->chart.height);
360 acre->chart.width / (acre->x_axis.max - acre->x_axis.min),
361 - acre->chart.height /(acre->y_axis.max - acre->y_axis.min));
362 cairo_translate (cr, -acre->x_axis.min, -acre->y_axis.min);
366 _draw_data (acre_t *acre)
368 cairo_t *cr = acre->cr;
374 cairo_set_source_rgb (cr, 0, 0, 0);
376 _set_transform_to_data_space (acre);
378 for (d = 0; d < acre->num_data; d++) {
379 data = acre->data[d];
381 for (i = 0; i < data->num_points; i++) {
388 cairo_identity_matrix (cr);
389 cairo_set_line_width (cr, 1.0);
399 _draw_frame_and_ticks (acre_t *acre)
401 cairo_t *cr = acre->cr;
406 cairo_set_source_rgb (cr, 0, 0, 0); /* black */
408 /* First the ticks within data space. */
411 _set_transform_to_data_space (acre);
413 step = _step_for_range (acre->x_axis.max -acre->x_axis.min);
414 x = (floor (acre->x_axis.min / step) + 1) * step;
415 while (x <= acre->x_axis.max) {
416 cairo_move_to (cr, x, acre->y_axis.min);
419 cairo_identity_matrix (cr);
420 cairo_rel_line_to (cr, 0, 0.5);
421 cairo_rel_line_to (cr, 0, -ACRE_TICK_SIZE);
422 cairo_set_line_width (cr, 1.0);
429 step = _step_for_range (acre->y_axis.max -acre->y_axis.min);
430 y = (floor (acre->y_axis.min / step) + 1) * step;
431 while (y <= acre->y_axis.max) {
432 cairo_move_to (cr, acre->x_axis.min, y);
435 cairo_identity_matrix (cr);
436 cairo_rel_line_to (cr, -0.5, 0);
437 cairo_rel_line_to (cr, ACRE_TICK_SIZE, 0);
438 cairo_set_line_width (cr, 1.0);
447 /* Then the frame drawn in pixel space. */
449 acre->chart.x - 0.5, acre->chart.y - 0.5,
450 acre->chart.width + 1.0, acre->chart.height + 1.0);
451 cairo_set_line_width (cr, 1.0);
457 /* Draw the plot to the given cairo context within a user-space
458 * rectangle from (0, 0) to (width, height). This size includes all
459 * space for extra-plot elements (such as the title, the axis labels,
463 acre_draw (acre_t *acre, cairo_t *cr, int width, int height)
467 acre->height = height;
471 cairo_set_source_rgb (cr, 1, 1, 1);
475 _draw_title_and_labels (acre);
477 _compute_axis_ranges (acre);
481 _draw_frame_and_ticks (acre);
484 /* Create a new dataset---a collection of (x, y) datapoints. A single
485 * plot can contain multiple datasets, (see acre_add_data). */
487 acre_data_create (void)
491 data = xmalloc (sizeof (acre_data_t));
496 data->points_size = 0;
497 data->num_points = 0;
502 /* Destroy an acre dataset. Do not call this function if the dataset
503 * has been added to an acre_t plot with acre_add_data. */
505 acre_data_destroy (acre_data_t *data)
512 /* Set the label for this dataset (to appear in the plot's key). */
514 acre_data_set_name (acre_data_t *data, const char *name)
518 data->name = strdup (name);
521 /* Add a datapoint to the given dataset. */
523 acre_data_add_point_2d (acre_data_t *data, double x, double y)
525 if (data->num_points >= data->points_size) {
526 data->points_size *= 2;
527 if (data->points_size == 0)
528 data->points_size = 16;
529 data->points = xrealloc_ab (data->points,
531 sizeof (acre_data_point_2d_t));
534 data->points[data->num_points].x = x;
535 data->points[data->num_points].y = y;