]> git.cworth.org Git - acre/blob - acre.c
Add preliminary data plotting.
[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 4
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 static void
277 _draw_data (acre_t *acre)
278 {
279     cairo_t *cr = acre->cr;
280     unsigned int d, i;
281     acre_data_t *data;
282
283     cairo_save (cr);
284
285     cairo_set_source_rgb (cr, 0, 0, 0);
286
287     cairo_translate (cr,
288                      acre->chart.x + 0.5,
289                      acre->chart.y + acre->chart.height - 0.5);
290     cairo_scale (cr,
291                  acre->chart.width / (acre->x_axis.max - acre->x_axis.min),
292                  - acre->chart.height /(acre->y_axis.max - acre->y_axis.min));
293     cairo_translate (cr, -acre->x_axis.min, -acre->y_axis.min);
294
295     for (d = 0; d < acre->num_data; d++) {
296         data = acre->data[d];
297         cairo_new_path (cr);
298         for (i = 0; i < data->num_points; i++) {
299             cairo_line_to (cr,
300                            data->points[i].x,
301                            data->points[i].y);
302         }
303         cairo_save (cr);
304         {
305             cairo_identity_matrix (cr);
306             cairo_set_line_width (cr, 1.0);
307             cairo_stroke (cr);
308         }
309         cairo_restore (cr);
310     }
311
312     cairo_restore (cr);
313 }
314
315 static void
316 _draw_frame_and_ticks (acre_t *acre)
317 {
318     cairo_t *cr = acre->cr;
319
320     cairo_save (cr);
321
322     cairo_rectangle (cr,
323                      acre->chart.x - 0.5, acre->chart.y - 0.5,
324                      acre->chart.width + 1.0, acre->chart.height + 1.0);
325     cairo_set_line_width (cr, 1.0);
326     cairo_set_source_rgb (cr, 0, 0, 0);
327     cairo_stroke (cr);
328
329     cairo_restore (cr);
330 }
331
332 /* Draw the plot to the given cairo context within a user-space
333  * rectangle from (0, 0) to (width, height). This size includes all
334  * space for extra-plot elements (such as the title, the axis labels,
335  * etc.)
336  */
337 void
338 acre_draw (acre_t *acre, cairo_t *cr, int width, int height)
339 {
340     acre->cr = cr;
341     acre->width = width;
342     acre->height = height;
343
344     cairo_save (cr);
345
346     cairo_set_source_rgb (cr, 1, 1, 1);
347
348     cairo_paint (cr);
349
350     _draw_title_and_labels (acre);
351
352     _compute_axis_ranges (acre);
353
354     _draw_data (acre);
355
356     _draw_frame_and_ticks (acre);
357 }
358
359 /* Create a new dataset---a collection of (x, y) datapoints. A single
360  * plot can contain multiple datasets, (see acre_add_data). */
361 acre_data_t *
362 acre_data_create (void)
363 {
364     acre_data_t *data;
365
366     data = xmalloc (sizeof (acre_data_t));
367
368     data->name = NULL;
369
370     data->points = NULL;
371     data->points_size = 0;
372     data->num_points = 0;
373
374     return data;
375 }
376
377 /* Destroy an acre dataset. Do not call this function if the dataset
378  * has been added to an acre_t plot with acre_add_data. */
379 void
380 acre_data_destroy (acre_data_t *data)
381 {
382     free (data->points);
383
384     free (data);
385 }
386
387 /* Set the label for this dataset (to appear in the plot's key). */
388 void
389 acre_data_set_name (acre_data_t *data, const char *name)
390 {
391     free (data->name);
392
393     data->name = strdup (name);
394 }
395
396 /* Add a datapoint to the given dataset. */
397 void
398 acre_data_add_point_2d (acre_data_t *data, double x, double y)
399 {
400     if (data->num_points >= data->points_size) {
401         data->points_size *= 2;
402         if (data->points_size == 0)
403             data->points_size = 16;
404         data->points = xrealloc_ab (data->points,
405                                     data->points_size,
406                                     sizeof (acre_data_point_2d_t));
407     }
408
409     data->points[data->num_points].x = x;
410     data->points[data->num_points].y = y;
411     data->num_points++;
412 }