1#!/usr/bin/env python3
  2
  3"""Generate PlotXY graphs."""
  4
  5import string  # (depreciated module) pylint: disable=W0402
  6import logging
  7from collections import OrderedDict
  8
  9import numpy as np
 10# always import matplotlib_agg before matplotlib
 11from chart.common import matplotlib_agg  # (unused import) pylint: disable=W0611
 12from matplotlib import ticker
 13from matplotlib import figure
 14from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
 15from matplotlib import font_manager
 16
 17from chart.plots.plot import Plot
 18from chart.reports.widget import Widget
 19from chart.common.exceptions import ConfigError
 20from chart.common.traits import is_listlike
 21from chart.common.prettyprint import Table
 22from chart.plotviewer.make_url import make_plot_url
 23from chart.project import SID
 24from chart.db import ts
 25from chart.plots.shapes import MarkerShape
 26from chart.plots.shapes import LineType
 27from chart.reports.widget import WidgetOptionChoice
 28
 29# if the user has not specified a filename, use this template
 30DEFAULT_FILENAME = 'GRAPHXY{number}{ext}'
 31DEFAULT_THUMB_FILENAME = 'GRAPHXYTHUMB{number}{ext}'
 32
 33# default size in points for the axis labels
 34LABEL_FONTSIZE = 8
 35
 36# spacing around the figure (allow space for axis labels) in px
 37# top margin is increased if we have title
 38LEFT_MARGIN = 70
 39BOTTOM_MARGIN = 50
 40TOP_MARGIN = 10
 41RIGHT_MARGIN = 10
 42
 43
 44class PlotXYGraph:
 45    """Render a 2D graph showing Y values plotted against X values."""
 46
 47    def __init__(self,
 48                 width,
 49                 height,
 50                 x,
 51                 y,
 52                 sid,
 53                 colour,
 54                 edgecolour,
 55                 x_axis_min,
 56                 x_axis_max,
 57                 y_axis_min,
 58                 y_axis_max,
 59                 title=None,
 60                 filename=None):
 61        self.filename = filename
 62        self.title = title
 63        self.width = width
 64        self.height = height
 65        self.names = []
 66        self.unit = None
 67        self.limits = [None, None]
 68        self.sid = sid
 69        self.x = x  # Move x and y datapoints out of contructor and put them in create_plot
 70        self.y = y
 71        self.colour = colour
 72        self.edgecolour = edgecolour
 73        self.x_min = x_axis_min
 74        self.x_max = x_axis_max
 75        self.y_min = y_axis_min
 76        self.y_max = y_axis_max
 77
 78        # Create a new figure and new subplot from a grid of 1x1
 79        self.fig = figure.Figure(figsize=(width / Plot.DPI,
 80                                          height / Plot.DPI),
 81                                 dpi=Plot.DPI)
 82
 83        bottom_margin = BOTTOM_MARGIN
 84        left_margin = LEFT_MARGIN
 85        right_margin = RIGHT_MARGIN
 86
 87        FigureCanvas(self.fig)  # needed internally by matplotlib
 88        if title is not None:
 89            t = self.fig.suptitle(title)
 90            top_margin = (height - t.get_window_extent(
 91                renderer=self.fig.canvas.get_renderer()).y0 + TOP_MARGIN)
 92
 93        else:
 94            top_margin = TOP_MARGIN
 95
 96        self.ax1 = self.fig.add_axes((
 97                left_margin / width,  # left
 98                bottom_margin / height,  # bottom
 99                1 - (left_margin + right_margin) / width,  # width
100                1 - (top_margin + bottom_margin) / height))
101
102    def create_plot(self,
103                 label_fontsize,
104                 xlabel,
105                 ylabel,
106                 marker_size,
107                 marker_shape,
108                 xunit,
109                 yunit,
110                 line_width,
111                 line_style):
112        """Create plot."""
113
114        # plot figure with selected data and settings
115        self.ax1.plot(self.x,
116                      self.y,
117                      color=self.colour,
118                      edgecolor=self.edgecolour,
119                      markersize=marker_size,
120                      marker=marker_shape.mpl,
121                      linestyle=line_style.mpl,
122                      linewidth=line_width)
123
124        # Format axes notation
125        formatter = ticker.FormatStrFormatter('%1.1e')
126        self.ax1.yaxis.set_major_formatter(formatter)
127        self.ax1.xaxis.set_major_formatter(formatter)
128
129        # Set x and y axes limits
130        if self.x_min is None or self.y_min is None:
131            # Set limits based on the data + 10% margin
132            y_width = (self.y.max() - self.y.min()) * .55
133            y_mid = (self.y.min() + self.y.max()) / 2
134            ylimit = [y_mid - y_width, y_mid + y_width]
135            x_width = (self.x.max() - self.x.min()) * .55
136            x_mid = (self.x.min() + self.x.max()) / 2
137            xlimit = [x_mid - x_width, x_mid + x_width]
138
139        else:
140            # Set limits according to user provided values
141            ylimit = [self.y_min, self.y_max]
142            xlimit = [self.x_min, self.x_max]
143
144        # configure 'x' and 'y' axes using specified limits
145
146        if xunit is not None:
147            xlabel = '{label} ({unit}) '.format(label=xlabel, unit=xunit)
148
149        if yunit is not None:
150            ylabel = '{label} ({unit}) '.format(label=ylabel, unit=yunit)
151
152        self.setup_yaxis(ylimits=ylimit, ylabels=ylabel, label_fontsize=label_fontsize)
153        self.setup_xaxis(xlimits=xlimit, xlabels=xlabel, label_fontsize=label_fontsize)
154
155    def setup_xaxis(self,
156              xlimits=None,
157              unit=None,
158              label_fontsize=LABEL_FONTSIZE,
159              xlabels=None,
160              tick_labels=None):
161        """Set up x-axis ticks and labels.
162
163        Args:
164
165            `xlimits` (list of lists): List of (min, max) pairs for the limits of each y-axis
166                Also control whether we have 1 or 2 y-axis
167            `unit` (str): Use `labels` instead.
168            `label_fontsize` (int): Font size for labels
169            `xlabel` (mixed): If a single string, left hand y-axis label. If a list of strings,
170                x-axis labels for multiple axes. Can be a dictionary of ('text', 'colour') instead.
171            `tick_label` (list of str): Force labels instead of numerically generated.
172
173        If `tick_labels` is given, this is a list of strings corresponding to each position in the
174        x-axis. In this case a suitably wide `left_margin` should have already been passed to
175        xaxis.
176        """
177
178        # if `unit` is given map it to `ylabels`
179        if unit is not None:
180            if xlabels is not None:
181                raise ConfigError('Use xlabels instead of unit')
182
183            if is_listlike(unit):
184                xlabels = unit
185
186            else:
187                xlabels = unit
188
189        # if `xlabels` only gives a single label make it a list
190        if not is_listlike(xlabels):
191            xlabels = [xlabels]
192
193        # if `xlabels` contains strings map them to [text, colour) dictionaries
194        for i in range(len(xlabels)):
195            if xlabels[i] is None:
196                xlabels[i] = {'text': '', 'colour': 'black'}
197
198            elif isinstance(xlabels[i], str):
199                xlabels[i] = {'text': xlabels[i], 'colour': 'black'}
200
201        if xlimits is None or len(xlimits) == 0:
202            xlimits = [None]
203
204        elif not is_listlike(xlimits[0]):
205            xlimits = [xlimits]
206
207        def imp(axis, label, label_color, tick_label, xlimit):
208            """Fix a single yaxis - we could have top and bottom axis."""
209
210            if tick_label is not None:
211                # client has supplied a fixed set of labels
212                BOTTOM_TICK_MARGIN = 0.2
213                TOP_TICK_MARGIN = 0.8
214                axis.set_xlim([-BOTTOM_TICK_MARGIN, len(tick_labels) - TOP_TICK_MARGIN])
215                axis.xaxis.set_major_formatter(ticker.FixedFormatter(tick_label))
216                axis.xaxis.set_major_locator(ticker.FixedLocator(list(range(len(tick_label)))))
217
218            elif xlimit is not None and xlimit[0] != xlimit[1]:  # !?!
219                axis.set_xlim(*xlimit)  # probably only if there is no data to plot
220
221                # test whether to use scientific notation
222                YAXIS_SCI_NOTATION = 100000
223                if (xlimit[0] < -YAXIS_SCI_NOTATION or
224                    xlimit[1] > YAXIS_SCI_NOTATION or
225                    (abs(xlimit[0]) < 0.001 and abs(xlimit[1]) < 0.001)):
226
227                    logging.debug('Using scientific notation for xaxis limits')
228
229                    # compute the exponent to apply to labels
230                    exp = np.floor(np.log10(np.max(np.abs(xlimit))))
231
232                    if exp % 3 != 0:
233                        # round the exponent down to a multiple of 3
234                        exp -= exp % 3
235
236                    label = '{label} * 10 ^ {exp}'.format(label=label, exp=exp)
237
238                    max_displayed_number = (np.max(np.abs(xlimit)) / (10 ** exp))
239                    integer_labels = max_displayed_number > 100.0
240
241                    def tick_format(val, _):
242                        """Custom formatter for x-axis labels to set exponents to `exp`."""
243                        res = val / np.power(10, exp)
244
245                        # for large values (>100) hide any decimal places
246                        if integer_labels:
247                            res = '{:.0f}'.format(res)  # <- dont use, it renders 100 as 100.0 !!!
248
249                        return res
250
251                    axis.xaxis.set_major_formatter(ticker.FuncFormatter(tick_format))
252
253                else:
254                    # otherwise use plain decimals
255                    axis.xaxis.set_major_formatter(ticker.ScalarFormatter())
256                    axis.ticklabel_format(style='plain', useOffset=False, axis='x')
257
258            # write the label (unit and optionally exponent) and set it's size
259            axis.set_xlabel(label, color=label_color)
260            axis.xaxis.get_label().set_fontsize(label_fontsize)
261
262            # modify the size of the tick labels
263            for l in axis.get_xticklabels():
264                l.set_fontsize(label_fontsize)
265                l.set_color(label_color)
266
267        self.ax1.xaxis.grid(True, which='major')
268        imp(self.ax1, xlabels[0]['text'], xlabels[0]['colour'], tick_labels, xlimits[0])
269        return [self.ax1]
270
271    def setup_yaxis(self,
272              ylimits=None,
273              unit=None,
274              label_fontsize=LABEL_FONTSIZE,
275              ylabels=None,
276              tick_labels=None):
277        """Set up y-axis ticks and labels.
278
279        Args:
280
281            `ylimits` (list of lists): List of (min, max) pairs for the limits of each y-axis
282                Also control whether we have 1 or 2 y-axis
283            `unit` (str): Use `labels` instead.
284            `label_fontsize` (int): Font size for labels
285            `ylabel` (mixed): If a single string, left hand y-axis label. If a list of strings,
286                y-axis labels for multiple axes. Can be a dictionary of ('text', 'colour') instead.
287            `tick_label` (list of str): Force labels instead of numerically generated.
288
289        If `tick_labels` is given, this is a list of strings corresponding to each position in the
290        y-axis. In this case a suitably wide `left_margin` should have already been passed to
291        xaxis.
292        """
293
294        # if `unit` is given map it to `ylabels`
295        if unit is not None:
296            if ylabels is not None:
297                raise ConfigError('Use ylabels instead of unit')
298
299            if is_listlike(unit):
300                ylabels = unit
301
302            else:
303                ylabels = unit
304
305        # if `ylabels` only gives a single label make it a list
306        if not is_listlike(ylabels):
307            ylabels = [ylabels]
308
309        # if `ylabels` contains strings map them to [text, colour) dictionaries
310        for i in range(len(ylabels)):
311            if ylabels[i] is None:
312                ylabels[i] = {'text': '', 'colour': 'black'}
313
314            elif isinstance(ylabels[i], str):
315                ylabels[i] = {'text': ylabels[i], 'colour': 'black'}
316
317        if ylimits is None or len(ylimits) == 0:
318            ylimits = [None]
319
320        elif not is_listlike(ylimits[0]):
321            ylimits = [ylimits]
322
323        def imp(axis, label, label_color, tick_label, ylimit):
324            """Fix a single yaxis - we could have top and bottom axis."""
325
326            # if label is None:
327                # label = nvl(unit, '')
328
329            # if label_color is None:
330                # label_color = 'Black'
331
332            if tick_label is not None:
333                # client has supplied a fixed set of labels
334                BOTTOM_TICK_MARGIN = 0.2
335                TOP_TICK_MARGIN = 0.8
336                axis.set_ylim([-BOTTOM_TICK_MARGIN, len(tick_labels) - TOP_TICK_MARGIN])
337                axis.yaxis.set_major_formatter(ticker.FixedFormatter(tick_label))
338                axis.yaxis.set_major_locator(ticker.FixedLocator(list(range(len(tick_label)))))
339                # axis.yaxis.set_major_locator(ticker.IndexLocator(1, 0))
340
341            elif ylimit is not None and ylimit[0] != ylimit[1]:  # !?!
342                axis.set_ylim(*ylimit)  # probably only if there is no data to plot
343
344                # test whether to use scientific notation
345                YAXIS_SCI_NOTATION = 100000
346                if (ylimit[0] < -YAXIS_SCI_NOTATION or
347                    ylimit[1] > YAXIS_SCI_NOTATION or
348                    (abs(ylimit[0]) < 0.001 and abs(ylimit[1]) < 0.001)):
349
350                    logging.debug('Using scientific notation for yaxis limits')
351
352                    # compute the exponent to apply to labels
353                    exp = np.floor(np.log10(np.max(np.abs(ylimit))))
354
355                    if exp % 3 != 0:
356                        # round the exponent down to a multiple of 3
357                        exp -= exp % 3
358
359                    label = '{label} * 10 ^ {exp}'.format(label=label, exp=exp)
360                    # print label
361
362                    max_displayed_number = (np.max(np.abs(ylimit)) / (10 ** exp))
363                    integer_labels = max_displayed_number > 100.0
364                    # print 'int lab test ', max_displayed_number,' result ', integer_labels
365
366                    def tick_format(val, _):
367                        """Custom formatter for y-axis labels to set exponents to `exp`."""
368                        res = val / np.power(10, exp)
369
370                        # for large values (>100) hide any decimal places
371                        if integer_labels:
372                            res = '{:.0f}'.format(res)  # <- dont use, it renders 100 as 100.0 !!!
373                            # res = str(int(res + 0.01))
374                            # res = '{0:d}'.format(int(float(res+0.1)))
375
376                        return res
377
378                    axis.yaxis.set_major_formatter(ticker.FuncFormatter(tick_format))
379
380                else:
381                    # otherwise use plain decimals
382                    axis.yaxis.set_major_formatter(ticker.ScalarFormatter())
383                    axis.ticklabel_format(style='plain', useOffset=False, axis='y')
384
385            # write the label (unit and optionally exponent) and set it's size
386            axis.set_ylabel(label, color=label_color)
387            axis.yaxis.get_label().set_fontsize(label_fontsize)
388
389            # modify the size of the tick labels
390            for l in axis.get_yticklabels():
391                l.set_fontsize(label_fontsize)
392                l.set_color(label_color)
393                # label.set_text('x' + label.get_text() + 'x')
394                # label.set_label('hello')
395                # print('y tick ', l.get_label())
396
397        self.ax1.yaxis.grid(True, which='major')
398
399        imp(self.ax1, ylabels[0]['text'], ylabels[0]['colour'], tick_labels, ylimits[0])
400        return [self.ax1]
401
402    def finalise(self,
403                 filename):
404        """Write plot image to disk."""
405
406        # allow filename to be set in finalise() if not done in constructor
407        if filename is not None:
408            self.filename = filename
409
410        if self.filename is None and self.title is not None:
411            self.filename = self.title.replace(' ', '_').replace('/', '_')
412
413        if self.filename is None:
414            raise ConfigError('Filename must be specified either in constructor or finalise '
415                              'function')
416
417        # add a .png filename extension is not already present
418        if self.filename.suffix != Plot.EXT:
419            self.filename.suffix = Plot.EXT
420
421        # save plot to file
422        self.fig.savefig(str(self.filename), dpi=100)
423
424        # return a data structure suitable for passing to report.add_image()
425        return {'filename': self.filename,
426                'title': self.title}
427
428
429class GraphXY(Widget):
430    """Display horizontal bar chart showing the min/max values for a list of parameters
431    in the given time range, and also show the MCS limits on the same axis.
432    """
433
434    name = 'graph-xy'
435
436    # todo: make these files and put in correct dir (widgets/static/widgets...)
437    image = 'widgets/graphxy.png'
438    thumbnail = 'widgets/graphxy_sm.png'
439
440    options = OrderedDict([
441            ('title', {'type': 'string',
442                       'description': 'Override default title',
443                       'optional': True}),
444            ('filename', {'type': 'string',
445                          'description': 'Override default filename',
446                          'optional': True}),
447            ('thumbnail-filename', {'type': 'string',
448                          'description': 'Override default thumbnail filename',
449                          'optional': True}),
450            ('x-series', {'type': 'field',
451                          'description': 'Field to show in X axis'}),
452            ('y-series', {'type': 'field',
453                          'description': 'Field to show in Y axis'}),
454            ('relative-start-time', {
455                    'description': 'Apply an offset from the report start time to the start of the '
456                    'graph',
457                    'type': 'duration',
458                    'optional': True}),
459            ('relative-stop-time', {
460                    'description': 'Apply an offset from the report start time to the stop of the '
461                    'graph',
462                    'type': 'duration',
463                    'optional': True}),
464            ('height', {'type': 'uint',
465                       'default': 800,
466                       'unit': 'pixels',
467                       'description': ('Height of output image')}),
468            ('width', {'type': 'uint',
469                       'default': 800,
470                       'unit': 'pixels',
471                       'description': ('Width of output image')}),
472            ('thumbnail-height', {'type': 'uint',
473                                 'default': 500,
474                                 'description': 'Height of initially displayed thumbnail image',
475                                 'unit': 'pixels'}),
476            ('thumbnail-width', {'type': 'uint',
477                                 'default': 500,
478                                 'description': 'Width of initially displayed thumbnail image',
479                                 'unit': 'pixels'}),
480            ('x-axis-min', {'type': 'float',
481                                 'optional': True,
482                                 'default': None,
483                                 'description': 'x-axis min limit',
484                                 'unit': 'pixels'}),
485            ('x-axis-max', {'type': 'float',
486                                 'optional': True,
487                                 'default': None,
488                                 'description': 'x-axis max limit',
489                                 'unit': 'pixels'}),
490            ('y-axis-min', {'type': 'float',
491                                 'optional': True,
492                                 'default': None,
493                                 'description': 'y-axis min limit',
494                                 'unit': 'pixels'}),
495            ('y-axis-max', {'type': 'float',
496                                 'optional': True,
497                                 'default': None,
498                                 'description': 'y-axis max limit',
499                                 'unit': 'pixels'}),
500            ('marker-colour', {'type': 'string',
501                               'description': 'Colour of marker symbols',
502                               'default': 'blue'}),
503            ('edge-colour', {'type': 'string',
504                               'description': 'Colour of edge symbols',
505                               'default': 'black'}),
506            ('marker-size', {'type': 'uint',
507                       'default': 2,
508                       'unit': 'pixels',
509                       'description': ('Size of plotting symbol')}),
510            ('line-style', {'type': 'string',
511                       'default': 'solid',
512                       'description': ('Line styles in plot')}),
513            ('line-width', {'type': 'uint',
514                       'default': 1,
515                       'unit': 'pixels',
516                       'description': ('Width of lines in plot')}),
517            ('marker-shape',
518                {'type': 'string',
519                 'default': 'circle',
520                 'description': 'Shape of plotting symbol',
521                 'choices': [WidgetOptionChoice(name='circle'),
522                             WidgetOptionChoice(name='plus'),
523                             WidgetOptionChoice(name='point'),
524                             WidgetOptionChoice(name='square'),
525                             WidgetOptionChoice(name='star'),
526                             WidgetOptionChoice(name='triangle')]}),
527            ('label-fontsize', {'type': 'uint',
528                                'unit': 'pt',
529                                'default': LABEL_FONTSIZE,
530                                'description': 'Font size for axis labels'}),
531            ('title-fontsize', {'type': 'uint',
532                                'unit': 'pt',
533                                'default': 0,
534                                'description': ('Font size for the title. 0 disabled embedded '
535                                                'title. Only 0 and 10 can be used.')}),
536            ])
537
538    document_options = OrderedDict([
539            ('sid', {'type': 'sid'}),
540            ('sensing_start', {'type': 'datetime'}),
541            ('sensing_stop', {'type': 'datetime'})])
542
543    def __init__(self):
544        super(GraphXY, self).__init__()
545        self.title = None
546
547    def get_time_range(self, dc, c, sid):  # (unused arg) pylint: disable=W0613
548        """Choose the time range for this graph based on the report start and stop settings
549        and the config settings, relative-start-time and relative-stop-time.
550        """
551
552        sensing_start = dc['sensing_start']
553        sensing_stop = dc['sensing_stop']
554
555        if 'relative-stop-time' in c:
556            sensing_stop = sensing_start + c['relative-stop-time']
557
558        if 'relative-start-time' in c:
559            sensing_start += c['relative-start-time']
560
561        return sensing_start, sensing_stop
562
563    def __str__(self):
564        """String representation of this widget, appears in template log file."""
565
566        # trap self.config here in case an exception was thrown early in construction but
567        # someone is still trying to print us.
568        if hasattr(self, 'config') and self.config is not None:
569            return 'GraphXY({title})'.format(title=self.title)
570
571        else:
572            return 'GraphXY'
573
574    def pre_html(self, document):
575        """Insert ourselves into the List of Figures widget."""
576        c = self.config
577        if 'title' not in c:
578            self.title = '{x} vs {y}'.format(x=c['x-series'].name, y=c['y-series'].name)
579
580        else:
581            self.title = c['title']
582
583        document.figures.append(self.title)
584
585    def html(self, document):
586        """Render ourselves."""
587
588        c = self.config
589        dc = document.config
590
591        if 'filename' not in c:
592            # `anon_counts` gives the number of anonymous images already created
593            # for this report
594            if 'graphxy' not in document.anon_counts:
595                document.anon_counts['graphxy'] = 1  # -> graphxy
596
597            else:
598                document.anon_counts['graphxy'] += 1
599
600            base_filename = DEFAULT_FILENAME.format(number=document.anon_counts['graphxy'],
601                                                    ext=Plot.EXT)
602            thumbnail_base_filename = DEFAULT_THUMB_FILENAME.format(
603                                                    number=document.anon_counts['graphxy'],
604                                                    ext=Plot.EXT)
605        else:
606            base_filename = c['filename']
607            thumbnail_base_filename = c['thumbnail-filename']
608
609        filename = document.theme.mod_filename(document, base_filename)
610        filename_thumb = document.theme.mod_filename(document, thumbnail_base_filename)
611
612        # adjust data sensing start and stop times
613        sensing_start, sensing_stop = self.get_time_range(dc, c, dc['sid'])
614
615        # get data from database
616        rows_x = ts.select(
617            table=c['x-series'].table,
618            fields=('SENSING_TIME', c['x-series']),
619            sid=dc['sid'],
620            sensing_start=sensing_start,
621            sensing_stop=sensing_stop,
622            ).fetchall()
623
624        rows_y = ts.select(
625            table=c['y-series'].table,
626            fields=('SENSING_TIME', c['y-series']),
627            sid=dc['sid'],
628            sensing_start=sensing_start,
629            sensing_stop=sensing_stop,
630            ).fetchall()
631
632        if len(rows_x) != len(rows_y):
633            logging.error('Tables do not have the same number of points.'.format(cc=len(rows_y)))
634            document.html.write('\n<p>{title} cannot be displayed because input tables have \
635            different numbers of points.</p>\n'.format(title=self.title))
636            return
637
638        if len(rows_x) == 0:
639            logging.warning('There are no samples to display')
640            document.html.write('\n<p>{title} There are no samples to display.</p>\n'.
641                                                                    format(title=self.title))
642            return
643
644        # prepare and copy data into X and Y containers to plot them
645        x = np.zeros((len(rows_x),), dtype=float)
646        y = np.zeros((len(rows_y),), dtype=float)
647
648        for cc, i in enumerate(range(len(rows_x))):
649            x[cc] = rows_x[i][1]
650            y[cc] = rows_y[i][1]
651
652        marker_shape = MarkerShape(c['marker-shape'])
653        line_style = LineType(c['line-style'])
654
655        plot = PlotXYGraph(title=self.title if c['title-fontsize'] > 0 else None,
656                           filename=filename,
657                           width=c['width'],
658                           height=c['height'],
659                           x=x,
660                           y=y,
661                           sid=dc['sid'],
662                           colour=c['marker-colour'],
663                           edgecolour=c['edge-colour'],
664                           x_axis_min=c['x-axis-min'],
665                           x_axis_max=c['x-axis-max'],
666                           y_axis_min=c['y-axis-min'],
667                           y_axis_max=c['y-axis-max'])
668
669        plot.create_plot(label_fontsize=c['label-fontsize'],
670                         xlabel=c['x-series'].name,
671                         ylabel=c['y-series'].name,
672                         xunit=c['x-series'].unit,
673                         yunit=c['y-series'].unit,
674                         marker_size=c['marker-size'],
675                         marker_shape=marker_shape,
676                         line_width=c['line-width'],
677                         line_style=line_style)
678
679        plot.finalise(filename=None)
680
681        # create the image again for thumbnail
682        thumbnail_plot = PlotXYGraph(title=self.title if c['title-fontsize'] > 0 else None,
683                           filename=filename_thumb,
684                           width=c['thumbnail-width'],
685                           height=c['thumbnail-height'],
686                           x=x,
687                           y=y,
688                           sid=dc['sid'],
689                           colour=c['marker-colour'],
690                           edgecolour=c['edge-colour'],
691                           x_axis_min=c['x-axis-min'],
692                           x_axis_max=c['x-axis-max'],
693                           y_axis_min=c['y-axis-min'],
694                           y_axis_max=c['y-axis-max'])
695
696        thumbnail_plot.create_plot(label_fontsize=c['label-fontsize'] - 1,
697                         xlabel=c['x-series'].name,
698                         ylabel=c['y-series'].name,
699                         xunit=c['x-series'].unit,
700                         yunit=c['y-series'].unit,
701                         marker_size=c['marker-size'],
702                         marker_shape=marker_shape,
703                         line_width=c['line-width'],
704                         line_style=line_style)
705
706        thumbnail_plot.finalise(filename=None)
707
708        # prepare per-datapoint statistics for the info page
709        datapoint_info = Table(headings=('Field Name',
710                                         'Number of datapoints',
711                                         'Min value',
712                                         'Max value'))
713        datapoint_info.append((c['x-series'].name, len(x), min(x), max(x)))
714        datapoint_info.append((c['y-series'].name, len(y), min(y), max(y)))
715
716        url = make_plot_url(datapoints=(
717            {'field': c['x-series']},
718            {'field': c['y-series']}),
719                plot_type='CORRELATION',
720                sid=dc['sid'],
721                sensing_start=dc['sensing_start'],
722                sensing_stop=dc['sensing_stop'])
723
724        document.append_figure(title=self.title,
725                               filename=filename_thumb,
726                               width=c['thumbnail-width'],
727                               height=c['thumbnail-height'],
728                               zoom_filename=filename,
729                               zoom_width=plot.width,
730                               zoom_height=plot.height,
731                               datapoint_info=datapoint_info,
732                               live_url=url,
733                               widget=self)