]> git.cworth.org Git - acre/blob - acre.c
8945292b3578ad530c0d653ee407da73d429a718
[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 #define _ISOC99_SOURCE /* for round() */
21 #define _XOPEN_SOURCE 500
22 #define _GNU_SOURCE /* for asprintf() */
23
24 #include "acre.h"
25 #include "xmalloc.h"
26
27 #include <string.h>
28 #include <stdarg.h>
29 #include <stdbool.h>
30 #include <math.h>
31
32 #include <lcms.h>
33
34 typedef struct _acre_data_point_2d {
35     double x;
36     double y;
37 } acre_data_point_2d_t;
38
39 struct _acre_data {
40     /* The name of this data set. */
41     char *name;
42
43     /* Minimum and mximum extents of data. */
44     acre_data_point_2d_t min;
45     acre_data_point_2d_t max;
46
47     /* The data itself. */
48     acre_data_point_2d_t *points;
49     unsigned int points_size;
50     unsigned int num_points;
51
52     /* The names of data points (if any).
53      *
54      * This array is indexed with the same index as the 'points' array
55      * to provide names for points. It is legal for this array to be
56      * NULL or smaller than 'points', (in which case, the
57      * corresponding points simply have no names).
58      */
59     char **names;
60     unsigned int names_size;
61     unsigned int num_names;
62 };
63
64 typedef struct _acre_axis {
65     char *label;
66
67     /* Range of data */
68     double data_min;
69     double data_max;
70
71     /* Range of data to be viewed. */
72     double view_min;
73     double view_max;
74
75     /* Has the view range been set? */
76     bool view_range_set;
77 } acre_axis_t;
78
79 typedef struct _acre_color {
80     double red;
81     double green;
82     double blue;
83 } acre_color_t;
84
85 struct _acre {
86     char *title;
87     acre_axis_t x_axis;
88     acre_axis_t y_axis;
89
90     acre_data_t **data;
91     unsigned int data_size;
92     unsigned int num_data;
93
94     /* Data for drawing. */
95     cairo_t *cr;
96     PangoFontDescription *font;
97     acre_color_t *colors;
98     int colors_size;
99     int num_colors;
100
101     /* Total size including labels. */
102     int width;
103     int height;
104
105     /* Position and size of chart alone. */
106     PangoRectangle chart;
107 };
108
109 static void
110 _find_x_range_given_y_range (acre_t *acre,
111                              double *x_min, double *x_max,
112                              double y_min, double y_max);
113
114 static void
115 _find_y_range_given_x_range (acre_t *acre,
116                              double *y_min, double *y_max,
117                              double x_min, double x_max);
118
119 /* Create a new, empty plot. */
120 acre_t *
121 acre_create (void)
122 {
123     acre_t *acre;
124
125     acre = xmalloc (sizeof (acre_t));
126
127     acre->title = NULL;
128
129     acre->x_axis.label = NULL;
130     acre->x_axis.data_min = 0.0;
131     acre->x_axis.data_max = 0.0;
132     acre->x_axis.view_min = 0.0;
133     acre->x_axis.view_max = 0.0;
134     acre->x_axis.view_range_set = false;
135
136     acre->y_axis.label = NULL;
137     acre->y_axis.data_min = 0.0;
138     acre->y_axis.data_max = 0.0;
139     acre->y_axis.view_min = 0.0;
140     acre->y_axis.view_max = 0.0;
141     acre->y_axis.view_range_set = false;
142
143     acre->data = NULL;
144     acre->data_size = 0;
145     acre->num_data = 0;
146
147     acre->cr = NULL;
148     acre->font = NULL;
149     acre->colors = NULL;
150     acre->num_colors = 0;
151     acre->colors_size = 0;
152
153     acre->width = 0;
154     acre->height = 0;
155
156     acre->chart.x = 0;
157     acre->chart.y = 0;
158     acre->chart.width = 0;
159     acre->chart.height = 0;
160
161     return acre;
162 }
163
164 /* Destroy a plot. */
165 void
166 acre_destroy (acre_t *acre)
167 {
168     unsigned int i;
169
170     free (acre->title);
171     free (acre->x_axis.label);
172     free (acre->y_axis.label);
173
174     for (i = 0; i < acre->num_data; i++)
175         acre_data_destroy (acre->data[i]);
176
177     free (acre->data);
178
179     free (acre->colors);
180
181     free (acre);
182 }
183
184 void
185 acre_set_title (acre_t *acre, const char *title)
186 {
187     free (acre->title);
188
189     acre->title = strdup (title);
190 }
191
192 void
193 acre_set_x_axis_label (acre_t *acre, const char *label)
194 {
195     free (acre->x_axis.label);
196
197     acre->x_axis.label = strdup (label);
198 }
199
200 void
201 acre_set_y_axis_label (acre_t *acre, const char *label)
202 {
203     free (acre->y_axis.label);
204
205     acre->y_axis.label = strdup (label);
206 }
207
208 void
209 acre_get_x_axis_data_range (acre_t *acre, double *x_min, double *x_max)
210 {
211     if (x_min)
212         *x_min = acre->x_axis.data_min;
213
214     if (x_max)
215         *x_max = acre->x_axis.data_max;
216 }
217
218 void
219 acre_get_x_axis_range (acre_t *acre, double *x_min, double *x_max)
220 {
221     /* If an X range has been set, return that. */
222     if (acre->x_axis.view_range_set) {
223         if (x_min)
224             *x_min = acre->x_axis.view_min;
225
226         if (x_max)
227             *x_max = acre->x_axis.view_max;
228
229         return;
230     }
231
232     /* Otherwise, if a Y range has been set, use that to compute X. */
233     if (acre->y_axis.view_range_set) {
234         _find_x_range_given_y_range (acre, x_min, x_max,
235                                      acre->y_axis.view_min,
236                                      acre->y_axis.view_max);
237
238         return;
239     }
240
241     /* Neither view range set. Return full, data-based X range. */
242     acre_get_x_axis_data_range (acre, x_min, x_max);
243 }
244
245 void
246 acre_set_x_axis_range (acre_t *acre, double x_min, double x_max)
247 {
248     acre->x_axis.view_min = x_min;
249     acre->x_axis.view_max = x_max;
250
251     acre->x_axis.view_range_set = true;
252 }
253
254 void
255 acre_set_x_axis_range_auto (acre_t *acre)
256 {
257     acre->x_axis.view_range_set = false;
258 }
259
260 void
261 acre_get_y_axis_data_range (acre_t *acre, double *y_min, double *y_max)
262 {
263     if (y_min)
264         *y_min = acre->y_axis.data_min;
265
266     if (y_max)
267         *y_max = acre->y_axis.data_max;
268 }
269
270 void
271 acre_get_y_axis_range (acre_t *acre, double *y_min, double *y_max)
272 {
273     /* If a Y range has been set, return that. */
274     if (acre->y_axis.view_range_set) {
275         if (y_min)
276             *y_min = acre->y_axis.view_min;
277
278         if (y_max)
279             *y_max = acre->y_axis.view_max;
280
281         return;
282     }
283
284     /* Otherwise, if an X range has been set, use that to compute Y. */
285     if (acre->x_axis.view_range_set) {
286         _find_y_range_given_x_range (acre, y_min, y_max,
287                                      acre->x_axis.view_min,
288                                      acre->x_axis.view_max);
289
290         return;
291     }
292
293     /* Neither view range set. Return full data-based Y range. */
294     acre_get_y_axis_data_range (acre, y_min, y_max);
295 }
296
297 void
298 acre_set_y_axis_range (acre_t *acre, double y_min, double y_max)
299 {
300     acre->y_axis.view_min = y_min;
301     acre->y_axis.view_max = y_max;
302
303     acre->y_axis.view_range_set = true;
304 }
305
306 void
307 acre_set_y_axis_range_auto (acre_t *acre)
308 {
309     acre->y_axis.view_range_set = false;
310 }
311
312 /* Add a dataset to the plot. The plot assumes ownership of the
313  * dataset so it is not necessary to call acre_data_destroy on it. */
314 void
315 acre_add_data (acre_t *acre, acre_data_t *data)
316 {
317     if (acre->num_data >= acre->data_size) {
318         acre->data_size *= 2;
319         if (acre->data_size == 0)
320             acre->data_size = 1;
321         acre->data = xrealloc_ab (acre->data,
322                                   acre->data_size,
323                                   sizeof (acre_data_t *));
324     }
325
326     acre->data[acre->num_data] = data;
327
328     if (acre->num_data == 0) {
329         acre->x_axis.data_min = data->min.x;
330         acre->y_axis.data_min = data->min.y;
331
332         acre->x_axis.data_max = data->max.x;
333         acre->y_axis.data_max = data->max.y;
334     } else {
335         if (data->min.x < acre->x_axis.data_min)
336             acre->x_axis.data_min = data->min.x;
337         if (data->min.y < acre->y_axis.data_min)
338             acre->y_axis.data_min = data->min.y;
339
340         if (data->max.x > acre->x_axis.data_max)
341             acre->x_axis.data_max = data->max.x;
342         if (data->max.y > acre->y_axis.data_max)
343             acre->y_axis.data_max = data->max.y;
344     }
345
346     acre->num_data++;
347 }
348
349 #define ACRE_FONT_FAMILY "sans"
350 #define ACRE_FONT_SIZE 12
351 #define ACRE_TITLE_FONT_SIZE 20
352 #define ACRE_PAD (ACRE_FONT_SIZE)
353 #define ACRE_TICK_MAJOR_SIZE 6
354 #define ACRE_TICK_MINOR_SIZE 3
355 #define ACRE_X_TICK_VALUE_PAD 2
356 #define ACRE_Y_TICK_VALUE_PAD 4
357 #define ACRE_LEGEND_PAD 4
358 #define ACRE_LEGEND_LINE_SIZE 10
359
360 static PangoLayout *
361 _create_layout (acre_t *acre, const char *text)
362 {
363     PangoLayout *layout;
364
365     if (text == NULL)
366             text = "";
367
368     layout = pango_cairo_create_layout (acre->cr);
369     pango_layout_set_font_description (layout, acre->font);
370     pango_layout_set_text (layout, text, -1);
371     pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER);
372
373     return layout;
374 }
375
376 #define PRINTF_FORMAT(fmt_index, va_index) __attribute__ ((__format__(__printf__, fmt_index, va_index)))
377
378 static PangoLayout *
379 _create_layout_vprintf (acre_t *acre, const char *fmt, va_list ap)
380 {
381     PangoLayout *layout;
382     char *text;
383
384     vasprintf (&text, fmt, ap);
385
386     layout = _create_layout (acre, text);
387
388     free (text);
389
390     return layout;
391 }
392
393 static PangoLayout *
394 _create_layout_printf (acre_t *acre, const char *fmt, ...)
395     PRINTF_FORMAT (2, 3);
396
397 static PangoLayout *
398 _create_layout_printf (acre_t *acre, const char *fmt, ...)
399 {
400     va_list ap;
401     PangoLayout *layout;
402
403     va_start (ap, fmt);
404
405     layout = _create_layout_vprintf (acre, fmt, ap);
406
407     va_end (ap);
408
409     return layout;
410 }
411
412 static void
413 _destroy_layout (PangoLayout *layout)
414 {
415     g_object_unref (layout);
416 }
417
418 static void
419 _show_layout (cairo_t *cr, PangoLayout *layout)
420 {
421     pango_cairo_show_layout (cr, layout);
422
423     _destroy_layout (layout);
424 }
425
426 static void
427 _draw_title_and_labels (acre_t *acre)
428 {
429     cairo_t *cr = acre->cr;
430     PangoFontDescription *title_font;
431     PangoLayout *title_layout, *x_axis_layout, *y_axis_layout;
432     PangoLayout *min_y, *max_y;
433     int min_y_width, max_y_width, y_axis_value_width;
434     int title_width, title_height;
435     int x_axis_width, x_axis_height;
436     int y_axis_width, y_axis_height;
437     PangoRectangle new_chart;
438
439     cairo_save (cr);
440
441     acre->font = pango_font_description_new ();
442     pango_font_description_set_family (acre->font, ACRE_FONT_FAMILY);
443     pango_font_description_set_absolute_size (acre->font,
444                                               ACRE_FONT_SIZE * PANGO_SCALE);
445
446     title_font = pango_font_description_new ();
447     pango_font_description_set_family (title_font, ACRE_FONT_FAMILY);
448     pango_font_description_set_absolute_size (title_font,
449                                               ACRE_TITLE_FONT_SIZE * PANGO_SCALE);
450
451     title_layout = _create_layout (acre, acre->title);
452     pango_layout_set_font_description (title_layout, title_font);
453     pango_font_description_free (title_font);
454
455     x_axis_layout = _create_layout (acre, acre->x_axis.label);
456     y_axis_layout = _create_layout (acre, acre->y_axis.label);
457
458     min_y = _create_layout_printf (acre, "%g",
459                                    round (acre->y_axis.view_min));
460     max_y = _create_layout_printf (acre, "%g",
461                                    round (acre->y_axis.view_max));
462
463     pango_layout_get_pixel_size (min_y, &min_y_width, NULL);
464     pango_layout_get_pixel_size (max_y, &max_y_width, NULL);
465     y_axis_value_width = MAX (min_y_width, max_y_width);
466
467     _destroy_layout (min_y);
468     _destroy_layout (max_y);
469
470     /* Iterate with the layout of the title and axis labels until they
471      * are stable, (this requires iteration since we don't know what
472      * to set their widths to in advance due to the wrapping of the
473      * other elements). */
474     while (1) {
475         pango_layout_set_width (title_layout, acre->chart.width * PANGO_SCALE);
476         pango_layout_set_width (x_axis_layout, acre->chart.width * PANGO_SCALE);
477         pango_layout_set_width (y_axis_layout, acre->chart.height * PANGO_SCALE);
478
479         pango_layout_get_pixel_size (title_layout, &title_width, &title_height);
480         pango_layout_get_pixel_size (x_axis_layout, &x_axis_width, &x_axis_height);
481         pango_layout_get_pixel_size (y_axis_layout, &y_axis_width, &y_axis_height);
482
483         new_chart.x = ACRE_PAD + y_axis_height +
484             ACRE_PAD + y_axis_value_width + ACRE_Y_TICK_VALUE_PAD;
485         new_chart.width = acre->width - acre->chart.x - ACRE_PAD;
486
487         new_chart.y = ACRE_PAD + title_height + ACRE_PAD;
488         new_chart.height = acre->height - acre->chart.y - 
489             (ACRE_X_TICK_VALUE_PAD + ACRE_FONT_SIZE +
490              ACRE_PAD + x_axis_height + ACRE_PAD);
491
492         if (new_chart.x == acre->chart.x &&
493             new_chart.y == acre->chart.y &&
494             new_chart.width == acre->chart.width &&
495             new_chart.height == acre->chart.height)
496         {
497             break;
498         }
499
500         acre->chart.x = new_chart.x;
501         acre->chart.y = new_chart.y;
502         acre->chart.width = new_chart.width;
503         acre->chart.height = new_chart.height;
504     }
505
506     cairo_set_source_rgb (cr, 0, 0, 0);
507
508     cairo_move_to (cr, acre->chart.x, ACRE_PAD);
509     _show_layout (cr, title_layout);
510
511     cairo_save (cr);
512     {
513         cairo_translate (cr, ACRE_PAD, acre->chart.y + acre->chart.height);
514         cairo_rotate (cr, - M_PI / 2.0);
515         cairo_move_to (cr, 0, 0);
516         _show_layout (cr, y_axis_layout);
517     }
518     cairo_restore (cr);
519
520     cairo_move_to (cr, acre->chart.x,
521                    acre->chart.y + acre->chart.height +
522                    ACRE_FONT_SIZE + ACRE_PAD);
523     _show_layout (cr, x_axis_layout);
524
525     cairo_restore (cr);
526 }
527
528 /* For a given axis range, compute a step size (in data space) to
529  * generate a suitable number of ticks (5 or so). */
530 static double
531 _step_for_range (double range, int *minor_divisions)
532 {
533     double step, scale_factor;
534
535     /* We want roughly 5 major ticks for the chart. */
536     step = range / 5;
537
538     /* Normalize the step so we can easily snap it to a desirable
539      * value. */
540     scale_factor = pow (10.0, floor (log10 (step)));
541     step /= scale_factor;
542
543     /* We want increments of 1, 2.5, 5, or 10 (times some power of
544      * 10). The threshold values between these are computed
545      * logarithmically. */
546     if (step < 3.535533905932738) {
547         if (step < 1.58113883008419) {
548             step = 1.0;
549             *minor_divisions = 4;
550         } else {
551             step = 2.5;
552             *minor_divisions = 5;
553         }
554     } else {
555         if (step < 7.071067811865475) {
556             step = 5.0;
557             *minor_divisions = 5;
558         } else {
559             step = 10.0;
560             *minor_divisions = 4;
561         }
562     }
563
564     /* Un-normalize and we now have the data value that we want to
565      * step at. */
566     return step * scale_factor;
567 }
568
569 /* Given an axis range, we can compute a desired data-space step
570  * amount for the major ticks (see _step_for_range). To get
571  * nice-looking pixel-snapped ticks we want to expand the range
572  * slightly. */
573 static void
574 _expand_range_for_width (double *axis_min, double *axis_max, int pixel_range)
575 {
576     double range, new_range, step, step_minor, pixel_step;
577     int minor_divisions;
578
579     range = *axis_max - *axis_min;
580
581     step = _step_for_range (range, &minor_divisions);
582     step_minor = step / minor_divisions;
583
584     pixel_step = step_minor * (pixel_range / range);
585
586     /* We expand the range by the ratio of the pixel step to the floor
587      * of the pixel_step.
588      */
589     new_range = range * pixel_step / floor (pixel_step);
590
591     /* And spread the increase out on either side of the range. */
592     *axis_min -= (new_range - range) / 2.0;
593     *axis_max += (new_range - range) / 2.0;
594 }
595
596 /* Setup a transformation in acre->cr such that data values plotted
597  * will appear where they should within the chart.
598  */
599 static void
600 _set_transform_to_data_space (acre_t *acre)
601 {
602     cairo_t *cr = acre->cr;
603
604     cairo_translate (cr,
605                      acre->chart.x,
606                      acre->chart.y + acre->chart.height);
607     cairo_scale (cr,
608                  acre->chart.width / (acre->x_axis.view_max - acre->x_axis.view_min),
609                  - acre->chart.height /(acre->y_axis.view_max - acre->y_axis.view_min));
610     cairo_translate (cr, -acre->x_axis.view_min, -acre->y_axis.view_min);
611 }
612
613 static void
614 _find_x_range_given_y_range (acre_t *acre,
615                              double *x_min, double *x_max,
616                              double y_min, double y_max)
617 {
618     acre_data_t *data;
619     unsigned d, i;
620     bool first;
621
622     first = true;
623
624     for (d = 0; d < acre->num_data; d++) {
625         data = acre->data[d];
626         for (i = 0; i < data->num_points; i++) {
627             if (data->points[i].y >= y_min &&
628                 data->points[i].y <= y_max)
629             {
630                 if (first) {
631                     *x_min = data->points[i].x;
632                     *x_max = data->points[i].x;
633                     first = false;
634                 } else {
635                     if (data->points[i].x < *x_min)
636                         *x_min = data->points[i].x;
637                     if (data->points[i].x > *x_max)
638                         *x_max = data->points[i].x;
639                 }
640             }
641         }
642     }
643
644     /* If nothing is visible, punt to full X data range. */
645     if (first) {
646         *x_min = acre->x_axis.data_min;
647         *x_max = acre->x_axis.data_max;
648     }
649 }
650
651 static void
652 _find_y_range_given_x_range (acre_t *acre,
653                              double *y_min, double *y_max,
654                              double x_min, double x_max)
655 {
656     acre_data_t *data;
657     unsigned d, i;
658     bool first;
659
660     first = true;
661
662     for (d = 0; d < acre->num_data; d++) {
663         data = acre->data[d];
664         for (i = 0; i < data->num_points; i++) {
665             if (data->points[i].x >= x_min &&
666                 data->points[i].x <= x_max)
667             {
668                 if (first) {
669                     *y_min = data->points[i].y;
670                     *y_max = data->points[i].y;
671                     first = false;
672                 } else {
673                     if (data->points[i].y < *y_min)
674                         *y_min = data->points[i].y;
675                     if (data->points[i].y > *y_max)
676                         *y_max = data->points[i].y;
677                 }
678             }
679         }
680     }
681
682     /* If nothing is visible, punt to full Y data range. */
683     if (first) {
684         *y_min = acre->y_axis.data_min;
685         *y_max = acre->y_axis.data_max;
686     }
687 }
688
689 static void
690 _compute_axis_ranges (acre_t *acre)
691 {
692     double x_adjust, y_adjust;
693     cairo_t *cr = acre->cr;
694
695     /* If neither view range is set, set both to data ranges. */
696     if (! acre->x_axis.view_range_set && ! acre->y_axis.view_range_set)
697     {
698         acre->x_axis.view_min = acre->x_axis.data_min;
699         acre->x_axis.view_max = acre->x_axis.data_max;
700
701         acre->y_axis.view_min = acre->y_axis.data_min;
702         acre->y_axis.view_max = acre->y_axis.data_max;
703     } else {
704         /* Otherwise, auto-fit unset range based on data. */
705         if (acre->x_axis.view_range_set && ! acre->y_axis.view_range_set) {
706             _find_y_range_given_x_range (acre,
707                                          &acre->y_axis.view_min,
708                                          &acre->y_axis.view_max,
709                                          acre->x_axis.view_min,
710                                          acre->x_axis.view_max);
711         }
712         else if (acre->y_axis.view_range_set && ! acre->x_axis.view_range_set) {
713             _find_x_range_given_y_range (acre,
714                                          &acre->x_axis.view_min,
715                                          &acre->x_axis.view_max,
716                                          acre->y_axis.view_min,
717                                          acre->y_axis.view_max);
718         }
719     }
720
721     /* Then, increase the axis ranges just enough so that the step
722      * sizes for the ticks will be integers.
723      */
724     _expand_range_for_width (&acre->x_axis.view_min,
725                              &acre->x_axis.view_max,
726                              acre->chart.width);
727
728     _expand_range_for_width (&acre->y_axis.view_min,
729                              &acre->y_axis.view_max,
730                              acre->chart.height);
731
732     /* Finally, we also translate the axis ranges slightly so that the
733      * ticks land on half-integer device-pixel positions.
734      */
735     cairo_save (cr);
736     {
737         _set_transform_to_data_space (acre);
738
739         x_adjust = 0.0;
740         y_adjust = 0.0;
741         cairo_user_to_device (cr, &x_adjust, &y_adjust);
742         x_adjust = (round (x_adjust + 0.5) - 0.5) - x_adjust;
743         y_adjust = (round (y_adjust + 0.5) - 0.5) - y_adjust;
744         cairo_device_to_user_distance (cr, &x_adjust, &y_adjust);
745
746         acre->x_axis.view_min -= x_adjust;
747         acre->x_axis.view_max -= x_adjust;
748
749         acre->y_axis.view_min -= y_adjust;
750         acre->y_axis.view_max -= y_adjust;
751     }
752     cairo_restore (cr);
753 }
754
755 static void
756 _choose_colors (acre_t *acre)
757 {
758     cmsHPROFILE lab_profile, srgb_profile;
759     cmsHTRANSFORM lab_to_srgb;
760     int i;
761     double theta, radius, srgb[3];
762     cmsCIELab lab;
763
764     lab_profile = cmsCreateLabProfile (NULL); /* D50 */
765     srgb_profile = cmsCreate_sRGBProfile ();
766
767     lab_to_srgb = cmsCreateTransform (lab_profile, TYPE_Lab_DBL,
768                                       srgb_profile, TYPE_RGB_DBL,
769                                       INTENT_PERCEPTUAL, 0);
770
771     acre->num_colors = acre->num_data;
772
773     if (acre->num_colors > acre->colors_size) {
774         acre->colors_size = acre->num_colors;
775         acre->colors = xrealloc (acre->colors,
776                                  acre->colors_size * sizeof (acre_color_t));
777     }
778
779     lab.L = 36;
780     radius = 130;
781     for (i = 0; i < acre->num_colors; i++) {
782         theta = 0.713 + 2 * M_PI * (double) i / acre->num_colors;
783         lab.a = radius * cos (theta);
784         lab.b = radius * sin (theta);
785
786         cmsDoTransform (lab_to_srgb, &lab, srgb, 1);
787
788         acre->colors[i].red = srgb[0];
789         acre->colors[i].green = srgb[1];
790         acre->colors[i].blue = srgb[2];
791     }
792
793     cmsDeleteTransform (lab_to_srgb);
794     cmsCloseProfile (lab_profile);
795     cmsCloseProfile (srgb_profile);
796 }
797
798 static void
799 _draw_data (acre_t *acre)
800 {
801     cairo_t *cr = acre->cr;
802     unsigned int d, i;
803     acre_data_t *data;
804
805     cairo_save (cr);
806
807     cairo_rectangle (cr,
808                      acre->chart.x, acre->chart.y,
809                      acre->chart.width, acre->chart.height);
810     cairo_clip (cr);
811
812     cairo_set_source_rgb (cr, 0, 0, 0);
813
814     _set_transform_to_data_space (acre);
815
816     for (d = 0; d < acre->num_data; d++) {
817         int color = d % acre->num_colors;
818         cairo_set_source_rgb (cr,
819                               acre->colors[color].red,
820                               acre->colors[color].green,
821                               acre->colors[color].blue);
822         data = acre->data[d];
823
824         cairo_new_path (cr);
825
826         for (i = 0; i < data->num_points; i++) {
827             cairo_line_to (cr,
828                            data->points[i].x,
829                            data->points[i].y);
830         }
831         cairo_save (cr);
832         {
833             cairo_identity_matrix (cr);
834             cairo_set_line_width (cr, 1.0);
835             cairo_stroke (cr);
836         }
837         cairo_restore (cr);
838     }
839
840     cairo_restore (cr);
841 }
842
843 typedef enum _ticks { ACRE_TICKS_X, ACRE_TICKS_Y } acre_ticks_t;
844
845 static void
846 _draw_ticks (acre_t *acre,
847              double axis_min, double axis_max,
848              acre_ticks_t ticks)
849 {
850     cairo_t *cr = acre->cr;
851     double t, step, sub_step;
852     int minor_divisions;
853
854     cairo_save (cr);
855
856     _set_transform_to_data_space (acre);
857
858     step = _step_for_range (axis_max - axis_min, &minor_divisions);
859     sub_step = step / minor_divisions;
860
861     for (t = (floor (axis_min / sub_step) + 1) * sub_step;
862          t <= axis_max;
863          t += sub_step)
864     {
865         int tick_size;
866         if (fabs((t / step) - round (t / step)) < 0.5 * (sub_step / step))
867             tick_size = ACRE_TICK_MAJOR_SIZE;
868         else
869             tick_size = ACRE_TICK_MINOR_SIZE;
870
871         /* tick */
872         cairo_save (cr);
873         {
874             if (ticks == ACRE_TICKS_X)
875                 cairo_move_to (cr, t, acre->y_axis.view_min);
876             else
877                 cairo_move_to (cr, acre->x_axis.view_min, t);
878
879             cairo_identity_matrix (cr);
880
881             if (ticks == ACRE_TICKS_X) {
882                 cairo_rel_line_to (cr, 0, 0.5);
883                 cairo_rel_line_to (cr, 0, -tick_size - 0.5);
884             } else {
885                 cairo_rel_line_to (cr, -0.5, 0);
886                 cairo_rel_line_to (cr, tick_size + 0.5, 0);
887             }
888
889             cairo_set_line_width (cr, 1.0);
890             cairo_stroke (cr);
891         }
892         cairo_restore (cr);
893
894         /* label */
895         if (tick_size == ACRE_TICK_MAJOR_SIZE)
896         {
897             PangoLayout *layout;
898             int width, height;
899
900             cairo_save (cr);
901
902             layout = _create_layout_printf (acre, "%g", t);
903
904             if (ticks == ACRE_TICKS_X)
905                 cairo_move_to (cr, t, acre->y_axis.view_min);
906             else
907                 cairo_move_to (cr, acre->x_axis.view_min, t);
908
909             cairo_identity_matrix (cr);
910             pango_layout_get_pixel_size (layout, &width, &height);
911
912             if (ticks == ACRE_TICKS_X)
913                 cairo_rel_move_to (cr, -width / 2, ACRE_X_TICK_VALUE_PAD);
914             else
915                 cairo_rel_move_to (cr, -width - ACRE_Y_TICK_VALUE_PAD,
916                                    -height/2);
917
918             _show_layout (cr, layout);
919
920             cairo_restore (cr);
921         }
922     }
923
924     cairo_restore (cr);
925 }
926
927 static void
928 _draw_legend (acre_t *acre)
929 {
930     PangoLayout *layout;
931     int label_width, max_label_width = 0;
932     int width, height;
933     unsigned int i;
934     cairo_t *cr = acre->cr;
935
936     cairo_save (cr);
937
938     for (i = 0; i < acre->num_data; i++) {
939         layout = _create_layout (acre, acre->data[i]->name);
940         pango_layout_get_pixel_size (layout, &label_width, NULL);
941         _destroy_layout (layout);
942         if (label_width > max_label_width)
943             max_label_width = label_width;
944     }
945
946     width = ACRE_LEGEND_PAD + ACRE_LEGEND_LINE_SIZE + ACRE_LEGEND_PAD + 
947         max_label_width + ACRE_LEGEND_PAD;
948     height = ACRE_LEGEND_PAD +
949         acre->num_data * (ACRE_FONT_SIZE + ACRE_LEGEND_PAD);
950
951     cairo_translate (cr, acre->chart.x, acre->chart.y);
952
953     cairo_translate (cr,
954                      acre->chart.width - ACRE_LEGEND_PAD - width,
955                      ACRE_LEGEND_PAD);
956
957     cairo_rectangle (cr, -0.5, -0.5, width + 1.0, height + 1.0);
958     cairo_set_source_rgb (cr, 0, 0, 0);
959     cairo_set_line_width (cr, 1.0);
960     cairo_stroke (cr);
961
962     cairo_translate (cr, ACRE_LEGEND_PAD, ACRE_LEGEND_PAD);
963
964     for (i = 0; i < acre->num_data; i++) {
965         cairo_rectangle (cr,
966                          0, ACRE_LEGEND_LINE_SIZE / 2,
967                          ACRE_LEGEND_LINE_SIZE, ACRE_LEGEND_LINE_SIZE / 2);
968         cairo_set_source_rgb (cr,
969                               acre->colors[i % acre->num_colors].red,
970                               acre->colors[i % acre->num_colors].green,
971                               acre->colors[i % acre->num_colors].blue);
972         cairo_fill (cr);
973
974         layout = _create_layout (acre, acre->data[i]->name);
975         cairo_move_to (cr, ACRE_LEGEND_LINE_SIZE + ACRE_LEGEND_PAD, 0);
976         cairo_set_source_rgb (cr, 0, 0, 0);
977         _show_layout (cr, layout);
978
979         cairo_translate (cr, 0, ACRE_LEGEND_PAD + ACRE_FONT_SIZE);
980     }
981
982     cairo_restore (cr);
983 }
984
985 static void
986 _draw_frame_and_ticks (acre_t *acre)
987 {
988     cairo_t *cr = acre->cr;
989
990     cairo_save (cr);
991
992     cairo_set_source_rgb (cr, 0, 0, 0); /* black */
993
994     /* ticks */
995     _draw_ticks (acre, acre->x_axis.view_min, acre->x_axis.view_max, ACRE_TICKS_X);
996     _draw_ticks (acre, acre->y_axis.view_min, acre->y_axis.view_max, ACRE_TICKS_Y);
997
998     /* frame */
999     cairo_rectangle (cr,
1000                      acre->chart.x - 0.5, acre->chart.y - 0.5,
1001                      acre->chart.width + 1.0, acre->chart.height + 1.0);
1002     cairo_set_line_width (cr, 1.0);
1003     cairo_stroke (cr);
1004
1005     cairo_restore (cr);
1006 }
1007
1008 /* Draw the plot to the given cairo context within a user-space
1009  * rectangle from (0, 0) to (width, height). This size includes all
1010  * space for extra-plot elements (such as the title, the axis labels,
1011  * etc.)
1012  */
1013 void
1014 acre_draw (acre_t *acre, cairo_t *cr, int width, int height)
1015 {
1016     acre->cr = cr;
1017
1018     acre->width = width;
1019     acre->height = height;
1020
1021     acre->chart.width = width;
1022     acre->chart.height = height;
1023
1024     cairo_save (cr);
1025
1026     cairo_set_source_rgb (cr, 1, 1, 1);
1027
1028     _choose_colors (acre);
1029
1030     /* We compute the axis ranges before doing label layout so that we
1031      * can account for the width of the y-axis value labels. */
1032     _compute_axis_ranges (acre);
1033
1034     _draw_title_and_labels (acre);
1035
1036     /* And we recompute the axis ranges now that the title and axis
1037      * label space is all measured and accounted for. */
1038     _compute_axis_ranges (acre);
1039
1040     _draw_data (acre);
1041
1042     if (acre->num_data > 1)
1043         _draw_legend (acre);
1044
1045     _draw_frame_and_ticks (acre);
1046 }
1047
1048 /* Create a new dataset---a collection of (x, y) datapoints. A single
1049  * plot can contain multiple datasets, (see acre_add_data). */
1050 acre_data_t *
1051 acre_data_create (void)
1052 {
1053     acre_data_t *data;
1054
1055     data = xmalloc (sizeof (acre_data_t));
1056
1057     data->name = NULL;
1058
1059     data->points = NULL;
1060     data->points_size = 0;
1061     data->num_points = 0;
1062
1063     return data;
1064 }
1065
1066 /* Destroy an acre dataset. Do not call this function if the dataset
1067  * has been added to an acre_t plot with acre_add_data. */
1068 void
1069 acre_data_destroy (acre_data_t *data)
1070 {
1071     unsigned i;
1072
1073     for (i = 0; i < data->num_names; i++) {
1074         if (data->names[i])
1075             free (data->names[i]);
1076     }
1077     free (data->names);
1078
1079     free (data->name);
1080
1081     free (data->points);
1082
1083     free (data);
1084 }
1085
1086 /* Set the label for this dataset (to appear in the plot's key). */
1087 void
1088 acre_data_set_name (acre_data_t *data, const char *name)
1089 {
1090     free (data->name);
1091
1092     data->name = strdup (name);
1093 }
1094
1095 /* Add a datapoint to the given dataset. */
1096 void
1097 acre_data_add_point_2d (acre_data_t *data, double x, double y)
1098 {
1099     if (data->num_points >= data->points_size) {
1100         data->points_size *= 2;
1101         if (data->points_size == 0)
1102             data->points_size = 16;
1103         data->points = xrealloc_ab (data->points,
1104                                     data->points_size,
1105                                     sizeof (acre_data_point_2d_t));
1106     }
1107
1108     data->points[data->num_points].x = x;
1109     data->points[data->num_points].y = y;
1110
1111     if (data->num_points == 0) {
1112         data->min.x = x;
1113         data->min.y = y;
1114
1115         data->max.x = x;
1116         data->max.y = y;
1117     } else {
1118         if (x < data->min.x)
1119             data->min.x = x;
1120         if (y < data->min.y)
1121             data->min.y = y;
1122
1123         if (x > data->max.x)
1124             data->max.x = x;
1125         if (y > data->max.y)
1126             data->max.y = y;
1127     }
1128
1129     data->num_points++;
1130 }
1131
1132 /* Add a datapoint with a name to the given dataset. */
1133 void
1134 acre_data_add_point_2d_named (acre_data_t *data, double x, double y, const char *name)
1135 {
1136     unsigned i;
1137
1138     acre_data_add_point_2d (data, x, y);
1139
1140     if (data->names_size < data->points_size) {
1141         data->names_size = data->points_size;
1142         data->names = xrealloc_ab (data->names,
1143                                    data->names_size,
1144                                    sizeof (char *));
1145     }
1146
1147     /* Initialize any newly-created holes in the array to NULL. */
1148     for (i = data->num_names; i < data->num_points - 1; i++)
1149         data->names[i] = NULL;
1150
1151     data->num_names = data->num_points;
1152
1153     data->names[data->num_names - 1] = xstrdup (name);
1154 }