]> git.cworth.org Git - acre/blob - acre.c
Draw major ticks along the X and Y axes.
[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 static void
255 _compute_axis_ranges (acre_t *acre)
256 {
257     unsigned int d, i;
258     acre_data_t *data;
259
260     for (d = 0; d < acre->num_data; d++) {
261         data = acre->data[d];
262         for (i = 0; i < data->num_points; i++) {
263             if (data->points[i].x < acre->x_axis.min)
264                 acre->x_axis.min = data->points[i].x;
265             if (data->points[i].x > acre->x_axis.max)
266                 acre->x_axis.max = data->points[i].x;
267
268             if (data->points[i].y < acre->y_axis.min)
269                 acre->y_axis.min = data->points[i].y;
270             if (data->points[i].y > acre->y_axis.max)
271                 acre->y_axis.max = data->points[i].y;
272         }
273     }
274 }
275
276 /* Setup a transformation in acre->cr such that data values plotted
277  * will appear where they should within the chart.
278  */
279 static void
280 _set_transform_to_data_space (acre_t *acre)
281 {
282     cairo_t *cr = acre->cr;
283
284     cairo_translate (cr,
285                      acre->chart.x + 0.5,
286                      acre->chart.y + acre->chart.height - 0.5);
287     cairo_scale (cr,
288                  acre->chart.width / (acre->x_axis.max - acre->x_axis.min),
289                  - acre->chart.height /(acre->y_axis.max - acre->y_axis.min));
290     cairo_translate (cr, -acre->x_axis.min, -acre->y_axis.min);
291 }
292
293 static void
294 _draw_data (acre_t *acre)
295 {
296     cairo_t *cr = acre->cr;
297     unsigned int d, i;
298     acre_data_t *data;
299
300     cairo_save (cr);
301
302     cairo_set_source_rgb (cr, 0, 0, 0);
303
304     _set_transform_to_data_space (acre);
305
306     for (d = 0; d < acre->num_data; d++) {
307         data = acre->data[d];
308         cairo_new_path (cr);
309         for (i = 0; i < data->num_points; i++) {
310             cairo_line_to (cr,
311                            data->points[i].x,
312                            data->points[i].y);
313         }
314         cairo_save (cr);
315         {
316             cairo_identity_matrix (cr);
317             cairo_set_line_width (cr, 1.0);
318             cairo_stroke (cr);
319         }
320         cairo_restore (cr);
321     }
322
323     cairo_restore (cr);
324 }
325
326 static double
327 _step_for_range (double range)
328 {
329     double step, scale_factor;
330
331     /* We want roughly 5 major ticks for the chart. */
332     step = range / 5;
333
334     /* Normalize the step so we can easily snap it to a desirable
335      * value. */
336     scale_factor = pow (10.0, floor (log10 (step)));
337     step /= scale_factor;
338
339     /* We want increments of 1, 2.5, 5, or 10 (times some power of
340      * 10). The threshold values between these are computed
341      * logarithmically. */
342     if (step < 3.535533905932738) {
343         if (step < 1.58113883008419)
344             step = 1.0;
345         else
346             step = 2.5;
347     } else {
348         if (step < 7.071067811865475)
349             step = 5.0;
350         else
351             step = 10.0;
352     }
353
354     /* Un-normalize and we now have the data value that we want to
355      * step at. */
356     return step * scale_factor;
357 }
358
359 static void
360 _draw_frame_and_ticks (acre_t *acre)
361 {
362     cairo_t *cr = acre->cr;
363     double step, x, y;
364
365     cairo_save (cr);
366
367     cairo_set_source_rgb (cr, 0, 0, 0); /* black */
368
369     /* First the ticks within data space. */
370     cairo_save (cr);
371     {
372         _set_transform_to_data_space (acre);
373
374         step = _step_for_range (acre->x_axis.max -acre->x_axis.min);
375         x = (floor (acre->x_axis.min / step) + 1) * step;
376         while (x <= acre->x_axis.max) {
377             cairo_move_to (cr, x, acre->y_axis.min);
378             cairo_save (cr);
379             {
380                 cairo_identity_matrix (cr);
381                 cairo_rel_line_to (cr, 0, 0.5);
382                 cairo_rel_line_to (cr, 0, -ACRE_TICK_SIZE);
383                 cairo_set_line_width (cr, 1.0);
384                 cairo_stroke (cr);
385             }
386             cairo_restore (cr);
387             x += step;
388         }
389
390         step = _step_for_range (acre->y_axis.max -acre->y_axis.min);
391         y = (floor (acre->y_axis.min / step) + 1) * step;
392         while (y <= acre->y_axis.max) {
393             cairo_move_to (cr, acre->x_axis.min, y);
394             cairo_save (cr);
395             {
396                 cairo_identity_matrix (cr);
397                 cairo_rel_line_to (cr, -0.5, 0);
398                 cairo_rel_line_to (cr, ACRE_TICK_SIZE, 0);
399                 cairo_set_line_width (cr, 1.0);
400                 cairo_stroke (cr);
401             }
402             cairo_restore (cr);
403             y += step;
404         }
405     }
406     cairo_restore (cr);
407
408     /* Then the frame drawn in pixel space. */
409     cairo_rectangle (cr,
410                      acre->chart.x - 0.5, acre->chart.y - 0.5,
411                      acre->chart.width + 1.0, acre->chart.height + 1.0);
412     cairo_set_line_width (cr, 1.0);
413     cairo_stroke (cr);
414
415     cairo_restore (cr);
416 }
417
418 /* Draw the plot to the given cairo context within a user-space
419  * rectangle from (0, 0) to (width, height). This size includes all
420  * space for extra-plot elements (such as the title, the axis labels,
421  * etc.)
422  */
423 void
424 acre_draw (acre_t *acre, cairo_t *cr, int width, int height)
425 {
426     acre->cr = cr;
427     acre->width = width;
428     acre->height = height;
429
430     cairo_save (cr);
431
432     cairo_set_source_rgb (cr, 1, 1, 1);
433
434     cairo_paint (cr);
435
436     _draw_title_and_labels (acre);
437
438     _compute_axis_ranges (acre);
439
440     _draw_data (acre);
441
442     _draw_frame_and_ticks (acre);
443 }
444
445 /* Create a new dataset---a collection of (x, y) datapoints. A single
446  * plot can contain multiple datasets, (see acre_add_data). */
447 acre_data_t *
448 acre_data_create (void)
449 {
450     acre_data_t *data;
451
452     data = xmalloc (sizeof (acre_data_t));
453
454     data->name = NULL;
455
456     data->points = NULL;
457     data->points_size = 0;
458     data->num_points = 0;
459
460     return data;
461 }
462
463 /* Destroy an acre dataset. Do not call this function if the dataset
464  * has been added to an acre_t plot with acre_add_data. */
465 void
466 acre_data_destroy (acre_data_t *data)
467 {
468     free (data->points);
469
470     free (data);
471 }
472
473 /* Set the label for this dataset (to appear in the plot's key). */
474 void
475 acre_data_set_name (acre_data_t *data, const char *name)
476 {
477     free (data->name);
478
479     data->name = strdup (name);
480 }
481
482 /* Add a datapoint to the given dataset. */
483 void
484 acre_data_add_point_2d (acre_data_t *data, double x, double y)
485 {
486     if (data->num_points >= data->points_size) {
487         data->points_size *= 2;
488         if (data->points_size == 0)
489             data->points_size = 16;
490         data->points = xrealloc_ab (data->points,
491                                     data->points_size,
492                                     sizeof (acre_data_point_2d_t));
493     }
494
495     data->points[data->num_points].x = x;
496     data->points[data->num_points].y = y;
497     data->num_points++;
498 }