From e26f9319cccab6cbfd1debe1b148ee7fce57417c Mon Sep 17 00:00:00 2001 From: Tom Keffer Date: Tue, 17 Nov 2009 02:21:18 +0000 Subject: [PATCH] Allow user to specify ymin, ymax, and min_interval for y-axis. Modified configuration file to take advantage of this capability for rain, wind, and pond temperatures. Updated readme.htm to reflect. --- CHANGES.txt | 5 ++- TODO.txt | 3 +- readme.htm | 82 ++++++++++++++++++++++------------------ weeplot/genplot.py | 43 +++++++++++---------- weeplot/utilities.py | 90 ++++++++++++++++++++++++++++++-------------- weeutil/weeutil.py | 12 ++++-- weewx.conf | 29 ++++++++++++-- weewx/__init__.py | 2 +- weewx/genimages.py | 8 +++- 9 files changed, 176 insertions(+), 98 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c7cce058..165e5c92 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,7 +3,10 @@ CHANGE HISTORY 1.2.0 11/16/09 -Improved axis scaling. +Improved axis scaling. The automatic axis scaling routine +now does a better job for ranges less than 1.0. The user can also +hardwire in min and max values, as well as specify a minimum increment, through +parameter 'yscale' in section [Images] in the configuration file. 1.1.0 11/14/09 diff --git a/TODO.txt b/TODO.txt index 1ce46863..6e61afd3 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,4 @@ TODO, in decreasing order of importance. -A few "del" here and there might reduce memory requirements. \ No newline at end of file +Rethink the logic of carrying over values from an old config file to the new, +when doing an upgrade. Present logic doesn't see commented out sections. \ No newline at end of file diff --git a/readme.htm b/readme.htm index 5d8270f8..673ee202 100644 --- a/readme.htm +++ b/readme.htm @@ -178,15 +178,14 @@ this program. If not, see reasons: it was a wet and miserable winter here in Oregon with not much else to do, so there was no good reason not to, and because I wanted a simple, easy-to-understand server to run my Davis VantagePro2 weather station on a Linux box. I had been using -wview, which is a very -high-performance, stable, and feature rich system authored by Michael -Teel with lots of users. Written in C, it's an amazingly efficient -system that can run on very underpowered boxes. In exchange, it's -huge (55,000+ lines of code), tightly integrated in with its companion -library, radlib (another 24,000+ lines), and complex. But, if -you're looking to be in good company and you want to run on -inexpensive, featherweight machines such as the -Linksys NSLU2, you can't beat it.

+wview, which is a very high-performance, +stable, and feature rich system authored by Michael Teel with lots of users. Written +in C, it's an amazingly efficient system that can run on very underpowered boxes. +In exchange, it's huge (55,000+ lines of code), tightly integrated in with its companion +library, radlib (another 24,000+ lines), and complex. But, if you're looking to +be in good company and you want to run on inexpensive, featherweight machines such +as the Linksys NSLU2, you can't +beat it.

Having made a career in C++ and Java, I was also interested in some more modern languages, so I thought I'd try either Python or Ruby (although, truth be told, the roots of Python are nearly as old as C++!). I picked Python because its libraries @@ -425,19 +424,19 @@ the setup.py script does include special provisions fo updating your configuration file weewx.conf, which can be handy when upgrading to a later version.

6. Configuring weewx

-

This section covers configuring your archive and statistical database (if -necessary; this step is required only if you are moving from -wview to weewx), configuring your -weather station, and configuring the configuration file -weewx.conf.

+

This section covers configuring your archive and statistical database (if necessary; +this step is required only if you are moving from +wview to weewx), configuring your weather +station, and configuring the configuration file weewx.conf.

In the following, $WEEWX_ROOT refers to the -weewx root directory, generally /home/weewx.

+weewx root directory, generally /home/weewx. +

6.1 Configuring the databases

This section is necessary only if you are moving from wview to weewx and wish to transfer your old data over. If you are starting afresh, you do not -need to follow this section --- the two main databases are created and populated -automatically by weewx.

+need to follow this section — the two main databases are created and populated automatically +by weewx.

