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.
20 #define _ISOC99_SOURCE /* for round() */
21 #define _XOPEN_SOURCE 500
22 #define _GNU_SOURCE /* for asprintf() */
31 typedef struct _acre_data_point_2d {
34 } acre_data_point_2d_t;
39 acre_data_point_2d_t *points;
40 unsigned int points_size;
41 unsigned int num_points;
44 typedef struct _acre_axis {
56 unsigned int data_size;
57 unsigned int num_data;
59 /* Data for drawing. */
61 PangoFontDescription *font;
63 /* Total size including labels. */
67 /* Position and size of chart alone. */
71 /* Create a new, empty plot. */
77 acre = xmalloc (sizeof (acre_t));
81 acre->x_axis.label = NULL;
82 acre->x_axis.min = 0.0;
83 acre->x_axis.max = 0.0;
85 acre->y_axis.label = NULL;
86 acre->y_axis.min = 0.0;
87 acre->y_axis.max = 0.0;
98 acre_destroy (acre_t *acre)
103 free (acre->x_axis.label);
104 free (acre->y_axis.label);
106 for (i = 0; i < acre->num_data; i++)
107 acre_data_destroy (acre->data[i]);
115 acre_set_title (acre_t *acre, const char *title)
119 acre->title = strdup (title);
123 acre_set_x_axis_label (acre_t *acre, const char *label)
125 free (acre->x_axis.label);
127 acre->x_axis.label = strdup (label);
131 acre_set_y_axis_label (acre_t *acre, const char *label)
133 free (acre->y_axis.label);
135 acre->y_axis.label = strdup (label);
138 /* Add a dataset to the plot. The plot assumes ownership of the
139 * dataset so it is not necessary to call acre_data_destroy on it. */
141 acre_add_data (acre_t *acre, acre_data_t *data)
143 if (acre->num_data >= acre->data_size) {
144 acre->data_size *= 2;
145 if (acre->data_size == 0)
147 acre->data = xrealloc_ab (acre->data,
149 sizeof (acre_data_t *));
152 acre->data[acre->num_data] = data;
156 #define ACRE_FONT_FAMILY "sans"
157 #define ACRE_FONT_SIZE 12
158 #define ACRE_TITLE_FONT_SIZE 32
159 #define ACRE_PAD (ACRE_FONT_SIZE)
160 #define ACRE_TICK_SIZE 6
163 _create_layout (acre_t *acre, const char *text)
167 layout = pango_cairo_create_layout (acre->cr);
168 pango_layout_set_font_description (layout, acre->font);
169 pango_layout_set_text (layout, text, -1);
170 pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER);
175 #define PRINTF_FORMAT(fmt_index, va_index) __attribute__ ((__format__(__printf__, fmt_index, va_index)))
178 _create_layout_vprintf (acre_t *acre, const char *fmt, va_list ap)
183 vasprintf (&text, fmt, ap);
185 layout = _create_layout (acre, text);
193 _create_layout_printf (acre_t *acre, const char *fmt, ...)
194 PRINTF_FORMAT (2, 3);
197 _create_layout_printf (acre_t *acre, const char *fmt, ...)
204 layout = _create_layout_vprintf (acre, fmt, ap);
212 _destroy_layout (PangoLayout *layout)
214 g_object_unref (layout);
218 _show_layout (cairo_t *cr, PangoLayout *layout)
220 pango_cairo_show_layout (cr, layout);
222 _destroy_layout (layout);
226 _draw_title_and_labels (acre_t *acre)
228 cairo_t *cr = acre->cr;
229 PangoFontDescription *title_font;
230 PangoLayout *title_layout, *x_axis_layout, *y_axis_layout;
231 int title_width, title_height;
232 int x_axis_width, x_axis_height;
233 int y_axis_width, y_axis_height;
234 PangoRectangle new_chart;
238 acre->font = pango_font_description_new ();
239 pango_font_description_set_family (acre->font, ACRE_FONT_FAMILY);
240 pango_font_description_set_absolute_size (acre->font,
241 ACRE_FONT_SIZE * PANGO_SCALE);
243 title_font = pango_font_description_new ();
244 pango_font_description_set_family (title_font, ACRE_FONT_FAMILY);
245 pango_font_description_set_absolute_size (title_font,
246 ACRE_TITLE_FONT_SIZE * PANGO_SCALE);
248 title_layout = _create_layout (acre, acre->title);
249 pango_layout_set_font_description (title_layout, title_font);
251 x_axis_layout = _create_layout (acre, acre->x_axis.label);
252 y_axis_layout = _create_layout (acre, acre->y_axis.label);
254 /* Iterate with the layout of the title and axis labels until they
255 * are stable, (this requires iteration since we don't know what
256 * to set their widths to in advance due to the wrapping of the
257 * other elements). */
260 acre->chart.width = acre->width;
261 acre->chart.height = acre->height;
263 pango_layout_set_width (title_layout, acre->chart.width * PANGO_SCALE);
264 pango_layout_set_width (x_axis_layout, acre->chart.width * PANGO_SCALE);
265 pango_layout_set_width (y_axis_layout, acre->chart.height * PANGO_SCALE);
267 pango_layout_get_pixel_size (title_layout, &title_width, &title_height);
268 pango_layout_get_pixel_size (x_axis_layout, &x_axis_width, &x_axis_height);
269 pango_layout_get_pixel_size (y_axis_layout, &y_axis_width, &y_axis_height);
271 new_chart.x = ACRE_PAD + y_axis_height +
272 ACRE_PAD + ACRE_FONT_SIZE;
273 new_chart.width = acre->width - acre->chart.x - ACRE_PAD;
275 new_chart.y = ACRE_PAD + title_height + ACRE_PAD;
276 new_chart.height = acre->height - acre->chart.y - (ACRE_FONT_SIZE + ACRE_PAD + x_axis_height + ACRE_PAD);
278 if (new_chart.x == acre->chart.x &&
279 new_chart.y == acre->chart.y &&
280 new_chart.width == acre->chart.width &&
281 new_chart.height == acre->chart.height)
286 acre->chart.x = new_chart.x;
287 acre->chart.y = new_chart.y;
288 acre->chart.width = new_chart.width;
289 acre->chart.height = new_chart.height;
292 cairo_set_source_rgb (cr, 0, 0, 0);
294 cairo_move_to (cr, acre->chart.x, ACRE_PAD);
295 _show_layout (cr, title_layout);
299 cairo_translate (cr, ACRE_PAD, acre->chart.y + acre->chart.height);
300 cairo_rotate (cr, - M_PI / 2.0);
301 cairo_move_to (cr, 0, 0);
302 _show_layout (cr, y_axis_layout);
306 cairo_move_to (cr, acre->chart.x,
307 acre->chart.y + acre->chart.height +
308 ACRE_FONT_SIZE + ACRE_PAD);
309 _show_layout (cr, x_axis_layout);
314 /* For a given axis range, compute a step size (in data space) to
315 * generate a suitable number of ticks (5 or so). */
317 _step_for_range (double range)
319 double step, scale_factor;
321 /* We want roughly 5 major ticks for the chart. */
324 /* Normalize the step so we can easily snap it to a desirable
326 scale_factor = pow (10.0, floor (log10 (step)));
327 step /= scale_factor;
329 /* We want increments of 1, 2.5, 5, or 10 (times some power of
330 * 10). The threshold values between these are computed
331 * logarithmically. */
332 if (step < 3.535533905932738) {
333 if (step < 1.58113883008419)
338 if (step < 7.071067811865475)
344 /* Un-normalize and we now have the data value that we want to
346 return step * scale_factor;
349 /* Given an axis range, we can compute a desired data-space step
350 * amount for the major ticks (see _step_for_range). To get
351 * nice-looking pixel-snapped ticks we want to expand the range
354 _expand_range_for_width (double *axis_min, double *axis_max, int pixel_size)
356 double range, new_range, step, pixel_step;
358 range = *axis_max - *axis_min;
360 step = _step_for_range (range);
361 pixel_step = step * pixel_size / range;
363 /* We expand the range by the ratio of the pixel step to the floor
366 new_range = range * pixel_step / floor (pixel_step);
368 /* And spread the increase out on either side of the range. */
369 *axis_min -= (new_range - range) / 2.0;
370 *axis_max += (new_range - range) / 2.0;
373 /* Setup a transformation in acre->cr such that data values plotted
374 * will appear where they should within the chart.
377 _set_transform_to_data_space (acre_t *acre)
379 cairo_t *cr = acre->cr;
383 acre->chart.y + acre->chart.height);
385 acre->chart.width / (acre->x_axis.max - acre->x_axis.min),
386 - acre->chart.height /(acre->y_axis.max - acre->y_axis.min));
387 cairo_translate (cr, -acre->x_axis.min, -acre->y_axis.min);
391 _compute_axis_ranges (acre_t *acre)
395 double x_adjust, y_adjust;
396 cairo_t *cr = acre->cr;
398 /* First, simply find the extrema of the data. */
399 for (d = 0; d < acre->num_data; d++) {
400 data = acre->data[d];
401 for (i = 0; i < data->num_points; i++) {
402 if (data->points[i].x < acre->x_axis.min)
403 acre->x_axis.min = data->points[i].x;
404 if (data->points[i].x > acre->x_axis.max)
405 acre->x_axis.max = data->points[i].x;
407 if (data->points[i].y < acre->y_axis.min)
408 acre->y_axis.min = data->points[i].y;
409 if (data->points[i].y > acre->y_axis.max)
410 acre->y_axis.max = data->points[i].y;
414 /* Next, increase the axis ranges just enough so that the step
415 * sizes for the ticks will be integers.
417 _expand_range_for_width (&acre->x_axis.min,
421 _expand_range_for_width (&acre->y_axis.min,
425 /* Finally, we also translate the axis ranges slightly so that the
426 * ticks land on half-integer device-pixel positions.
430 _set_transform_to_data_space (acre);
434 cairo_user_to_device (cr, &x_adjust, &y_adjust);
435 x_adjust = (round (x_adjust + 0.5) - 0.5) - x_adjust;
436 y_adjust = (round (y_adjust + 0.5) - 0.5) - y_adjust;
437 cairo_device_to_user_distance (cr, &x_adjust, &y_adjust);
439 acre->x_axis.min -= x_adjust;
440 acre->x_axis.max -= x_adjust;
442 acre->y_axis.min -= y_adjust;
443 acre->y_axis.max -= y_adjust;
449 _draw_data (acre_t *acre)
451 cairo_t *cr = acre->cr;
457 cairo_set_source_rgb (cr, 0, 0, 0);
459 _set_transform_to_data_space (acre);
461 for (d = 0; d < acre->num_data; d++) {
462 data = acre->data[d];
464 for (i = 0; i < data->num_points; i++) {
471 cairo_identity_matrix (cr);
472 cairo_set_line_width (cr, 1.0);
482 _draw_frame_and_ticks (acre_t *acre)
484 cairo_t *cr = acre->cr;
489 cairo_set_source_rgb (cr, 0, 0, 0); /* black */
491 /* First the ticks within data space. */
494 _set_transform_to_data_space (acre);
496 step = _step_for_range (acre->x_axis.max -acre->x_axis.min);
497 x = (floor (acre->x_axis.min / step) + 1) * step;
498 while (x <= acre->x_axis.max) {
499 cairo_move_to (cr, x, acre->y_axis.min);
505 cairo_identity_matrix (cr);
506 cairo_rel_line_to (cr, 0, 0.5);
507 cairo_rel_line_to (cr, 0, -ACRE_TICK_SIZE-0.5);
508 cairo_set_line_width (cr, 1.0);
518 layout = _create_layout_printf (acre, "%g", x);
519 cairo_move_to (cr, x, acre->y_axis.min);
520 cairo_identity_matrix (cr);
521 pango_layout_get_pixel_size (layout, &width, &height);
522 cairo_rel_move_to (cr, -width / 2, 2);
523 _show_layout (cr, layout);
531 step = _step_for_range (acre->y_axis.max -acre->y_axis.min);
532 y = (floor (acre->y_axis.min / step) + 1) * step;
533 while (y <= acre->y_axis.max) {
534 cairo_move_to (cr, acre->x_axis.min, y);
540 cairo_identity_matrix (cr);
541 cairo_rel_line_to (cr, -0.5, 0);
542 cairo_rel_line_to (cr, ACRE_TICK_SIZE+0.5, 0);
543 cairo_set_line_width (cr, 1.0);
553 layout = _create_layout_printf (acre, "%g", y);
554 cairo_move_to (cr, acre->x_axis.min, y);
555 cairo_identity_matrix (cr);
556 pango_layout_get_pixel_size (layout, &width, &height);
557 cairo_rel_move_to (cr, -width-2, -height/2);
558 _show_layout (cr, layout);
568 /* Then the frame drawn in pixel space. */
570 acre->chart.x - 0.5, acre->chart.y - 0.5,
571 acre->chart.width + 1.0, acre->chart.height + 1.0);
572 cairo_set_line_width (cr, 1.0);
578 /* Draw the plot to the given cairo context within a user-space
579 * rectangle from (0, 0) to (width, height). This size includes all
580 * space for extra-plot elements (such as the title, the axis labels,
584 acre_draw (acre_t *acre, cairo_t *cr, int width, int height)
588 acre->height = height;
592 cairo_set_source_rgb (cr, 1, 1, 1);
596 _draw_title_and_labels (acre);
598 _compute_axis_ranges (acre);
602 _draw_frame_and_ticks (acre);
605 /* Create a new dataset---a collection of (x, y) datapoints. A single
606 * plot can contain multiple datasets, (see acre_add_data). */
608 acre_data_create (void)
612 data = xmalloc (sizeof (acre_data_t));
617 data->points_size = 0;
618 data->num_points = 0;
623 /* Destroy an acre dataset. Do not call this function if the dataset
624 * has been added to an acre_t plot with acre_add_data. */
626 acre_data_destroy (acre_data_t *data)
633 /* Set the label for this dataset (to appear in the plot's key). */
635 acre_data_set_name (acre_data_t *data, const char *name)
639 data->name = strdup (name);
642 /* Add a datapoint to the given dataset. */
644 acre_data_add_point_2d (acre_data_t *data, double x, double y)
646 if (data->num_points >= data->points_size) {
647 data->points_size *= 2;
648 if (data->points_size == 0)
649 data->points_size = 16;
650 data->points = xrealloc_ab (data->points,
652 sizeof (acre_data_point_2d_t));
655 data->points[data->num_points].x = x;
656 data->points[data->num_points].y = y;