p3lib 1.1.108__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
p3lib/table_plot.py ADDED
@@ -0,0 +1,675 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import threading
5
+ import queue
6
+ import os
7
+ import tempfile
8
+
9
+ from time import sleep, time
10
+ from datetime import datetime
11
+
12
+ from p3lib.json_networking import JSONServer, JsonServerHandler
13
+ from p3lib.bokeh_gui import StatusBarWrapper, SingleAppServer, ReadOnlyTableWrapper
14
+
15
+ from bokeh.settings import PrioritizedSetting
16
+ from bokeh.settings import Settings as bokeh_settings
17
+ from bokeh.layouts import gridplot, row, column
18
+ from bokeh.plotting import figure, ColumnDataSource
19
+ from bokeh.palettes import Category20
20
+ from bokeh.models import HoverTool, Div
21
+ from bokeh.models.widgets.buttons import Button
22
+ from bokeh.models.widgets import TextInput
23
+ from bokeh.plotting import output_file, save
24
+
25
+ # Example code for generating plots from 2D table data is shown at the bottom of this file.
26
+
27
+ class PSError(Exception):
28
+ pass
29
+
30
+ class StaticPlotParams(object):
31
+ """@brief Holds plot parameters that do not change through the life of a single plot window."""
32
+
33
+ def __init__(self):
34
+ """@brief Constructor."""
35
+ self.windowTitle = "UNSET WINDOW TITLE" # The HTML page title for the GUI.
36
+ self.tableColNames = None # The names of all the columns in the plot table. Each column is displayed as a row plot in the GUI.
37
+ self.xAxisType = None # The type of X axis. Either Table2DPlotServer.FLOAT_X_AXIS_TYPE or Table2DPlotServer.DATETIME_X_AXIS_TYPE.
38
+ self.plotPanelHeight = 250 # The vertical size in pixels of each individual trace plot panel.
39
+ self.linePlot = True # If True then a line plot is displayed. If False then a scatter plot is displayed.
40
+ self.plotLineWidth = 2 # The width of the line in pixels when al ine plot is displayed.
41
+ self.scatterPlotDotSize = 8 # The size in pixels of the dot size when a scatter plot is displayed.
42
+ self.theme = "dark_minimal" # The Bokeh color theme used for the plots
43
+ self.xAxisName = None # The name of the X axis on all plots
44
+ self.singlePanel = False # If False each trace is displayed in it's own panel
45
+ # Each panel is one above the other.
46
+ self.htmlFile = None # The text that appears in the HTML file save text box.
47
+ self.disableResult = False # If True then the Result field on the right hand end of the trace is not displayed.
48
+ self.resultWidth = 50 # The width in pixels of the result table
49
+ self.resultTitle = "Result" # The title of the table where the result of the trace is displayed.
50
+
51
+ class Table2DPlotServer(object):
52
+
53
+ """@brief Provides a server that accepts JSON data in the form of a 2D table.
54
+ Each row of which contains one or more plot points in a time series.
55
+ The first column is assumed to be the time (float or dataetime)"""
56
+
57
+ DEFAULT_HOST = "localhost"
58
+ DEFAULT_PORT = 31927
59
+
60
+ # Valid dict keys for the data received by the server START
61
+ WINDOW_TTLE = "WINDOW_TTLE"
62
+ TABLE_COLUMNS = "TABLE_COLUMNS"
63
+ TABLE_ROW = "TABLE_ROW"
64
+ XAXIS_TYPE = "XAXIS_TYPE"
65
+ PLOT_PANEL_HEIGHT = "PLOT_PANEL_HEIGHT"
66
+ LINE_PLOT = "LINE_PLOT"
67
+ PLOT_LINE_WIDTH = "PLOT_LINE_WIDTH"
68
+ SCATTER_PLOT_DOT_SIZE = "SCATTER_PLOT_DOT_SIZE"
69
+ THEME = "THEME"
70
+ X_AXIS_NAME = "X_AXIS_NAME"
71
+ HTML_FILE = "HTML_FILE"
72
+ DISABLE_RESULT = "DISABLE_RESULT"
73
+ RESULT_WIDTH = "RESULT_WIDTH"
74
+ RESULT_TITLE = "RESULT_TITLE"
75
+ SET_RESULT = "SET_RESULT"
76
+ # Valid dict keys for the data received by the server END
77
+
78
+ # Dict keys for the data sent by the server START
79
+ ERROR = "ERROR"
80
+ # Dict keys for the data sent by the server END
81
+
82
+ FLOAT_X_AXIS_TYPE = 1
83
+ DATETIME_X_AXIS_TYPE = 2
84
+ VALID_X_AXIS_TYPES = (FLOAT_X_AXIS_TYPE, DATETIME_X_AXIS_TYPE)
85
+ VALID_THEMES = ("caliber", "dark_minimal", "light_minimal", "night_sky", "contrast")
86
+
87
+ @staticmethod
88
+ def GetTimeString(dateTimeInstance):
89
+ """@brief Get a string representation of a datetime instance.
90
+ @param dateTimeInstance The datetime instance to convert to a string."""
91
+ return dateTimeInstance.strftime("%d/%m/%Y-%H:%M:%S.%f")
92
+
93
+ def __init__(self):
94
+ """@brief Constructor."""
95
+ self._host = Table2DPlotServer.DEFAULT_HOST
96
+ self._port = Table2DPlotServer.DEFAULT_PORT
97
+ self._server = None
98
+ self._bokeh2DTablePlotter = None
99
+ self._bokehServerBasePort = Bokeh2DTablePlotter.BOKEH_SERVER_BASE_PORT
100
+ self.staticPlotParams = StaticPlotParams()
101
+
102
+ # Setters start
103
+
104
+ def setHost(self, host):
105
+ """@brief Set the host address for the server to bind to.
106
+ @param host The servers bind address."""
107
+ self._host = host
108
+
109
+ def setPort(self, port):
110
+ """@brief Set the port for the server to bind to.
111
+ @param port The servers bind port."""
112
+ self._port = port
113
+
114
+ # Setters end
115
+
116
+ class ServerSessionHandler(JsonServerHandler):
117
+ """@brief Inner class to handle connections to the server."""
118
+ def handle(self):
119
+ errorDict = None
120
+ try:
121
+ while True:
122
+
123
+ # Set static parameters start
124
+
125
+ rxDict = self.rx()
126
+ if Table2DPlotServer.WINDOW_TTLE in rxDict:
127
+ wTitle = rxDict[Table2DPlotServer.WINDOW_TTLE]
128
+ if isinstance(wTitle,str) and len(wTitle) > 0 and len(wTitle) <= 250:
129
+ self.server.parent.staticPlotParams.windowTitle = wTitle
130
+ else:
131
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.WINDOW_TTLE} must be > 0 and <= 250 characters in length: {wTitle}"}
132
+
133
+ elif Table2DPlotServer.XAXIS_TYPE in rxDict:
134
+ xAxisType = rxDict[Table2DPlotServer.XAXIS_TYPE]
135
+ if xAxisType in Table2DPlotServer.VALID_X_AXIS_TYPES:
136
+ self.server.parent.staticPlotParams.xAxisType = xAxisType
137
+ else:
138
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.XAXIS_TYPE} invalid: {xAxisType}"}
139
+
140
+
141
+ elif Table2DPlotServer.PLOT_PANEL_HEIGHT in rxDict:
142
+ pHeight = rxDict[Table2DPlotServer.PLOT_PANEL_HEIGHT]
143
+ if pHeight > 10 and pHeight < 2048:
144
+ self.server.parent.staticPlotParams.plotPanelHeight = pHeight
145
+ else:
146
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.PLOT_PANEL_HEIGHT} invalid: {pHeight} must be > 10 and < 2048"}
147
+
148
+ elif Table2DPlotServer.LINE_PLOT in rxDict:
149
+ lPlot = rxDict[Table2DPlotServer.LINE_PLOT]
150
+ if lPlot in (True, False):
151
+ self.server.parent.staticPlotParams.linePlot = lPlot
152
+ else:
153
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.LINE_PLOT} invalid, must be True of False not {lPlot}"}
154
+
155
+ elif Table2DPlotServer.PLOT_LINE_WIDTH in rxDict:
156
+ plWidth = rxDict[Table2DPlotServer.PLOT_LINE_WIDTH]
157
+ if plWidth > 0 and plWidth < 100:
158
+ self.server.parent.staticPlotParams.plotLineWidth = plWidth
159
+ else:
160
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.PLOT_LINE_WIDTH} invalid, must be > 0 and < 100 not {plWidth}"}
161
+
162
+ elif Table2DPlotServer.SCATTER_PLOT_DOT_SIZE in rxDict:
163
+ spDotSize = rxDict[Table2DPlotServer.SCATTER_PLOT_DOT_SIZE]
164
+ if spDotSize > 0 and spDotSize < 250:
165
+ self.server.parent.staticPlotParams.scatterPlotDotSize = spDotSize
166
+ else:
167
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.SCATTER_PLOT_DOT_SIZE} invalid, must be > 0 and < 250 not {spDotSize}"}
168
+
169
+ elif Table2DPlotServer.THEME in rxDict:
170
+ theme = rxDict[Table2DPlotServer.THEME]
171
+ if theme in Table2DPlotServer.VALID_THEMES:
172
+ self.server.parent.staticPlotParams.theme = theme
173
+ else:
174
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.XAXIS_TYPE} invalid: {theme}"}
175
+
176
+ elif Table2DPlotServer.X_AXIS_NAME in rxDict:
177
+ xAxisName = rxDict[Table2DPlotServer.X_AXIS_NAME]
178
+ if isinstance(xAxisName,str) and len(xAxisName) > 0 and len(xAxisName) <= 150:
179
+ self.server.parent.staticPlotParams.xAxisName = xAxisName
180
+ else:
181
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.X_AXIS_NAME} must be > 0 and <= 150 characters in length: {xAxisName}"}
182
+
183
+ elif Table2DPlotServer.HTML_FILE in rxDict:
184
+ htmlFile = rxDict[Table2DPlotServer.HTML_FILE]
185
+ if isinstance(htmlFile,str) and len(htmlFile) > 0 and len(htmlFile) <= 250:
186
+ self.server.parent.staticPlotParams.htmlFile = htmlFile
187
+ else:
188
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.HTML_FILE} must be > 0 and <= 250 characters in length: {htmlFile}"}
189
+
190
+ elif Table2DPlotServer.DISABLE_RESULT in rxDict:
191
+ disableResult = rxDict[Table2DPlotServer.DISABLE_RESULT]
192
+ if disableResult in (True, False):
193
+ self.server.parent.staticPlotParams.disableResult = disableResult
194
+ else:
195
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.DISABLE_RESULT} invalid, must be True of False not {disableResult}"}
196
+
197
+ elif Table2DPlotServer.RESULT_WIDTH in rxDict:
198
+ resultWidth = rxDict[Table2DPlotServer.RESULT_WIDTH]
199
+ if resultWidth > 10 and resultWidth < 2048:
200
+ self.server.parent.staticPlotParams.resultWidth = resultWidth
201
+ else:
202
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.RESULT_WIDTH} invalid: {resultWidth} must be > 10 and < 2048"}
203
+
204
+ elif Table2DPlotServer.RESULT_TITLE in rxDict:
205
+ resultTitle = rxDict[Table2DPlotServer.RESULT_TITLE]
206
+ if isinstance(resultTitle,str) and len(resultTitle) > 0 and len(resultTitle) <= 250:
207
+ self.server.parent.staticPlotParams.resultTitle = resultTitle
208
+ else:
209
+ errorDict = {Table2DPlotServer.ERROR: f"{Table2DPlotServer.RESULT_TITLE} must be > 0 and <= 250 characters in length: {resultTitle}"}
210
+
211
+ # Set static parameters stop
212
+
213
+ elif Table2DPlotServer.SET_RESULT in rxDict:
214
+ guiMsg = GUIMessage()
215
+ guiMsg.type = GUIMessage.RESULT_DATA_TYPE
216
+ guiMsg.data = rxDict[Table2DPlotServer.SET_RESULT]
217
+ if self.server.parent._bokeh2DTablePlotter:
218
+ self.server.parent._bokeh2DTablePlotter.sendMessage(guiMsg)
219
+
220
+ elif Table2DPlotServer.TABLE_COLUMNS in rxDict:
221
+ self.server.parent.createNewPlot(rxDict[Table2DPlotServer.TABLE_COLUMNS])
222
+
223
+ elif Table2DPlotServer.TABLE_ROW in rxDict:
224
+ self.server.parent.updatePlot(rxDict[Table2DPlotServer.TABLE_ROW])
225
+
226
+ else:
227
+ errorDict = {Table2DPlotServer.ERROR: f"{rxDict} contains no valid keys to be processed."}
228
+
229
+ if errorDict:
230
+ self.tx(self.request, errorDict)
231
+
232
+ except:
233
+ # PJA TODO show error
234
+ raise
235
+
236
+ def info(self, msg):
237
+ print(f"INFO: {msg}")
238
+
239
+ def createNewPlot(self, tableColList):
240
+ """@brief Create a new plot. Each trace will be a table column.
241
+ @param tableColList The list of 2D table columns."""
242
+ if len(tableColList) < 2:
243
+ raise PSError("BUG: _createNewPlot() called with a list of less than 2 columns.")
244
+ self.staticPlotParams.tableColNames = tableColList
245
+
246
+ if self.staticPlotParams.xAxisType not in Table2DPlotServer.VALID_X_AXIS_TYPES:
247
+ raise PSError(f"BUG: _createNewPlot() called before {Table2DPlotServer.XAXIS_TYPE} set.")
248
+
249
+ # Supress bokeh some bokeh warnings to reduce chatter on stdout/err
250
+ #bokeh_settings.log_level = PrioritizedSetting("log_level", "BOKEH_LOG_LEVEL", default="fatal", dev_default="debug")
251
+ bokeh_settings.log_level = PrioritizedSetting("log_level", "BOKEH_LOG_LEVEL", default="warn", dev_default="warn")
252
+ if not self._bokeh2DTablePlotter or (self._iptGUI and not self._options.overlay):
253
+ self.info("Opening a web browser window.")
254
+ self._bokehServerPort = SingleAppServer.GetNextUnusedPort(basePort=self._bokehServerBasePort+1)
255
+ self._bokeh2DTablePlotter = Bokeh2DTablePlotter(self.staticPlotParams.windowTitle, self._bokehServerPort)
256
+ self._bokeh2DTablePlotter.setStaticPlotParams(self.staticPlotParams)
257
+ self._bokeh2DTablePlotter.runNonBlockingBokehServer(self._bokeh2DTablePlotter.app)
258
+ # Allow a short while for the server to start.
259
+ sleep(0.25)
260
+
261
+ def updatePlot(self, rowValueList):
262
+ """@brief Update a plot. Each trace will be a table column.
263
+ @param rowData A list of each value to be plotted."""
264
+ self._rowValueList = rowValueList
265
+ if len(self.staticPlotParams.tableColNames) != len(self._rowValueList):
266
+ raise PSError(f"BUG: _updatePlot() called with a list that is not equal to the number of table columns ({self.staticPlotParams.tableColNames}/{self._rowValueList}).")
267
+
268
+ guiMessage = GUIMessage()
269
+ guiMessage.type = GUIMessage.TABLE_DATA_TYPE
270
+ guiMessage.data = rowValueList
271
+ self._bokeh2DTablePlotter.sendMessage(guiMessage)
272
+
273
+ def start(self, blocking=False):
274
+ """@brief Start the server running.
275
+ @param blocking IF True then the server blocks."""
276
+ self._server = JSONServer((self._host, self._port), Table2DPlotServer.ServerSessionHandler)
277
+ self._server.parent = self # Set parent to give inner class access to this classes instance
278
+ self._server = threading.Thread(target=self._server.serve_forever)
279
+ if not blocking:
280
+ self._server.setDaemon(True)
281
+ self._server.start()
282
+
283
+ class GUIMessage(object):
284
+ """@brief Contains the messages passed to the GUI message queue."""
285
+
286
+ TABLE_DATA_TYPE = "TDATA"
287
+ RESULT_DATA_TYPE = "RDATA"
288
+
289
+ def __init__(self):
290
+ self.type = None
291
+ self.data = None
292
+
293
+ def __repr__(self):
294
+ """@brief Return this instance state as a string."""
295
+ return f"type: {self.type}, data {self.data}"
296
+
297
+ class Bokeh2DTablePlotter(SingleAppServer):
298
+ UPDATE_MSEC = 100
299
+ MAX_GUI_BLOCK_SECONDS = 1.0
300
+ BOKEH_SERVER_BASE_PORT = 36000
301
+ TOOLS = "box_select,box_zoom,lasso_select,pan,xpan,ypan,poly_select,tap,wheel_zoom,xwheel_zoom,ywheel_zoom,xwheel_pan,ywheel_pan,examine,undo,redo,reset,save,xzoom_in,xzoom_out,yzoom_in,yzoom_out,crosshair"
302
+
303
+ @staticmethod
304
+ def GetColumnDataSource():
305
+ """@return the expected column data source."""
306
+ return ColumnDataSource({'name': [], 'x': [], 'y': []})
307
+
308
+ @staticmethod
309
+ def GetTimeValue(xValue):
310
+ """@brief Get the X value time as either a float value or a datetime instance converted from a string."""
311
+ if isinstance(xValue, str):
312
+ timeValue = datetime.strptime(xValue, "%d/%m/%Y-%H:%M:%S.%f")
313
+ else:
314
+ timeValue = float(xValue)
315
+ return timeValue
316
+
317
+
318
+ def __init__(self, docTitle, bokehServerPort):
319
+ """@Constructor.
320
+ @param docTitle The title of the HTML doc page.
321
+ @param bokehServerPort The TCP port to bind the server to."""
322
+ super().__init__(bokehPort=bokehServerPort)
323
+ self.docTitle = docTitle
324
+ self._guiInitComplete = False
325
+ self._guiTable = [[]] # A two dimensional table that holds the GUI components.
326
+ self._msgQueue = queue.Queue() # Queue through which messages pass into the GUI thread.
327
+ self._plotColumnDataSourceList = None
328
+ self._plotFigureList = None
329
+ self._plotFigureList = None
330
+ self._plotColorIndex = 3
331
+ self._pColor = None
332
+ self._resultTableList = []
333
+
334
+ self._newPlotColor()
335
+
336
+ # Setters start
337
+
338
+ def setStaticPlotParams(self, staticPlotParams):
339
+ """@brief Set table columnm names. This must be called before runNonBlockingBokehServer() is called.
340
+ @param staticPlotParams All the parameters that may be set which do not change through the life of the plot."""
341
+ self._staticPlotParams = staticPlotParams
342
+
343
+ # Setters stop
344
+
345
+ def sendMessage(self, msgDict):
346
+ """@brief Send a message to the GUI thread.
347
+ @brief msgDict The dict holding the message t be sent to the GUI."""
348
+ self._msgQueue.put(msgDict)
349
+
350
+ def app(self, doc):
351
+ """@brief create the app to run in the bokeh server.
352
+ @param doc The document to add the plot to."""
353
+ self._doc = doc
354
+ self._doc.title = self.docTitle
355
+ self._doc.theme = self._staticPlotParams.theme
356
+ # The status bar is added to the bottom of the window showing status information.
357
+ self._statusBar = StatusBarWrapper()
358
+ self._doc.add_periodic_callback(self._updateGUI, Bokeh2DTablePlotter.UPDATE_MSEC)
359
+
360
+ def _updateGUI(self):
361
+ """@brief Called to update the state of the GUI."""
362
+ try:
363
+ self._update()
364
+ except:
365
+ # PJA Handle exception here
366
+ raise
367
+
368
+ def _getSaveHTMLComponment(self):
369
+ """@brief Get a component to be used to save plots to an HTML file."""
370
+ saveButton = Button(label="Save HTML File", button_type="success", width=50)
371
+ saveButton.on_click(self._savePlot)
372
+
373
+ self.fileToSave = TextInput()
374
+
375
+ #If the HTML file has been defined then use this
376
+ if self._staticPlotParams.htmlFile:
377
+ self.fileToSave.value = self._staticPlotParams.htmlFile
378
+ else:
379
+ # else set default output file name
380
+ self.fileToSave.value = os.path.join( tempfile.gettempdir(), "result.html" )
381
+
382
+ return row(self.fileToSave, saveButton)
383
+
384
+ def _savePlot(self):
385
+ """@brief Save an html file with the current GUI state."""
386
+ try:
387
+ htmlFile = self.fileToSave.value
388
+ if len(htmlFile) > 0:
389
+ fileBasename = os.path.basename(htmlFile)
390
+ filePath = htmlFile.replace(fileBasename, "")
391
+ if not htmlFile.endswith(".html"):
392
+ htmlFile = htmlFile + ".html"
393
+ if os.path.isdir(filePath):
394
+ if os.access(filePath, os.W_OK):
395
+ msg = "Saving {}. Please wait...".format(htmlFile)
396
+ self._setStatus(msg)
397
+ output_file(filename=htmlFile, title="Static HTML file")
398
+ save(self._doc)
399
+ self._setStatus( "Saved {}".format(htmlFile) )
400
+ else:
401
+ self._setStatus( "{} exists but no write access.".format(filePath) )
402
+ else:
403
+ self._setStatus( "{} path not found.".format(filePath) )
404
+ else:
405
+ self._setStatus("Please enter the html file to save.")
406
+
407
+ except Exception as ex:
408
+ self._setStatus( str(ex) )
409
+
410
+ def _setStatus(self, msg):
411
+ """@brief Show a status message in the GUI.
412
+ @param msg The message to show."""
413
+ self._statusBar.setStatus(msg)
414
+
415
+ def _update(self):
416
+ """@Called periodically to update the GUI state."""
417
+ callTime = time()
418
+ # If the GUI layout is not yet complete
419
+ if not self._guiInitComplete:
420
+ self._addPlots()
421
+ hmtlSaveRow = self._getSaveHTMLComponment()
422
+ self._guiTable.append([hmtlSaveRow])
423
+ # Put the status bar below all the traces.
424
+ self._guiTable.append([self._statusBar.getWidget()])
425
+ gp = gridplot( children = self._guiTable, sizing_mode='stretch_width', toolbar_location="below", merge_tools=True)
426
+ self._doc.add_root( gp )
427
+ self._setStatus("GUI init complete.")
428
+ self._guiInitComplete = True
429
+
430
+ #While we have data in the queue to process
431
+ while not self._msgQueue.empty():
432
+ #Don't block the bokeh thread for to long or it will crash.
433
+ if time() > callTime+Bokeh2DTablePlotter.MAX_GUI_BLOCK_SECONDS:
434
+ self._uio.debug("Quit _update() with {} outstanding messages after {:.1f} seconds.".format( self._plotDataQueue.qsize(), time()-callTime ))
435
+ break
436
+
437
+ msgReceived = self._msgQueue.get()
438
+ if msgReceived and msgReceived.type == GUIMessage.TABLE_DATA_TYPE:
439
+ self._processPlotPoint(msgReceived.data)
440
+
441
+ elif msgReceived and msgReceived.type == GUIMessage.RESULT_DATA_TYPE:
442
+ self._processResult(msgReceived.data)
443
+
444
+ def _getResultTable(self):
445
+ """@brief Get a table to contain the result data.
446
+ @return The table widget."""
447
+ resultTable = ReadOnlyTableWrapper(["results"], showLastRows=-1)
448
+ resultTable.getWidget().width=self._staticPlotParams.resultWidth
449
+ resultTable.getWidget().sizing_mode = 'stretch_height'
450
+ resultTable.getWidget().header_row=False
451
+ div = Div(text = self._staticPlotParams.resultTitle, name = "bokeh_div", styles={'font-weight': 'bold'})
452
+ # Add the title div above the table
453
+ titledTable = column(div, resultTable.getWidget(), sizing_mode="stretch_height")
454
+ self._resultTableList.append(resultTable)
455
+ return titledTable
456
+
457
+ def _addPlots(self):
458
+ """@brief Display all the plots to be displayed."""
459
+ self._plotColumnDataSourceList = []
460
+ self._plotFigureList = []
461
+ if self._staticPlotParams.tableColNames is None:
462
+ raise PSError("BUG: self._staticPlotParams.tableColNames is None")
463
+
464
+ # Add plot traces ignoring the first column as we expect this to be a float value or datetime.
465
+ for tableColName in self._staticPlotParams.tableColNames[1:]:
466
+ plotFigure = self._getPlotFigure(tableColName)
467
+ self._plotFigureList.append(plotFigure)
468
+ resultTable = self._getResultTable()
469
+ if self._staticPlotParams.disableResult:
470
+ # Don't add the result table to the GUI layout
471
+ tRow = row(plotFigure, height=self._staticPlotParams.plotPanelHeight)
472
+ else:
473
+ tRow = row(plotFigure, resultTable, height=self._staticPlotParams.plotPanelHeight)
474
+
475
+ self._guiTable.append([tRow])
476
+
477
+ def _getPlotFigure(self, tableColName):
478
+ """@brief Create a plot figure.
479
+ @param tableColName The name of the trace.
480
+ @return The figure instance."""
481
+ # Add the hover menu for each plot point
482
+ if self._staticPlotParams.xAxisType == Table2DPlotServer.FLOAT_X_AXIS_TYPE:
483
+ tooltips=[
484
+ ("name", "@name"),
485
+ ("Seconds", "@x{0.0}"),
486
+ (tableColName, "@y{0.0}"),
487
+ ]
488
+ xAxisName = "Seconds" # We defailt the X axis name to seconds.
489
+ xAxisType = 'auto'
490
+
491
+ elif self._staticPlotParams.xAxisType == Table2DPlotServer.DATETIME_X_AXIS_TYPE:
492
+ tooltips=[
493
+ ("name", "@name"),
494
+ ('date', "$x{%Y-%m-%d}"),
495
+ ('time', "$x{%H:%M:%S}"),
496
+ (tableColName, "@y{0.0}"),
497
+ ]
498
+ xAxisName = "Time"
499
+ xAxisType = 'datetime'
500
+
501
+ else:
502
+ raise PSError(f"BUG: Invalid X Axis type set ({self._staticPlotParams.xAxisType}).")
503
+
504
+ #If the x axis name is set this overrides the name associated with the x axis type.
505
+ if self._staticPlotParams.xAxisName:
506
+ xAxisName = self._staticPlotParams.xAxisName
507
+
508
+ # Set up plot/figure attributes.
509
+ plot = figure(title="", # Don't set the title as the Y axis has the title
510
+ sizing_mode = 'stretch_both',
511
+ x_axis_label=xAxisName,
512
+ y_axis_label=tableColName,
513
+ tools=Bokeh2DTablePlotter.TOOLS,
514
+ active_drag="box_zoom",
515
+ x_axis_type=xAxisType)
516
+
517
+ # For sliding window of displayed values
518
+ # plot.x_range.follow = "end"
519
+ # from datetime import timedelta
520
+ # plot.x_range.follow_interval = 200
521
+ # timedelta(seconds=50)
522
+
523
+ source = Bokeh2DTablePlotter.GetColumnDataSource()
524
+ self._plotColumnDataSourceList.append( source )
525
+
526
+ if self._staticPlotParams.linePlot:
527
+ plot.line('x', 'y', source=source, line_width=self._staticPlotParams.plotLineWidth, color=self._pColor)
528
+ else:
529
+ plot.circle('x', 'y', source=source, size=self._staticPlotParams.scatterPlotDotSize, color=self._pColor)
530
+
531
+ if self._staticPlotParams.xAxisType == Table2DPlotServer.FLOAT_X_AXIS_TYPE:
532
+ plot.add_tools(HoverTool(
533
+ tooltips=tooltips
534
+ ))
535
+
536
+ else:
537
+ formatters = {'$x': 'datetime'}
538
+ plot.add_tools(HoverTool(
539
+ tooltips=tooltips,
540
+ formatters=formatters
541
+ ))
542
+
543
+ #self._newPlotColor()
544
+
545
+ return plot
546
+
547
+ def _newPlotColor(self):
548
+ """@brief Allocate a new color for the plot trace."""
549
+ # Use the next color for the next plot
550
+ self._plotColorIndex += 1
551
+ if self._plotColorIndex > 20:
552
+ self._plotColorIndex = 3
553
+ self._pColor = Category20[20][self._plotColorIndex]
554
+
555
+ def _processPlotPoint(self, plotPointList):
556
+ """@brief Update a single plot point for each trace displayed."""
557
+ xValue = Bokeh2DTablePlotter.GetTimeValue(plotPointList[0])
558
+ colNumber = 1
559
+ for plotPoint in plotPointList[1:]:
560
+ plotPoint= {'name': [self._staticPlotParams.tableColNames[colNumber]],
561
+ 'x': [xValue],
562
+ 'y': [plotPointList[colNumber]] }
563
+
564
+ if colNumber < len(self._plotColumnDataSourceList)+1:
565
+ self._plotColumnDataSourceList[colNumber-1].stream(plotPoint)
566
+
567
+ colNumber += 1
568
+
569
+ def _processResult(self, resultList):
570
+ """@brief Process a final result message.
571
+ @param resultList A list of the final results for each column in the table."""
572
+ plotIndex = 0
573
+ for resultTable in self._resultTableList:
574
+ resultTable.setRows( [ [resultList[plotIndex]] ] )
575
+ plotIndex += 1
576
+
577
+
578
+
579
+
580
+ """
581
+ # Example code. This shows how 2D table data may be plotted.
582
+
583
+ #!/usr/bin/env python
584
+ # -*- coding: utf-8 -*-
585
+
586
+ from time import sleep
587
+ from datetime import datetime
588
+
589
+ from p3lib.table_plot import Table2DPlotServer
590
+ from p3lib.json_networking import JSONClient
591
+
592
+ # This starts the server in the background waiting for the data to be plotted.
593
+ table2DPlotServer = Table2DPlotServer()
594
+ table2DPlotServer.start()
595
+
596
+ # Create the client to talk to the above server and send parameters and table data
597
+ client = JSONClient(Table2DPlotServer.DEFAULT_HOST, Table2DPlotServer.DEFAULT_PORT)
598
+
599
+ # Set the X Axis type
600
+ # Set X AXIS as a float value values
601
+ xAxisTypeDict = {Table2DPlotServer.XAXIS_TYPE: Table2DPlotServer.FLOAT_X_AXIS_TYPE}
602
+ # Set X Axis as datetime values
603
+ #xAxisTypeDict = {Table2DPlotServer.XAXIS_TYPE: Table2DPlotServer.DATETIME_X_AXIS_TYPE}
604
+ client.tx(xAxisTypeDict)
605
+
606
+ # Set height of each trace panel to 500 pixels (default = 250)
607
+ #paramDict = {Table2DPlotServer.PLOT_PANEL_HEIGHT: 500}
608
+ #client.tx(paramDict)
609
+
610
+ # Set the plot line with to 10 pixels rather than the default of 10
611
+ #paramDict = {Table2DPlotServer.PLOT_LINE_WIDTH: 10}
612
+ #client.tx(paramDict)
613
+
614
+ # Set scatter plot rather than the default line plot
615
+ #paramDict = {Table2DPlotServer.LINE_PLOT: False}
616
+ #client.tx(paramDict)
617
+
618
+ # Set scatter plot dot size rather than the default of 8
619
+ #paramDict = {Table2DPlotServer.SCATTER_PLOT_DOT_SIZE: 20}
620
+ #client.tx(paramDict)
621
+
622
+ # Set the color theme for the plot
623
+ # Valid strings are caliber,dark_minimal,light_minimal,night_sky,contrast
624
+ #paramDict = {Table2DPlotServer.THEME: "night_sky"}
625
+ #client.tx(paramDict)
626
+
627
+ # Force the X axis name.
628
+ # If not set and XAXIS_TYPE = SECONDS_X_AXIS_TYPE the # x axis will be Seconds.
629
+ # If not set and XAXIS_TYPE = DATETIME_X_AXIS_TYPE the # x axis will be Time".
630
+ #paramDict = {Table2DPlotServer.X_AXIS_NAME: "ABCDEF"}
631
+ #client.tx(paramDict)
632
+
633
+ # Set the html file
634
+ #paramDict = {Table2DPlotServer.HTML_FILE: "/home/auser/result.html"}
635
+ #client.tx(paramDict)
636
+
637
+ # Remove the result table from the GUI.
638
+ #paramDict = {Table2DPlotServer.DISABLE_RESULT: True}
639
+ #client.tx(paramDict)
640
+
641
+ # Set the width (in pixels) of the result table.
642
+ # If set to small then it will expand to fit the the text.
643
+ #paramDict = {Table2DPlotServer.RESULT_WIDTH: 10}
644
+ #client.tx(paramDict)
645
+
646
+ # Set the title of the result table. By default this is result.
647
+ #paramDict = {Table2DPlotServer.RESULT_TITLE: "FINAL RESULT"}
648
+ #client.tx(paramDict)
649
+
650
+
651
+ titleDict = {Table2DPlotServer.WINDOW_TTLE: "An example Plot"}
652
+ client.tx(titleDict)
653
+
654
+ headerDict = {Table2DPlotServer.TABLE_COLUMNS: ["Seconds","Value 1","Value 2", "V3", "V4"]}
655
+ client.tx(headerDict)
656
+
657
+ for seconds in range(1,100):
658
+ # Add a plot point for each trace with the X axis as seconds
659
+ rowDict = {Table2DPlotServer.TABLE_ROW: [seconds, seconds+10, seconds*2, seconds*30, seconds/10.0]}
660
+
661
+ # Use this when setting X axis as datetime rather than seconds values.
662
+ # timeStr = Table2DPlotServer.GetTimeString(datetime.now())
663
+ # rowDict = {Table2DPlotServer.TABLE_ROW: [timeStr, seconds+10, seconds*2]}
664
+ client.tx(rowDict)
665
+ sleep(.02)
666
+
667
+ # Set the final result in the table on the right.
668
+ resultDict = {Table2DPlotServer.SET_RESULT: [1.1,2.2,3.3,4.4]}
669
+ client.tx(resultDict)
670
+
671
+ # Stay running to keep server running or saving an HTML file will not work.
672
+ while True:
673
+ sleep(1)
674
+
675
+ """