Two databases are maintained by weewx:

-

Because wview and weewx use identical schema for the first of these (the -archive database), it can be just copied over. However, the second (the -statistical databases) are different --- the weewx statistical database must be -built manually and backfilled. This is done using the configuration -script configure.py.

-

Here's a summary of how to transfer your wview data to -weewx.

+

Because wview and weewx use identical schema for the first of these (the archive +database), it can be just copied over. However, the second (the statistical databases) +are different — the weewx statistical database must be built manually and backfilled. +This is done using the configuration script configure.py. +

+

Here's a summary of how to transfer your wview data to weewx.

mkdir $WEEWX_ROOT/archive
cp /usr/local/var/wview/archive/wview-archive.sdb $WEEWX_ROOT/archive/weewx.sdb
$WEEWX_ROOT/bin/configure.py --create-stats $WEEWX_ROOT/weewx.conf
@@ -465,10 +463,9 @@ MB) of data (while wview was running in the background).

VantagePro are the time and the archive interval.

Time

The time on the VP is automatically synchronized with the -weewx server every four hours. -However, you should run a NTP daemon on your server to insure that it is synchronized -with the correct time. Doing so will greatly reduce errors, especially if you send -data to services such as the Weather Underground.

+weewx server every four hours. However, you should run a NTP daemon on your +server to insure that it is synchronized with the correct time. Doing so will greatly +reduce errors, especially if you send data to services such as the Weather Underground.

Archive interval

The archive interval is set in the main configuration file $WEEWX_ROOT/weewx.conf. Look for the entry archive_interval @@ -683,17 +680,30 @@ this is each of the SQL types to be included in the image. Values specified in t level above can be overridden. For example, here's a typical set of options for sub-sub-section [[[monthrain]]]:

[[[monthrain]]]
-  [[[[rain]]]]
  plot_type = bar
-  aggregate_type = sum
-  aggregate_interval = 86400
-  label = Rain (daily avg)

+  yscale = None, None, 0.02
+  [[[[rain]]]]
+    aggregate_type = sum
+    aggregate_interval = 86400
+    label = Rain (daily avg)

