From f89d8a893f83ad0cb95819ca60e6f4a718f7d988 Mon Sep 17 00:00:00 2001 From: Friedrich Beckmann Date: Mon, 1 Jun 2015 00:20:01 +0200 Subject: [PATCH] histogram tick drawing - added format generation for optimum tick drawing 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 | 73 +++++++++++++++++++++++ src/math/chart-geometry.h | 5 +- src/output/cairo-chart.c | 24 ++++++++ src/output/cairo-chart.h | 6 +- src/output/charts/plot-hist-cairo.c | 49 +++++++++------- tests/automake.mk | 9 ++- tests/math/chart-geometry.at | 27 +++++++++ tests/math/chart-get-ticks-format-test.c | 75 ++++++++++++++++++++++++ 8 files changed, 245 insertions(+), 23 deletions(-) create mode 100644 tests/math/chart-get-ticks-format-test.c diff --git a/src/math/chart-geometry.c b/src/math/chart-geometry.c index 35380a6929..26268fd2f3 100644 --- a/src/math/chart-geometry.c +++ b/src/math/chart-geometry.c @@ -23,6 +23,10 @@ #include "decimal.h" #include +#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; +} diff --git a/src/math/chart-geometry.h b/src/math/chart-geometry.h index 08ae1d0ca3..67d35bb62f 100644 --- a/src/math/chart-geometry.h +++ b/src/math/chart-geometry.h @@ -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 diff --git a/src/output/cairo-chart.c b/src/output/cairo-chart.c index 01c5d5c044..2cf3c2d6b8 100644 --- a/src/output/cairo-chart.c +++ b/src/output/cairo-chart.c @@ -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); +} diff --git a/src/output/cairo-chart.h b/src/output/cairo-chart.h index 24a2dce516..116a8c9901 100644 --- a/src/output/cairo-chart.h +++ b/src/output/cairo-chart.h @@ -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 */ diff --git a/src/output/charts/plot-hist-cairo.c b/src/output/charts/plot-hist-cairo.c index 006b39225e..7e2afee9b0 100644 --- a/src/output/charts/plot-hist-cairo.c +++ b/src/output/charts/plot-hist-cairo.c @@ -15,7 +15,7 @@ along with this program. If not, see . */ #include -#include "math/decimal.h" +#include "math/chart-geometry.h" #include "output/charts/plot-hist.h" #include @@ -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); diff --git a/tests/automake.mk b/tests/automake.mk index 4cb4283e9d..be44b342e9 100644 --- a/tests/automake.mk +++ b/tests/automake.mk @@ -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 diff --git a/tests/math/chart-geometry.at b/tests/math/chart-geometry.at index 14e15844ed..e15560b69d 100644 --- a/tests/math/chart-geometry.at +++ b/tests/math/chart-geometry.at @@ -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 index 0000000000..a1b155e329 --- /dev/null +++ b/tests/math/chart-get-ticks-format-test.c @@ -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 . */ + +#include +#include +#include +#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; +} -- 2.30.2