histogram tick drawing - added format generation for optimum tick drawing
authorFriedrich Beckmann <friedrich.beckmann@hs-augsburg.de>
Sun, 31 May 2015 22:20:01 +0000 (00:20 +0200)
committerJohn Darrington <john@darrington.wattle.id.au>
Mon, 1 Jun 2015 04:55:39 +0000 (06:55 +0200)
Fixes bug #45192 which showed up in the gtk3 branch on MacOS.

It replaces the decimal_xxx computations with a routine that computes a
formatting string and a scale factor for a given axis range and number of
bins. The formatting will switch between normal and scientific depending on
the length of the displayed label text. The formatting is such that no
rounded numbers like 3.9999999 when in fact 4 is expected is shown.
Differences between bins remain visible in the tick labels.

In addition the width of the rendered labels is compared with the width
of the bins in the display such that the labels will automatically
switch between horizontal and 45 degree display.

src/math/chart-geometry.c
src/math/chart-geometry.h
src/output/cairo-chart.c
src/output/cairo-chart.h
src/output/charts/plot-hist-cairo.c
tests/automake.mk
tests/math/chart-geometry.at
tests/math/chart-get-ticks-format-test.c [new file with mode: 0644]

index 35380a6929b6c4529e0fe2d5579e03cad9b25f23..26268fd2f32c89032ebb4b5673cc21032b7dfbf5 100644 (file)
 #include "decimal.h"
 #include <stdlib.h>
 
+#include "gl/xalloc.h"
+#include "gl/minmax.h"
+#include "gl/xvasprintf.h"
+
 static const double standard_tick[] = {1, 2, 5, 10};
 
 /* Adjust tick to be a sensible value
@@ -149,3 +153,72 @@ chart_get_scale (double highdbl, double lowdbl,
        }
     }
 }
+
+/*
+ * Compute the optimum format string and the scaling
+ * for the tick drawing on a chart axis
+ * Input:  max:     the maximum value of the range
+ *         min:     the minimum value of the range
+ *         nticks:  the number of tick intervals (bins) on the axis
+ * Return: fs:      format string for printf to print the tick value
+ *         scale:   scaling factor for the tick value
+ * The format string has to be freed after usage.
+ * An example format string and scalefactor:
+ * Non Scientific: "%.3lf", scale=1.00
+ * Scientific:     "%.2lfe3", scale = 0.001
+ * Usage example:
+ *   fs = chart_get_ticks_format(95359943.3,34434.9,8,&scale,&long);
+ *   printf(fs,value*scale);
+ *   free(fs);
+ */
+char *
+chart_get_ticks_format (const double max, const double min,
+                       const unsigned int nticks, double *scale)
+{
+  assert(max > min);
+  double interval = (max - min)/nticks;
+  double logmax = log10(fmax(fabs(max),fabs(min)));
+  double logintv = log10(interval);
+  int logshift = 0;
+  char *format_string = NULL;
+  int nrdecs = 0;
+
+  if (logmax > 0.0 && logintv < 0.0)
+    {
+      nrdecs = MIN(6,(int)(fabs(logintv))+1);
+      logshift = 0;
+      format_string = xasprintf("%%.%dlf",nrdecs);
+    }
+  else if (logmax > 0.0) /*logintv is > 0*/
+    {
+      if (logintv < 3.0)
+       {
+         logshift = 0; /* No scientific format */
+         nrdecs = 0;
+         format_string = xstrdup("%.0lf");
+       }
+      else
+       {
+         logshift = (int)logmax;
+         nrdecs = MIN(6,(int)(logmax-logintv)+1);
+         format_string = xasprintf("%%.%dlfe%d",nrdecs,logshift);
+       }
+    }
+  else /* logmax and logintv are < 0 */
+    {
+      if (logmax > -3.0)
+       {
+         logshift = 0; /* No scientific format */
+         nrdecs = (int)(-logintv) + 1;
+         format_string = xasprintf("%%.%dlf",nrdecs);
+       }
+      else
+       {
+         logshift = (int)logmax-1;
+         nrdecs = MIN(6,(int)(logmax-logintv)+1);
+         format_string = xasprintf("%%.%dlfe%d",nrdecs,logshift);
+       }
+    }
+  *scale = pow(10.0,-(double)logshift);
+  return format_string;
+}
index 08ae1d0ca3e100c23eee6b4a2aea4f9db634a611..67d35bb62fe423afcd248859c1db5e119c00ff39 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2004 Free Software Foundation, Inc.
+   Copyright (C) 2004, 2015 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -24,5 +24,8 @@ void chart_rounded_tick (double tick, struct decimal *);
 void chart_get_scale (double high, double low,
                      struct decimal *lower, struct decimal *interval, int *n_ticks);
 
