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'])