]> git.cworth.org Git - acre/blob - acre.c
Cleverly adjust axis ranges for integer tick spacing.
[acre] / acre.c
1 /* acre - A cairo-based library for creating plots and charts.
2  *
3  * Copyright © 2009 Carl Worth <cworth@cworth.org>
4  *
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.
9  *
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.
14  *
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.
18  */
19
20 #include "acre.h"
21 #include "xmalloc.h"
22
23 #include <string.h>
24 #include <math.h>
25
26 typedef struct _acre_data_point_2d {
27     double x;
28     double y;
29 } acre_data_point_2d_t;
30
31 struct _acre_data {
32     char *name;
33
34     acre_data_point_2d_t *points;
35     unsigned int points_size;
36     unsigned int num_points;
37 };
38
39 typedef struct _acre_axis {
40     char *label;
41     double min;
42     double max;
43 } acre_axis_t;
44
45 struct _acre {
46     char *title;
47     acre_axis_t x_axis;
48     acre_axis_t y_axis;
49
50     acre_data_t **data;
51     unsigned int data_size;
52     unsigned int num_data;
53
54     /* Data for drawing. */
55     cairo_t *cr;
56
57     /* Total size including labels. */
58     int width;
59     int height;
60
61     /* Position and size of chart alone. */
62     PangoRectangle chart;
63 };
64
65 /* Create a new, empty plot. */
66 acre_t *
67 acre_create (void)
68 {
69     acre_t *acre;
70
71     acre = xmalloc (sizeof (acre_t));
72
73     acre->title = NULL;
74
75     acre->x_axis.label = NULL;
76     acre->x_axis.min = 0.0;
77     acre->x_axis.max = 0.0;
78
79     acre->y_axis.label = NULL;
80     acre->y_axis.min = 0.0;
81     acre->y_axis.max = 0.0;
82
83     acre->data = NULL;
84     acre->data_size = 0;
85     acre->num_data = 0;
86
87     return acre;
88 }
89
90 /* Destroy a plot. */
91 void
92 acre_destroy (acre_t *acre)
93 {
94     unsigned int i;
95
96     free (acre->title);
97     free (acre->x_axis.label);
98     free (acre->y_axis.label);
99
100     for (i = 0; i < acre->num_data; i++)
101         acre_data_destroy (acre->data[i]);
102
103     free (acre->data);
104
105     free (acre);
106 }
107
108 void
109 acre_set_title (acre_t *acre, const char *title)
110 {
111     free (acre->title);
112
113     acre->title = strdup (title);
114 }
115
116 void
117 acre_set_x_axis_label (acre_t *acre, const char *label)
118 {
119     free (acre->x_axis.label);
120
121     acre->x_axis.label = strdup (label);
122 }
123
124 void
125 acre_set_y_axis_label (acre_t *acre, const char *label)
126 {
127     free (acre->y_axis.label);
128
129     acre->y_axis.label = strdup (label);
130 }
131
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. */
134 void
135 acre_add_data (acre_t *acre, acre_data_t *data)
136 {
137     if (acre->num_data >= acre->data_size) {
138         acre->data_size *= 2;
139         if (acre->data_size == 0)
140             acre->data_size = 1;
141         acre->data = xrealloc_ab (acre->data,
142                                   acre->data_size,
143                                   sizeof (acre_data_t *));
144     }
145
146     acre->data[acre->num_data] = data;
147     acre->num_data++;
148 }
149
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
155
156 static void
157 _draw_title_and_labels (acre_t *acre)
158 {
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;
166
167     cairo_save (cr);
168
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);
173
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);
178
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);
183
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);
188
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);
193
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). */
198     acre->chart.x = 0;
199     acre->chart.y = 0;
200     acre->chart.width = acre->width;
201     acre->chart.height = acre->height;
202     while (1) {
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);
206
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);
210
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;
214
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);
217
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)
222         {
223             break;
224         }
225
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;
230     }
231
232     cairo_set_source_rgb (cr, 0, 0, 0);
233
234     cairo_move_to (cr, acre->chart.x, ACRE_PAD);
235     pango_cairo_show_layout (cr, title_layout);
236
237     cairo_save (cr);
238     {
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);
243     }
244     cairo_restore (cr);
245
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);
250
251     cairo_restore (cr);
252 }
253
254 /* For a given axis range, compute a step size (in data space) to
255  * generate a suitable number of ticks (5 or so). */
256 static double
257 _step_for_range (double range)
258 {
259     double step, scale_factor;
260
261     /* We want roughly 5 major ticks for the chart. */
262     step = range / 5;
263
264     /* Normalize the step so we can easily snap it to a desirable
265      * value. */
266     scale_factor = pow (10.0, floor (log10 (step)));
267     step /= scale_factor;
268
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)
274             step = 1.0;
275         else
276             step = 2.5;
277     } else {
278         if (step < 7.071067811865475)
279             step = 5.0;
280         else
281             step = 10.0;
282     }
283
284     /* Un-normalize and we now have the data value that we want to
285      * step at. */
286     return step * scale_factor;
287 }
288
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
292  * slightly. */
293 static double
294 _expand_range (double data_range, int pixel_size)
295 {
296     double step, pixel_step;
297
298     step = _step_for_range (data_range);
299     pixel_step = step * pixel_size / data_range;
300
301     /* We expand the range by the ratio of the pixel step to the floor
302      * of the pixel_step.
303      */
304     return data_range * pixel_step / floor (pixel_step);
305 }
306
307 static void
308 _compute_axis_ranges (acre_t *acre)
309 {
310     unsigned int d, i;
311     acre_data_t *data;
312     double x_range, new_x_range;
313     double y_range, new_y_range;
314
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;
323
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;
328         }
329     }
330
331     /* Next, increase the axis ranges just enough so that the step
332      * sizes for the ticks will be integers.
333      */
334     x_range = acre->x_axis.max - acre->x_axis.min;
335     new_x_range = _expand_range (x_range, acre->chart.width);
336
337     y_range = acre->y_axis.max - acre->y_axis.min;
338     new_y_range = _expand_range (y_range, acre->chart.height);
339
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;
343
344     acre->y_axis.min -= (new_y_range - y_range) / 2.0;
345     acre->y_axis.max += (new_y_range - y_range) / 2.0;
346 }
347
348 /* Setup a transformation in acre->cr such that data values plotted
349  * will appear where they should within the chart.
350  */
351 static void
352 _set_transform_to_data_space (acre_t *acre)
353 {
354     cairo_t *cr = acre->cr;
355
356     cairo_translate (cr,
357                      acre->chart.x,
358                      acre->chart.y + acre->chart.height);
359     cairo_scale (cr,
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);
363 }
364
365 static void
366 _draw_data (acre_t *acre)
367 {
368     cairo_t *cr = acre->cr;
369     unsigned int d, i;
370     acre_data_t *data;
371
372     cairo_save (cr);
373
374     cairo_set_source_rgb (cr, 0, 0, 0);
375
376     _set_transform_to_data_space (acre);
377
378     for (d = 0; d < acre->num_data; d++) {
379         data = acre->data[d];
380         cairo_new_path (cr);
381         for (i = 0; i < data->num_points; i++) {
382             cairo_line_to (cr,
383                            data->points[i].x,
384                            data->points[i].y);
385         }
386         cairo_save (cr);
387         {
388             cairo_identity_matrix (cr);
389             cairo_set_line_width (cr, 1.0);
390             cairo_stroke (cr);
391         }
392         cairo_restore (cr);
393     }
394
395     cairo_restore (cr);
396 }
397
398 static void
399 _draw_frame_and_ticks (acre_t *acre)
400 {
401     cairo_t *cr = acre->cr;
402     double step, x, y;
403
404     cairo_save (cr);
405
406     cairo_set_source_rgb (cr, 0, 0, 0); /* black */
407
408     /* First the ticks within data space. */
409     cairo_save (cr);
410     {
411         _set_transform_to_data_space (acre);
412
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);
417             cairo_save (cr);
418             {
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);
423                 cairo_stroke (cr);
424             }
425             cairo_restore (cr);
426             x += step;
427         }
428
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);
433             cairo_save (cr);
434             {
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);
439                 cairo_stroke (cr);
440             }
441             cairo_restore (cr);
442             y += step;
443         }
444     }
445     cairo_restore (cr);
446
447     /* Then the frame drawn in pixel space. */
448     cairo_rectangle (cr,
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);
452     cairo_stroke (cr);
453
454     cairo_restore (cr);
455 }
456
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,
460  * etc.)
461  */
462 void
463 acre_draw (acre_t *acre, cairo_t *cr, int width, int height)
464 {
465     acre->cr = cr;
466     acre->width = width;
467     acre->height = height;
468
469     cairo_save (cr);
470
471     cairo_set_source_rgb (cr, 1, 1, 1);
472
473     cairo_paint (cr);
474
475     _draw_title_and_labels (acre);
476
477     _compute_axis_ranges (acre);
478
479     _draw_data (acre);
480
481     _draw_frame_and_ticks (acre);
482 }
483
484 /* Create a new dataset---a collection of (x, y) datapoints. A single
485  * plot can contain multiple datasets, (see acre_add_data). */
486 acre_data_t *
487 acre_data_create (void)
488 {
489     acre_data_t *data;
490
491     data = xmalloc (sizeof (acre_data_t));
492
493     data->name = NULL;
494
495     data->points = NULL;
496     data->points_size = 0;
497     data->num_points = 0;
498
499     return data;
500 }
501
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. */
504 void
505 acre_data_destroy (acre_data_t *data)
506 {
507     free (data->points);
508
509     free (data);
510 }
511
512 /* Set the label for this dataset (to appear in the plot's key). */
513 void
514 acre_data_set_name (acre_data_t *data, const char *name)
515 {
516     free (data->name);
517
518     data->name = strdup (name);
519 }
520
521 /* Add a datapoint to the given dataset. */
522 void
523 acre_data_add_point_2d (acre_data_t *data, double x, double y)
524 {
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,
530                                     data->points_size,
531                                     sizeof (acre_data_point_2d_t));
532     }
533
534     data->points[data->num_points].x = x;
535     data->points[data->num_points].y = y;
536     data->num_points++;
537 }