+char *
+chart_get_ticks_format (const double max, const double min, const unsigned int nticks,
+                       double *scale);
 
 #endif
index 01c5d5c044c45dfb8b1961f813acb392006b9519..2cf3c2d6b8bf886da5c770329746adb9d587560e 100644 (file)
@@ -569,3 +569,27 @@ xrchart_line(cairo_t *cr, const struct xrchart_geometry *geom,
   cairo_line_to (cr, x2, y2);
   cairo_stroke (cr);
 }
+
+void
+xrchart_text_extents (cairo_t *cr, const struct xrchart_geometry *geom,
+                     const char *utf8,
+                     double *width, double *height)
+{
+  PangoFontDescription *desc;
+  PangoLayout *layout;
+  int width_pango;
+  int height_pango;
+
+  desc = pango_font_description_from_string ("sans serif");
+  if (desc == NULL)
+      return;
+  pango_font_description_set_absolute_size (desc, geom->font_size * PANGO_SCALE);
+  layout = pango_cairo_create_layout (cr);
+  pango_layout_set_font_description (layout, desc);
+  pango_layout_set_text (layout, utf8, -1);
+  pango_layout_get_size (layout, &width_pango, &height_pango);
+  *width = (double) width_pango / PANGO_SCALE;
+  *height = (double) height_pango / PANGO_SCALE;
+  g_object_unref (layout);
+  pango_font_description_free (desc);
+}
index 24a2dce51690fd0e1007712f6d7dcd56a4602e39..116a8c990110be01d1e21a900874e18823a703c9 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2009, 2011 Free Software Foundation, Inc.
+   Copyright (C) 2009, 2011, 2015 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -178,5 +178,9 @@ void xrchart_draw_spreadlevel (const struct chart_item *, cairo_t *,
 void xrchart_draw_scatterplot (const struct chart_item *, cairo_t *,
                          struct xrchart_geometry *);
 
+/* Get the width and height of rendered label text */
+void xrchart_text_extents (cairo_t *cr, const struct xrchart_geometry *geom,
+                          const char *utf8,
+                          double *width, double *height);
 
 #endif /* output/cairo-chart.h */
index 006b39225e274f73e7d0dd2241a203ff7143d25c..7e2afee9b022cc621578623c889a474ad7b033dc 100644 (file)
@@ -15,7 +15,7 @@
    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
 #include <config.h>
-#include "math/decimal.h"
+#include "math/chart-geometry.h"
 #include "output/charts/plot-hist.h"
 
 #include <float.h>
@@ -25,6 +25,7 @@
 #include "output/cairo-chart.h"
 
 #include "gl/xvasprintf.h"
+#include "gl/minmax.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
@@ -68,7 +69,8 @@ histogram_write_legend (cairo_t *cr, const struct xrchart_geometry *geom,
 
 static void
 hist_draw_bar (cairo_t *cr, const struct xrchart_geometry *geom,
-               const gsl_histogram *h, int bar, bool label)
+               const gsl_histogram *h, int bar, const char *tick_format_string,
+              const double tickscale, const bool tickoversize)
 {
   double upper;
   double lower;
@@ -102,21 +104,9 @@ hist_draw_bar (cairo_t *cr, const struct xrchart_geometry *geom,
   cairo_restore (cr);
   cairo_stroke (cr);
 
-  if (label)
-    {
-      struct decimal decupper;
-      struct decimal declower;
-      struct decimal middle;
-      decimal_from_double (&declower, lower);
-      decimal_from_double (&decupper, upper);
-      middle = declower;
-      decimal_add (&middle, &decupper);
-      decimal_int_divide (&middle, 2);
-      char *str = decimal_to_string (&middle);
-      draw_tick (cr, geom, SCALE_ABSCISSA, bins > 10,
-                x_pos + width / 2.0, "%s", str);
-      free (str);
-    }
+  draw_tick (cr, geom, SCALE_ABSCISSA, tickoversize,
+            x_pos + width / 2.0, tick_format_string, (upper+lower)/2.0*tickscale);
+
 }
 
 void
@@ -126,6 +116,11 @@ xrchart_draw_histogram (const struct chart_item *chart_item, cairo_t *cr,
   struct histogram_chart *h = to_histogram_chart (chart_item);
   int i;
   int bins;
+  char *tick_format_string;
+  char *test_text;
+  double width, left_width, right_width, unused;
+  double tickscale;
+  bool tickoversize;
 
   xrchart_write_title (cr, geom, _("HISTOGRAM"));
 
@@ -138,14 +133,28 @@ xrchart_draw_histogram (const struct chart_item *chart_item, cairo_t *cr,
       return;
     }
 
-  bins = gsl_histogram_bins (h->gsl_hist);
-
   xrchart_write_yscale (cr, geom, 0, gsl_histogram_max_val (h->gsl_hist));
 
+  /* Draw the ticks and compute if the rendered tick text is wider than the bin */
+  bins = gsl_histogram_bins (h->gsl_hist);
+  tick_format_string = chart_get_ticks_format (gsl_histogram_max (h->gsl_hist),
+                                              gsl_histogram_min (h->gsl_hist),
+                                              bins,
+                                              &tickscale);
+  test_text = xasprintf(tick_format_string, gsl_histogram_max (h->gsl_hist)*tickscale);
+  xrchart_text_extents (cr, geom, test_text, &right_width, &unused);
+  free(test_text);
+  test_text = xasprintf(tick_format_string, gsl_histogram_min (h->gsl_hist)*tickscale);
+  xrchart_text_extents (cr, geom, test_text, &left_width, &unused);
+  free(test_text);
+  width = MAX(left_width, right_width);
+  tickoversize = width > 0.9 *
+    ((double)(geom->axis[SCALE_ABSCISSA].data_max - geom->axis[SCALE_ABSCISSA].data_min))/bins;
   for (i = 0; i < bins; i++)
     {
-      hist_draw_bar (cr, geom, h->gsl_hist, i, true);
+      hist_draw_bar (cr, geom, h->gsl_hist, i, tick_format_string, tickscale, tickoversize);
     }
+  free(tick_format_string);
 
   histogram_write_legend (cr, geom, h->n, h->mean, h->stddev);
 
index 4cb4283e9d69b9bc38e45c2d7c4def754e769f21..be44b342e9267ccc2f491457f960b308f1383a4d 100644 (file)
@@ -226,8 +226,15 @@ tests_math_chart_get_scale_test_LDADD = \
        src/math/libpspp-math.la \
        src/libpspp/liblibpspp.la \
        src/libpspp-core.la \
-       gl/libgl.la 
+       gl/libgl.la
 
+check_PROGRAMS += tests/math/chart-get-ticks-format-test
+tests_math_chart_get_ticks_format_test_SOURCES = tests/math/chart-get-ticks-format-test.c
+tests_math_chart_get_ticks_format_test_LDADD = \
+       src/math/libpspp-math.la \
+       src/libpspp/liblibpspp.la \
+       src/libpspp-core.la \
+       gl/libgl.la
 
 check_PROGRAMS += tests/math/decimal-test
 tests_math_decimal_test_SOURCES = tests/math/decimal-test.c
index 14e15844edad66c81555ab0f36ae2b836e0159a7..e15560b69dbe9d09721bdb0408d948b78429f3e6 100644 (file)
@@ -33,3 +33,30 @@ AT_SETUP([Chart Scale])
 AT_CHECK([../../math/chart-get-scale-test], [0], [ignore])
 
 AT_CLEANUP
+
+
+AT_SETUP([Chart Ticks Format])
+
+AT_CHECK([../../math/chart-get-ticks-format-test], [0], [dnl
+max: 1000, min: 10, nticks: 10, fs: %.0lf, scale: 1, example: 505
+max: 10000, min: 10, nticks: 10, fs: %.0lf, scale: 1, example: 5005
+max: 100000, min: 10, nticks: 10, fs: %.2lfe5, scale: 1e-05, example: 0.50e5
+max: 1e+06, min: 10, nticks: 10, fs: %.2lfe6, scale: 1e-06, example: 0.50e6
+max: 1e+07, min: 10, nticks: 10, fs: %.2lfe7, scale: 1e-07, example: 0.50e7
+max: 1e+08, min: 10, nticks: 10, fs: %.2lfe8, scale: 1e-08, example: 0.50e8
+max: 0.1, min: 0.01, nticks: 10, fs: %.3lf, scale: 1, example: 0.055
+max: 1e-05, min: 1e-06, nticks: 10, fs: %.2lfe-6, scale: 1e+06, example: 5.50e-6
+max: 1.00001e-05, min: 1e-05, nticks: 10, fs: %.6lfe-5, scale: 100000, example: 1.000005e-5
+max: 1e+08, min: 1e+08, nticks: 10, fs: %.0lf, scale: 1, example: 100000005
+max: 100000, min: -500000, nticks: 10, fs: %.1lfe5, scale: 1e-05, example: -2.0e5
+max: 5, min: -5, nticks: 10, fs: %.0lf, scale: 1, example: 0
+max: 5, min: -4.999, nticks: 10, fs: %.1lf, scale: 1, example: 0.0
+max: 5, min: -4.999, nticks: 9, fs: %.0lf, scale: 1, example: 0
+max: 5, min: 0, nticks: 10, fs: %.1lf, scale: 1, example: 2.5
+max: 0, min: -5, nticks: 9, fs: %.1lf, scale: 1, example: -2.5
+max: 1.001e-95, min: 1e-95, nticks: 10, fs: %.5lfe-95, scale: 1e+95, example: 1.00050e-95
+max: 1.001e+98, min: 1e+98, nticks: 10, fs: %.5lfe98, scale: 1e-98, example: 1.00050e98
+max: 1.001e+33, min: 1e-22, nticks: 10, fs: %.2lfe33, scale: 1e-33, example: 0.50e33
+])
+
+AT_CLEANUP
diff --git a/tests/math/chart-get-ticks-format-test.c b/tests/math/chart-get-ticks-format-test.c
new file mode 100644 (file)
index 0000000..a1b155e
--- /dev/null
@@ -0,0 +1,75 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2015 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include "math/chart-geometry.h"
+#include "libpspp/compiler.h"
+
+struct range {
+  double max;
+  double min;
+  int nticks;
+};
+
+struct range tv[] = {
+  {       1000.0,            10.0,     10},
+  {      10000.0,            10.0,     10},
+  {     100000.0,            10.0,     10},
+  {    1000000.0,            10.0,     10},
+  {   10000000.0,            10.0,     10},
+  {  100000000.0,            10.0,     10},
+  {          0.1,            0.01,     10},
+  {      0.00001,        0.000001,     10},
+  { 0.0000100001,         0.00001,     10},
+  {  100000010.0,     100000000.0,     10},
+  {     100000.0,       -500000.0,     10},
+  {          5.0,            -5.0,     10},
+  {          5.0,          -4.999,     10},
+  {          5.0,          -4.999,      9},
+  {          5.0,             0.0,     10},
+  {          0.0,            -5.0,      9},
+  {    1.001E-95,         1.0E-95,     10},
+  {     1.001E98,          1.0E98,     10},
+  {     1.001E33,         1.0E-22,     10},
+  {          0.0,             0.0,     -1}
+};
+
+int
+main (int argc UNUSED, char **argv UNUSED)
+{
+  char *fs;
+  double scale;
+  int i = 0;
+  double max, min;
+  int nticks;
+
+  for(i=0;tv[i].nticks > 0;i++)
+    {
+      max = tv[i].max;
+      min = tv[i].min;
+      nticks = tv[i].nticks;
+      fs = chart_get_ticks_format (max, min, nticks, &scale);
+      printf("max: %lg, min: %lg, nticks: %d, fs: %s, scale: %lg, example: ",
+            max, min, nticks, fs, scale);
+      printf(fs,((max-min)/2.0+min)*scale);
+      printf("\n");
+      free(fs);
+    }
+
+  return 0;
+}