1#!/usr/bin/env python3
2
3"""Implementation of the Histogram widget."""
4
5import logging
6import collections
7from datetime import timedelta
8from datetime import datetime
9
10# always import matplotlib_agg before matplotlib
11from chart.common import matplotlib_agg # (unused import) pylint: disable=W0611
12import matplotlib.colors
13
14from chart.common.path import Path
15from chart.plots.sampling import Sampling
16from chart.reports.widget import Widget
17from chart.plotviewer.hardcopy import render_histogram
18from chart.plotviewer.plot_utils import flot_palette
19from chart.plotviewer.plot_utils import DataPoint
20from chart.plotviewer import plot_settings
21from chart.plots.plot import Plot
22from chart.reports.widget import WidgetOption
23from chart.reports.widget import WidgetOptionChoice
24from chart.plotviewer.plot_utils import PlotException
25
26# Force orbital stats if the report duration is over this
27ORBITAL_STATS_DURATION = timedelta(days=3)
28
29DEFAULT_WIDTH = 1000
30DEFAULT_HEIGHT = 600
31
32logger = logging.getLogger()
33
34
35class HistogramWidget(Widget):
36 """Render a histogram for a list of data points."""
37
38 name = 'histogram'
39
40 thumbnail = 'widgets/histogram.png'
41
42 options = [
43 WidgetOption(name='title',
44 datatype='string',
45 description='Histogram title',
46 optional=True),
47 WidgetOption(name='filename',
48 datatype=str,
49 description='Force output filename',
50 optional=True),
51 WidgetOption(name='datapoint',
52 description='Data point(s) to compute',
53 multiple=True,
54 datatype=[
55 WidgetOption(name='field',
56 datatype='field',
57 description=(
58 'Timeseries data point to plot. Either a datapoint or an '
59 'event+property must be specified'),
60 optional=True),
61 WidgetOption(name='event',
62 datatype='eventproperty',
63 description=('Plot a numeric or duration property from an '
64 'event class'),
65 optional=True),
66 WidgetOption(name='axis',
67 datatype=int,
68 description='Specify axis (1=left, 2=right)',
69 default=1),
70 WidgetOption(name='label',
71 datatype=str,
72 description='Legend label',
73 optional=True),
74 WidgetOption(name='colour',
75 datatype='colour',
76 description='Force a specific render colour',
77 optional=True),
78 ]),
79 WidgetOption(name='sampling',
80 datatype=str,
81 description='Quantisation of input data',
82 default='all-points',
83 choices=[WidgetOptionChoice(
84 name='auto',
85 description=('Switch between all-points and stats data '
86 'depending on duration of report')),
87 WidgetOptionChoice(name='all-points',
88 description='Use tables only'),
89 WidgetOptionChoice(name='orbital-stats',
90 description='Use per-orbit averages only')]),
91 WidgetOption(name='calibrated',
92 datatype=bool,
93 description='Use calibrated data',
94 default=True),
95 WidgetOption(name='width',
96 datatype=int,
97 description='Width',
98 unit='pixels',
99 default=DEFAULT_WIDTH),
100 WidgetOption(name='height',
101 datatype=int,
102 description='Height',
103 default=DEFAULT_HEIGHT,
104 unit='pixels'),
105 WidgetOption(name='ylabel',
106 datatype=str,
107 description='Override label for y-axis',
108 optional=True),
109 WidgetOption(name='label-fontsize',
110 datatype=int,
111 description='Font size for axis labels',
112 default=10,
113 unit='pt'),
114 WidgetOption(name='absolute-start-time',
115 datatype=datetime,
116 description='Force start time for histogram',
117 optional=True),
118 WidgetOption(name='absolute-stop-time',
119 datatype=datetime,
120 description='Force stop time for histogram',
121 optional=True),
122 ]
123
124 document_options = collections.OrderedDict([
125 ('sid', {'type': 'sid'}),
126 ('sensing_start', {'type': 'datetime'}),
127 ('sensing_stop', {'type': 'datetime'})])
128
129 def __str__(self):
130 """Represent this widget as a string, is used in template log file."""
131 return 'Histogram'
132
133 def pre_html(self, document):
134 """Insert ourselves into the List of Figures widget."""
135 c = self.config
136 # document.figures.append(c['title'])
137
138 # Build an automatic title if the user didn't supply one
139 if 'title' not in c:
140 c['title'] = 'Histogram'
141
142 # Make sure we appear in the LoF widget
143 document.figures.append(c['title'])
144
145 def html(self, document):
146 """Render ourselves."""
147 c = self.config
148 dc = document.config
149
150 # If the user didn't supply a filename we deduce one
151 if 'filename' not in c:
152 # `anon_counts` gives the number of anonymous images already created
153 # for this report
154 if 'histogram' not in document.anon_counts:
155 document.anon_counts['histogram'] = 1
156 else:
157 document.anon_counts['histogram'] += 1
158
159 filename = Path('HISTOGRAM_{cc}.png'.format(cc=document.anon_counts['histogram']))
160
161 else:
162 filename = Path(c['filename'])
163
164 plot = Plot(filename=filename,
165 width=c['width'],
166 height=c['height'],
167 legend='none',
168 title=c['title'])
169
170 ax1 = plot.xaxis_non_ts()
171
172 if c['sampling'] == 'all-points':
173 subsampling = Sampling.ALL_POINTS
174
175 else:
176 subsampling = Sampling.ORBITAL
177
178 # build datapoints list expected by interactive plotting code
179 datapoints = []
180 for i, dp in enumerate(c['datapoint']):
181 if 'colour' in dp:
182 colour = matplotlib.colors.rgb2hex(matplotlib.colors.ColorConverter().to_rgb(dp['colour']))
183
184 else:
185 colour = flot_palette[i % len(flot_palette)]
186
187 datapoints.append(
188 DataPoint(sid=dc['sid'],
189 field=dp['field'],
190 table=dp['field'].table,
191 colour=colour
192 ))
193
194 sensing_start = dc['sensing_start']
195 if 'absolute-start-time' in c:
196 sensing_start = c['absolute-start-time']
197
198 sensing_stop = dc['sensing_stop']
199 if 'absolute-stop-time' in c:
200 sensing_stop = c['absolute-stop-time']
201
202 try:
203 render_histogram(ax1,
204 datapoints,
205 dc['sid'],
206 sensing_start=sensing_start,
207 sensing_stop=sensing_stop,
208 subsampling=subsampling,
209 calibrated=c['calibrated'],
210 min_val=None,
211 max_val=None,
212 opacity=plot_settings.DEFAULT_OPACITY,
213 logscale=False,
214 tick_fontsize=8,
215 label_fontsize=10)
216
217 except PlotException:
218 # Users would probably prefer to see a set of empty axis here
219 logger.warning('Dropping histogram due to no data')
220 document.html.write('<p>No data for histogram.</p>\n')
221 return
222
223 res = plot.finalise()
224 document.append_figure(filename=res['filename'],
225 title=c['title'],
226 width=c['width'],
227 height=c['height'])