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)