+ _show_layout (cr, y_axis_layout);
+ }
+ cairo_restore (cr);
+
+ cairo_move_to (cr, acre->chart.x,
+ acre->chart.y + acre->chart.height +
+ ACRE_FONT_SIZE + ACRE_PAD);
+ _show_layout (cr, x_axis_layout);
+
+ cairo_restore (cr);
+}
+
+/* For a given axis range, compute a step size (in data space) to
+ * generate a suitable number of ticks (5 or so). */
+static double
+_step_for_range (double range, int *minor_divisions)
+{
+ double step, scale_factor;
+
+ /* We want roughly 5 major ticks for the chart. */
+ step = range / 5;
+
+ /* Normalize the step so we can easily snap it to a desirable
+ * value. */
+ scale_factor = pow (10.0, floor (log10 (step)));
+ step /= scale_factor;
+
+ /* We want increments of 1, 2.5, 5, or 10 (times some power of
+ * 10). The threshold values between these are computed
+ * logarithmically. */
+ if (step < 3.535533905932738) {
+ if (step < 1.58113883008419) {
+ step = 1.0;
+ *minor_divisions = 4;
+ } else {
+ step = 2.5;
+ *minor_divisions = 5;
+ }
+ } else {
+ if (step < 7.071067811865475) {
+ step = 5.0;
+ *minor_divisions = 5;
+ } else {
+ step = 10.0;
+ *minor_divisions = 4;
+ }
+ }
+
+ /* Un-normalize and we now have the data value that we want to
+ * step at. */
+ return step * scale_factor;
+}
+
+/* Given an axis range, we can compute a desired data-space step
+ * amount for the major ticks (see _step_for_range). To get
+ * nice-looking pixel-snapped ticks we want to expand the range
+ * slightly. */
+static void
+_expand_range_for_width (double *axis_min, double *axis_max, int pixel_range)
+{
+ double range, new_range, step, step_minor, pixel_step;
+ int minor_divisions;
+
+ range = *axis_max - *axis_min;
+
+ step = _step_for_range (range, &minor_divisions);
+ step_minor = step / minor_divisions;
+
+ pixel_step = step_minor * (pixel_range / range);
+
+ /* We expand the range by the ratio of the pixel step to the floor
+ * of the pixel_step.
+ */
+ new_range = range * pixel_step / floor (pixel_step);
+
+ /* And spread the increase out on either side of the range. */
+ *axis_min -= (new_range - range) / 2.0;
+ *axis_max += (new_range - range) / 2.0;
+}
+
+/* Setup a transformation in acre->cr such that data values plotted
+ * will appear where they should within the chart.
+ */
+static void
+_set_transform_to_data_space (acre_t *acre)
+{
+ cairo_t *cr = acre->cr;
+
+ cairo_translate (cr,
+ acre->chart.x,
+ acre->chart.y + acre->chart.height);
+ cairo_scale (cr,
+ acre->chart.width / (acre->x_axis.view_max - acre->x_axis.view_min),
+ - acre->chart.height /(acre->y_axis.view_max - acre->y_axis.view_min));
+ cairo_translate (cr, -acre->x_axis.view_min, -acre->y_axis.view_min);
+}
+
+static void
+_find_x_range_given_y_range (acre_t *acre,
+ double *x_min, double *x_max,
+ double y_min, double y_max)
+{
+ acre_data_t *data;
+ unsigned d, i;
+ bool first;
+
+ first = true;
+
+ for (d = 0; d < acre->num_data; d++) {
+ data = acre->data[d];
+ for (i = 0; i < data->num_points; i++) {
+ if (data->points[i].y >= y_min &&
+ data->points[i].y <= y_max)
+ {
+ if (first) {
+ *x_min = data->points[i].x;
+ *x_max = data->points[i].x;
+ first = false;
+ } else {
+ if (data->points[i].x < *x_min)
+ *x_min = data->points[i].x;
+ if (data->points[i].x > *x_max)
+ *x_max = data->points[i].x;
+ }
+ }
+ }
+ }
+
+ /* If nothing is visible, punt to full X data range. */
+ if (first) {
+ *x_min = acre->x_axis.data_min;
+ *x_max = acre->x_axis.data_max;
+ }
+}
+
+static void
+_find_y_range_given_x_range (acre_t *acre,
+ double *y_min, double *y_max,
+ double x_min, double x_max)
+{
+ acre_data_t *data;
+ unsigned d, i;
+ bool first;
+
+ first = true;
+
+ for (d = 0; d < acre->num_data; d++) {
+ data = acre->data[d];
+ for (i = 0; i < data->num_points; i++) {
+ if (data->points[i].x >= x_min &&
+ data->points[i].x <= x_max)
+ {
+ if (first) {
+ *y_min = data->points[i].y;
+ *y_max = data->points[i].y;
+ first = false;
+ } else {
+ if (data->points[i].y < *y_min)
+ *y_min = data->points[i].y;
+ if (data->points[i].y > *y_max)
+ *y_max = data->points[i].y;
+ }
+ }
+ }
+ }
+
+ /* If nothing is visible, punt to full Y data range. */
+ if (first) {
+ *y_min = acre->y_axis.data_min;
+ *y_max = acre->y_axis.data_max;
+ }
+}
+
+static void
+_compute_axis_ranges (acre_t *acre)
+{
+ double x_adjust, y_adjust;
+ cairo_t *cr = acre->cr;
+
+ /* If neither view range is set, set both to data ranges. */
+ if (! acre->x_axis.view_range_set && ! acre->y_axis.view_range_set)
+ {
+ acre->x_axis.view_min = acre->x_axis.data_min;
+ acre->x_axis.view_max = acre->x_axis.data_max;
+
+ acre->y_axis.view_min = acre->y_axis.data_min;
+ acre->y_axis.view_max = acre->y_axis.data_max;
+ } else {
+ /* Otherwise, auto-fit unset range based on data. */
+ if (acre->x_axis.view_range_set && ! acre->y_axis.view_range_set) {
+ _find_y_range_given_x_range (acre,
+ &acre->y_axis.view_min,
+ &acre->y_axis.view_max,
+ acre->x_axis.view_min,
+ acre->x_axis.view_max);
+ }
+ else if (acre->y_axis.view_range_set && ! acre->x_axis.view_range_set) {
+ _find_x_range_given_y_range (acre,
+ &acre->x_axis.view_min,
+ &acre->x_axis.view_max,
+ acre->y_axis.view_min,
+ acre->y_axis.view_max);
+ }
+ }
+
+ /* Next, we want to ensure that the data never collides with the
+ * ticks. So we expand each axis on its minimum side as needed. */
+ cairo_save (cr);
+ {
+ double x, y;
+
+ _set_transform_to_data_space (acre);
+
+ x = ACRE_TICK_MAJOR_SIZE + 2.0;
+ y = ACRE_TICK_MAJOR_SIZE + 2.0;
+ cairo_device_to_user_distance (cr, &x, &y);
+
+ acre->x_axis.view_min -= x;
+ acre->y_axis.view_min += y;