This will generate an image file with name monthrain.png. -In it will be a bar plot showing SQL type 'rain'. The aggregation type will be summing -(overriding the averaging specified in sub-section [[month_images]], +It will be a bar plot. Option yscale controls the y-axis +scaling — if left out, the scale will automatically be chosen. However, in this +example we are choosing to exercise some degree of control by specifying values +explicitly. It is a 3-way tuple (ylow, +yhigh, min_interval), where +ymin and ymax are the minimum +and maximum y-axis values, respectively, and min_interval +is the minimum tick interval. If set to 'None', the corresponding +value will be automatically chosen. So, in this example, we are letting +weewx pick sensible y minimum and maximum values, but +we are requiring that the tick increment (min_interval) +be at least 0.02. Continuing on in the example, there will be only one SQL type +plotted, rain. The aggregation type will be summing (overriding +the averaging specified in sub-section [[month_images]], so you get the total rain over the aggregate period rather than the average) over -an aggregation interval of 86,400 seconds (one day). It will also include the indicated -label.

+an aggregation interval of 86,400 seconds (one day). The plot line will be titled +with the indicated label ('Rain (daily avg)')

More than one SQL type can be included in a plot. For example, here's how to generate a plot with the week's outside temperature as well as dewpoint:

[[[monthtempdew]]]
diff --git a/weeplot/genplot.py b/weeplot/genplot.py index 32026d89..5d52d5b6 100644 --- a/weeplot/genplot.py +++ b/weeplot/genplot.py @@ -296,28 +296,27 @@ class GeneralPlot(object): """Calculates y scaling. Can be used 'as-is' for most purposes. """ - if self.yscale is None: - # The filter is necessary because unfortunately the value 'None' is not - # excluded from min and max (i.e., min(None, x) is not necessarily x). - # Plus, min of an empty list throws a ValueError exception. - ymin = ymax = None - for line in self.line_list: - try : - yline_min = min(filter(lambda v : v is not None, line.y)) - ymin = min(yline_min, ymin) if ymin is not None else yline_min - except ValueError: - pass - try : - yline_max = max(filter(lambda v : v is not None, line.y)) - ymax = max(yline_max, ymax) if ymax is not None else yline_max - except ValueError: - pass - - if ymin is None and ymax is None : - # No valid data. Pick an arbitrary scaling - self.yscale=(0.0, 1.0, 0.2) - else: - self.yscale = weeplot.utilities.scale(ymin, ymax) + # The filter is necessary because unfortunately the value 'None' is not + # excluded from min and max (i.e., min(None, x) is not necessarily x). + # Plus, min of an empty list throws a ValueError exception. + ymin = ymax = None + for line in self.line_list: + try : + yline_min = min(filter(lambda v : v is not None, line.y)) + ymin = min(yline_min, ymin) if ymin is not None else yline_min + except ValueError: + pass + try : + yline_max = max(filter(lambda v : v is not None, line.y)) + ymax = max(yline_max, ymax) if ymax is not None else yline_max + except ValueError: + pass + + if ymin is None and ymax is None : + # No valid data. Pick an arbitrary scaling + self.yscale=(0.0, 1.0, 0.2) + else: + self.yscale = weeplot.utilities.scale(ymin, ymax, self.yscale) def _calcXLabelFormat(self): if self.x_label_format is None: diff --git a/weeplot/utilities.py b/weeplot/utilities.py index 7b5d06de..932f1f7d 100644 --- a/weeplot/utilities.py +++ b/weeplot/utilities.py @@ -16,25 +16,31 @@ import math import weeplot -def scale(fmn, fmx, nsteps = 10, prescale = None): +def scale(fmn, fmx, prescale = (None, None, None), nsteps = 10): """Calculates an appropriate min, max, and step size for scaling axes on a plot. + The origin (zero) is guaranteed to be on an interval boundary. + fmn: The minimum data value fmx: The maximum data value. Must be greater than or equal to fmn. - nsteps: The nominal number of desired steps. Default = 10 + prescale: A 3-way tuple. A non-None min or max value (positions 0 and 1, + respectively) will be fixed to that value. A non-None interval (position 2) + be at least as big as that value. Default = (None, None, None) - prescale: One or more of the results may be preset. [optional] + nsteps: The nominal number of desired steps. Default = 10 Returns: a three-way tuple. First value is the lowest scale value, second the highest. The third value is the step (increment) between them. """ - minscale = maxscale = interval = None + + (minscale, maxscale, min_interval) = prescale + # Make sure fmn and fmx are float values, in case a user passed + # in integers: + fmn = float(fmn) + fmx = float(fmx) - if prescale is not None : - (minscale, maxscale, interval) = prescale - if fmx < fmn : raise weeplot.ViolatedPrecondition, "scale() called with max value less than min value" @@ -44,31 +50,58 @@ def scale(fmn, fmx, nsteps = 10, prescale = None): else : fmx = fmn + .01*abs(fmn) - if interval is None: - range = float(fmx) - float(fmn) - steps = range / nsteps - - mag = math.floor(math.log10(steps)) - magPow = math.pow(10.0, mag) - magMsd = math.floor(steps/magPow + 0.5) - - if magMsd > 5.0: - magMsd = 10.0 - elif magMsd > 2.0: - magMsd = 5.0 - elif magMsd > 1.0: - magMsd = 2 + range = fmx - fmn + steps = range / nsteps - interval = magMsd * magPow + mag = math.floor(math.log10(steps)) + magPow = math.pow(10.0, mag) + magMsd = math.floor(steps/magPow + 0.5) - if minscale is None: - Nmin = math.floor(fmn / interval) - minscale = Nmin * interval + if magMsd > 5.0: + magMsd = 10.0 + elif magMsd > 2.0: + magMsd = 5.0 + elif magMsd > 1.0: + magMsd = 2 - if maxscale is None: - Nmax = math.ceil(fmx / interval) - maxscale = Nmax * interval + # This will be the nominal interval size + interval = magMsd * magPow + # Test it against the desired minimum, if any + if min_interval is None or interval > min_interval: + # Either no min interval was specified, or its safely + # less than the chosen interval. + if minscale is None: + minscale = interval * math.floor(fmn / interval) + + if maxscale is None: + maxscale = interval * math.ceil(fmx / interval) + + else: + + # The request for a minimum interval has kicked in. + # Sometimes this can make for a plot with just one or + # two intervals in it. Adjust the min and max values + # to get a nice plot + interval = min_interval + + if minscale is None: + if maxscale is None: + # Both can float. Pick values so the range is near the bottom + # of the scale: + minscale = interval * math.floor(fmn / interval) + maxscale = minscale + interval * nsteps + else: + # Only minscale can float + minscale = maxscale - interval * nsteps + else: + if maxscale is None: + # Only maxscale can float + maxscale = minscale + interval * nsteps + else: + # Both are fixed --- nothing to be done + pass + return (minscale, maxscale, interval) @@ -273,6 +306,7 @@ if __name__ == '__main__' : assert(scale(1.1, 12.3) == (1.0, 13.0, 1.0)) assert(scale(-1.1, 12.3) == (-2.0, 13.0, 1.0)) assert(scale(-12.1, -5.3) == (-13.0, -5.0, 1.0)) + assert(scale(0.0, 0.05, (None, None, .1), 10) == (0.0, 1.0, 0.1)) t= time.time() scaletime(t - 24*3600 - 20, t) diff --git a/weeutil/weeutil.py b/weeutil/weeutil.py index 874d63ba..c57cb045 100644 --- a/weeutil/weeutil.py +++ b/weeutil/weeutil.py @@ -81,9 +81,15 @@ def get_font_handle(fontpath, *args): return font +def convertToFloat(seq): + """Convert a sequence with strings to floats, honoring 'Nones'""" + + if seq is None: return None + res = [None if s in ('None', 'none') else float(s) for s in seq] + return res -def accumulatescalars(d): - """Merges scalar options above a ConfigObj section with itself, accumulating the results. +def accumulateLeaves(d): + """Merges leaf options above a ConfigObj section with itself, accumulating the results. This routine is useful for specifying defaults near the root node, then having them overridden in the leaf nodes of a ConfigObj. @@ -97,7 +103,7 @@ def accumulatescalars(d): cum_dict = configobj.ConfigObj() else : # Otherwise, recursively accumulate scalars above me - cum_dict = accumulatescalars(d.parent) + cum_dict = accumulateLeaves(d.parent) # Now merge my scalars into the results: merge_dict = {} diff --git a/weewx.conf b/weewx.conf index f5279bce..2a6aec1e 100644 --- a/weewx.conf +++ b/weewx.conf @@ -261,6 +261,12 @@ socket_timeout = 20 aggregate_type = none width = 1 time_length = 86400 # == 24 hours + # The following merits an explanation. The y-axis scale used for plotting + # can be controlled using option 'yscale'. It is a 3-way tuple, with + # values (ylow, yhigh, min_interval). If set to "None", a parameter is + # set automatically, otherwise the value is used. However, in the case of + # min_interval, what is set is the *minimum* y-axis tick interval. + yscale = None, None, None [[day_images]] x_label_format = %H:%M @@ -279,16 +285,20 @@ socket_timeout = 20 [[[[heatindex]]]] [[[dayrain]]] + # Make sure the y-axis increment is at least 0.02 for the rain plot: + yscale = None, None, 0.02 + plot_type = bar [[[[rain]]]] - plot_type = bar aggregate_type = sum aggregate_interval = 3600 label = Rain (hourly avg) + [[[dayrx]]] [[[[rxCheckPercent]]]] [[[daypond]]] + yscale = None, None, 0.5 [[[[extraTemp1]]]] [[[daywind]]] @@ -300,6 +310,8 @@ socket_timeout = 20 [[[[inHumidity]]]] [[[daywinddir]]] + # Hardwire in the y-axis scale for wind direction: + yscale = 0.0, 360.0, 60.0 [[[[windDir]]]] [[week_images]] @@ -321,13 +333,15 @@ socket_timeout = 20 [[[[heatindex]]]] [[[weekrain]]] + yscale = None, None, 0.02 + plot_type = bar [[[[rain]]]] - plot_type = bar aggregate_type = sum aggregate_interval = 86400 label = Rain (daily avg) [[[weekpond]]] + yscale = None, None, 0.5 [[[[extraTemp1]]]] [[[weekrx]]] @@ -343,6 +357,7 @@ socket_timeout = 20 [[[[inHumidity]]]] [[[weekwinddir]]] + yscale = 0.0, 360.0, 60.0 [[[[windDir]]]] [[month_images]] @@ -364,13 +379,15 @@ socket_timeout = 20 [[[[heatindex]]]] [[[monthrain]]] + yscale = None, None, 0.02 + plot_type = bar [[[[rain]]]] - plot_type = bar aggregate_type = sum aggregate_interval = 86400 label = Rain (daily avg) [[[monthpond]]] + yscale = None, None, 0.5 [[[[extraTemp1]]]] [[[monthrx]]] @@ -386,6 +403,7 @@ socket_timeout = 20 [[[[inHumidity]]]] [[[monthwinddir]]] + yscale = 0.0, 360.0, 60.0 [[[[windDir]]]] [[year_images]] @@ -413,14 +431,16 @@ socket_timeout = 20 [[[[heatindex]]]] [[[yearrain]]] + yscale = None, None, 0.02 + plot_type = bar [[[[rain]]]] - plot_type = bar aggregate_type = sum # aggregate_interval = 2629800 # Magic number: the length of a nominal month aggregate_interval = 604800 # == 1 week label = Rain (weekly avg) [[[yearpond]]] + yscale = None, None, 0.5 [[[[extraTemp1]]]] [[[yearrx]]] @@ -431,6 +451,7 @@ socket_timeout = 20 [[[[inHumidity]]]] [[[yearwinddir]]] + yscale = 0.0, 360.0, 60.0 [[[[windDir]]]] diff --git a/weewx/__init__.py b/weewx/__init__.py index 153f31fa..a61ead42 100644 --- a/weewx/__init__.py +++ b/weewx/__init__.py @@ -12,7 +12,7 @@ """ import time -__version__="1.2.0a1" +__version__="1.2.0a2" # Holds the program launch time in unix epoch seconds: # Useful for calculating 'uptime.' diff --git a/weewx/genimages.py b/weewx/genimages.py index bb455049..197e98d6 100644 --- a/weewx/genimages.py +++ b/weewx/genimages.py @@ -60,10 +60,13 @@ class GenImages(object): for plotname in self.image_dict[timespan].sections : # Accumulate all options from parent nodes: - plot_options = weeutil.weeutil.accumulatescalars(self.image_dict[timespan][plotname]) + plot_options = weeutil.weeutil.accumulateLeaves(self.image_dict[timespan][plotname]) # Get the name of the file that the image is going to be saved to: img_file = os.path.join(self.image_root, '%s.png' % plotname) + + if plotname == 'dayrain': + pass # Check whether this plot needs to be done: s = plot_options.get('aggregate_interval') @@ -81,7 +84,7 @@ class GenImages(object): # Loop over each line type ('outTemp', 'rain', etc.) within the plot. for line_type in line_type_list: # Accumulate options from parent nodes. - line_options[line_type] = weeutil.weeutil.accumulatescalars(self.image_dict[timespan][plotname][line_type]) + line_options[line_type] = weeutil.weeutil.accumulateLeaves(self.image_dict[timespan][plotname][line_type]) # Look for aggregation type: aggregate_type = line_options[line_type].get('aggregate_type') @@ -108,6 +111,7 @@ class GenImages(object): # Set the min, max time axis here. plot.setXScaling((minstamp, maxstamp, timeinc)) + plot.setYScaling(weeutil.weeutil.convertToFloat(plot_options.get('yscale'))) # Get a suitable bottom label: bottom_label_format = plot_options.get('bottom_label_format', '%m/%d/%y %H:%M')