#!/usr/bin/env python
"""
Plot graphs of weather data according to an XML recipe.
usage: python Plot.py [options] data_dir temp_dir xml_file output_file
options are:
\t-h or --help\t\tdisplay this help
data_dir is the root directory of the weather data
temp_dir is a workspace for temporary files e.g. /tmp
xml_file is the name of the source file that describes the plot
output_file is the name of the image file to be created e.g. 24hrs.png
"""
import codecs
from datetime import datetime, timedelta
import getopt
import os
import subprocess
import sys
import xml.dom.minidom
from conversions import *
import DataStore
import Localisation
from TimeZone import Local
from WeatherStation import dew_point, wind_chill, apparent_temp
[docs]class BasePlotter(object):
def __init__(self, params, raw_data, hourly_data,
daily_data, monthly_data, work_dir):
self.raw_data = raw_data
self.hourly_data = hourly_data
self.daily_data = daily_data
self.monthly_data = monthly_data
self.work_dir = work_dir
self.pressure_offset = eval(params.get('fixed', 'pressure offset'))
# set language related stuff
self.encoding = params.get('config', 'gnuplot encoding', 'iso_8859_1')
# create work directory
if not os.path.isdir(self.work_dir):
os.makedirs(self.work_dir)
[docs] def DoPlot(self, input_file, output_file):
# read XML graph description
self.doc = xml.dom.minidom.parse(input_file)
self.graph = self.doc.childNodes[0]
# get list of plots
plot_list = self.GetPlotList()
self.plot_count = len(plot_list)
if self.plot_count < 1:
# nothing to plot
self.doc.unlink()
return 1
# get start and end datetimes
self.x_lo = self.GetValue(self.graph, 'start', None)
self.x_hi = self.GetValue(self.graph, 'stop', None)
self.duration = self.GetValue(self.graph, 'duration', None)
if self.duration == None:
self.duration = timedelta(hours=24)
else:
self.duration = eval('timedelta(%s)' % self.duration)
if self.x_lo != None:
self.x_lo = eval('datetime(%s)' % self.x_lo)
if self.x_hi != None:
self.x_hi = eval('datetime(%s)' % self.x_hi)
self.duration = self.x_hi - self.x_lo
else:
self.x_hi = self.x_lo + self.duration
elif self.x_hi != None:
self.x_hi = eval('datetime(%s)' % self.x_hi)
self.x_lo = self.x_hi - self.duration
else:
self.x_hi = self.hourly_data.before(datetime.max)
if self.x_hi == None:
self.x_hi = datetime.utcnow() # only if no hourly data
# set end of graph to start of the next hour after last item
self.x_hi = self.x_hi + timedelta(minutes=55)
self.x_hi = self.x_hi.replace(minute=0, second=0)
self.x_lo = self.x_hi - self.duration
self.x_hi = self.x_hi + Local.utcoffset(self.x_lo)
self.x_lo = self.x_hi - self.duration
self.utcoffset = Local.utcoffset(self.x_lo)
# open gnuplot command file
self.tmp_files = []
cmd_file = os.path.join(self.work_dir, 'plot.cmd')
self.tmp_files.append(cmd_file)
of = open(cmd_file, 'w')
# write gnuplot set up
self.rows = self.GetDefaultRows()
self.cols = (self.plot_count + self.rows - 1) / self.rows
self.rows, self.cols = eval(self.GetValue(
self.graph, 'layout', '%d, %d' % (self.rows, self.cols)))
w, h = self.GetDefaultPlotSize()
w = w * self.cols
h = h * self.rows
w, h = eval(self.GetValue(self.graph, 'size', '(%d, %d)' % (w, h)))
fileformat = self.GetValue(self.graph, 'fileformat', 'png')
if fileformat == 'svg':
terminal = '%s enhanced font "arial,9" size %d,%d dynamic rounded' % (
fileformat, w, h)
else:
terminal = '%s large size %d,%d' % (fileformat, w, h)
terminal = self.GetValue(self.graph, 'terminal', terminal)
of.write('set encoding %s\n' % (self.encoding))
of.write('set terminal %s\n' % (terminal))
of.write('set output "%s"\n' % (output_file))
# set overall title
title = self.GetValue(self.graph, 'title', '')
if title:
title = title.encode(self.encoding)
title = 'title "%s"' % title
of.write('set multiplot layout %d, %d %s\n' % (self.rows, self.cols, title))
# do actual plots
of.write(self.GetPreamble())
for plot_no in range(self.plot_count):
plot = plot_list[plot_no]
# set key / title location
title = self.GetValue(plot, 'title', '')
title = title.encode(self.encoding)
of.write('set key horizontal title "%s"\n' % title)
# optional yaxis labels
ylabel = self.GetValue(plot, 'ylabel', '')
if ylabel:
ylabel = ylabel.encode(self.encoding)
ylabelangle = self.GetValue(plot, 'ylabelangle', '')
if ylabelangle:
ylabelangle = ' rotate by %s' % (ylabelangle)
of.write('set ylabel "%s"%s\n' % (ylabel, ylabelangle))
else:
of.write('set ylabel\n')
y2label = self.GetValue(plot, 'y2label', '')
if y2label:
y2label = y2label.encode(self.encoding)
y2labelangle = self.GetValue(plot, 'y2labelangle', '')
if y2labelangle:
y2labelangle = ' rotate by %s' % (y2labelangle)
of.write('set y2label "%s"%s\n' % (y2label, y2labelangle))
else:
of.write('set y2label\n')
# set data source
source = self.GetValue(plot, 'source', 'raw')
if source == 'raw':
source = self.raw_data
elif source == 'hourly':
source = self.hourly_data
elif source == 'monthly':
source = self.monthly_data
else:
source = self.daily_data
# do the plot
of.write(self.PlotData(plot_no, plot, source))
of.close()
self.doc.unlink()
# run gnuplot on file
subprocess.check_call(['gnuplot', cmd_file])
for file in self.tmp_files:
os.unlink(file)
return 0
[docs] def GetChildren(self, node, name):
result = []
for child in node.childNodes:
if child.localName == name:
result.append(child)
return result
[docs] def GetValue(self, node, name, default):
for child in node.childNodes:
if child.localName == name:
if child.childNodes:
return child.childNodes[0].data.strip()
else:
return ''
return default
[docs]class Record(object):
pass
[docs]class GraphPlotter(BasePlotter):
[docs] def GetPlotList(self):
return self.GetChildren(self.graph, 'plot')
[docs] def GetDefaultRows(self):
return self.plot_count
[docs] def GetDefaultPlotSize(self):
return 200 / self.cols, 600 / self.cols
[docs] def GetPreamble(self):
result = """set style fill solid
set xdata time
set timefmt "%Y-%m-%dT%H:%M:%S"
"""
result += 'set xrange ["%s":"%s"]\n' % (
self.x_lo.isoformat(), self.x_hi.isoformat())
lmargin = eval(self.GetValue(self.graph, 'lmargin', '5'))
result += 'set lmargin %g\n' % (lmargin)
rmargin = eval(self.GetValue(self.graph, 'rmargin', '-1'))
result += 'set rmargin %g\n' % (rmargin)
if self.duration <= timedelta(hours=24):
xformat = '%H%M'
elif self.duration <= timedelta(days=7):
xformat = '%a %d'
else:
xformat = '%Y/%m/%d'
xformat = self.GetValue(self.graph, 'xformat', xformat)
result += 'set format x "%s"\n' % xformat
xtics = self.GetValue(self.graph, 'xtics', None)
if xtics:
result += 'set xtics %d\n' % (eval(xtics) * 3600)
result = result.encode(self.encoding)
return result
[docs] def PlotData(self, plot_no, plot, source):
_ = Localisation.translation.ugettext
subplot_list = self.GetChildren(plot, 'subplot')
subplot_count = len(subplot_list)
if subplot_count < 1:
return ''
result = ''
pressure_offset = self.pressure_offset
# label x axis of last plot
if plot_no == self.plot_count - 1:
if self.duration <= timedelta(hours=24):
xlabel = _('Time (%Z)')
elif self.duration <= timedelta(days=7):
xlabel = _('Day')
else:
xlabel = _('Date')
xlabel = self.GetValue(self.graph, 'xlabel', xlabel)
xlabel = xlabel.encode(self.encoding)
result += 'set xlabel "%s"\n' % (
self.x_lo.replace(tzinfo=Local).strftime(xlabel))
dateformat = '%Y/%m/%d'
dateformat = self.GetValue(self.graph, 'dateformat', dateformat)
dateformat = dateformat.encode(self.encoding)
ldat = self.x_lo.replace(tzinfo=Local).strftime(dateformat)
rdat = self.x_hi.replace(tzinfo=Local).strftime(dateformat)
if ldat != '':
result += 'set label "%s" at "%s", graph -0.3 left\n' % (
ldat, self.x_lo.isoformat())
if rdat != ldat:
result += 'set label "%s" at "%s", graph -0.3 right\n' % (
rdat, self.x_hi.isoformat())
# set bottom margin
bmargin = eval(self.GetValue(plot, 'bmargin', '-1'))
result += 'set bmargin %g\n' % (bmargin)
# set y ranges and tics
yrange = self.GetValue(plot, 'yrange', None)
y2range = self.GetValue(plot, 'y2range', None)
ytics = self.GetValue(plot, 'ytics', 'autofreq')
y2tics = self.GetValue(plot, 'y2tics', '')
if y2tics and not y2range:
y2range = yrange
elif y2range and not y2tics:
y2tics = 'autofreq'
if yrange:
result += 'set yrange [%s]\n' % (yrange.replace(',', ':'))
else:
result += 'set yrange [*:*]\n'
if y2range:
result += 'set y2range [%s]\n' % (y2range.replace(',', ':'))
if y2tics:
result += 'set ytics nomirror %s; set y2tics %s\n' % (ytics, y2tics)
else:
result += 'unset y2tics; set ytics mirror %s\n' % (ytics)
# set grid
result += 'unset grid\n'
grid = self.GetValue(plot, 'grid', None)
if grid != None:
result += 'set grid %s\n' % grid
# x_lo & x_hi are in local time, data is indexed in UTC
start = self.x_lo - self.utcoffset
stop = self.x_hi - self.utcoffset
cumu_start = start
if source == self.raw_data:
boxwidth = 240 # assume 5 minute data interval
start = source.before(start)
elif source == self.hourly_data:
boxwidth = 2800
start = source.before(start)
interval = timedelta(minutes=90)
elif source == self.monthly_data:
boxwidth = 2800 * 24 * 30
interval = timedelta(days=46)
else:
interval = timedelta(hours=36)
boxwidth = 2800 * 24
boxwidth = eval(self.GetValue(plot, 'boxwidth', str(boxwidth)))
result += 'set boxwidth %d\n' % boxwidth
command = self.GetValue(plot, 'command', None)
if command:
result += '%s\n' % command
stop = source.after(stop)
if stop:
stop = stop + timedelta(minutes=1)
# write data files
subplots = []
for subplot_no in range(subplot_count):
subplot = Record()
subplot.subplot = subplot_list[subplot_no]
subplot.dat_file = os.path.join(self.work_dir, 'plot_%d_%d.dat' % (
plot_no, subplot_no))
self.tmp_files.append(subplot.dat_file)
subplot.dat = open(subplot.dat_file, 'w')
subplot.xcalc = self.GetValue(subplot.subplot, 'xcalc', None)
subplot.ycalc = self.GetValue(subplot.subplot, 'ycalc', None)
subplot.cummulative = 'last_ycalc' in subplot.ycalc
if subplot.xcalc:
subplot.xcalc = compile(subplot.xcalc, '<string>', 'eval')
subplot.ycalc = compile(subplot.ycalc, '<string>', 'eval')
subplot.last_ycalcs = 0.0
subplot.last_idx = None
subplots.append(subplot)
for data in source[start:stop]:
for subplot in subplots:
if subplot.xcalc:
idx = eval(subplot.xcalc)
if idx is None:
continue
else:
idx = data['idx']
idx += self.utcoffset
if not subplot.cummulative and subplot.last_idx:
if source == self.raw_data:
interval = timedelta(minutes=((data['delay']*3)+1)/2)
if idx - subplot.last_idx > interval:
# missing data
subplot.dat.write('%s ?\n' % (idx.isoformat()))
subplot.last_idx = idx
try:
if subplot.cummulative and data['idx'] <= cumu_start:
value = 0.0
else:
last_ycalc = subplot.last_ycalcs
value = eval(subplot.ycalc)
subplot.dat.write('%s %g\n' % (idx.isoformat(), value))
subplot.last_ycalcs = value
except TypeError:
if not subplot.cummulative:
subplot.dat.write('%s ?\n' % (idx.isoformat()))
subplot.last_ycalcs = 0.0
for subplot in subplots:
# ensure the data file isn't empty
idx = self.x_hi + self.duration
subplot.dat.write('%s ?\n' % (idx.isoformat()))
subplot.dat.close()
# plot data
result += 'plot '
colour = 0
for subplot_no in range(subplot_count):
subplot = subplots[subplot_no]
colour = eval(self.GetValue(subplot.subplot, 'colour', str(colour+1)))
style = self.GetValue(
subplot.subplot, 'style', 'smooth unique lc %d lw 1' % (colour))
words = style.split()
if len(words) > 1 and words[0] in ('+', 'x', 'line'):
width = int(words[1])
else:
width = 1
if style == 'box':
style = 'lc %d lw 0 with boxes' % (colour)
elif words[0] == '+':
style = 'lc %d lw %d pt 1 with points' % (colour, width)
elif words[0] == 'x':
style = 'lc %d lw %d pt 2 with points' % (colour, width)
elif words[0] == 'line':
style = 'smooth unique lc %d lw %d' % (colour, width)
axes = self.GetValue(subplot.subplot, 'axes', 'x1y1')
title = self.GetValue(subplot.subplot, 'title', '')
result += ' "%s" using 1:($2) axes %s title "%s" %s' % (
subplot.dat_file, axes, title, style)
if subplot_no != subplot_count - 1:
result += ', \\'
result += '\n'
result = result.encode(self.encoding)
return result
[docs]def main(argv=None):
if argv is None:
argv = sys.argv
try:
opts, args = getopt.getopt(argv[1:], "h", ['help'])
except getopt.error, msg:
print >>sys.stderr, 'Error: %s\n' % msg
print >>sys.stderr, __doc__.strip()
return 1
# process options
for o, a in opts:
if o == '-h' or o == '--help':
print __doc__.strip()
return 0
# check arguments
if len(args) != 4:
print >>sys.stderr, 'Error: 4 arguments required\n'
print >>sys.stderr, __doc__.strip()
return 2
params = DataStore.params(args[0])
Localisation.SetApplicationLanguage(params)
return GraphPlotter(
params,
DataStore.calib_store(args[0]), DataStore.hourly_store(args[0]),
DataStore.daily_store(args[0]), DataStore.monthly_store(args[0]),
args[1]
).DoPlot(args[2], args[3])
if __name__ == "__main__":
sys.exit(main())