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/__init__.py +0 -0
- p3lib/ate.py +108 -0
- p3lib/bokeh_auth.py +363 -0
- p3lib/bokeh_gui.py +845 -0
- p3lib/boot_manager.py +420 -0
- p3lib/conduit.py +145 -0
- p3lib/database_if.py +289 -0
- p3lib/file_io.py +154 -0
- p3lib/gnome_desktop_app.py +146 -0
- p3lib/helper.py +420 -0
- p3lib/json_networking.py +239 -0
- p3lib/login.html +98 -0
- p3lib/mqtt_rpc.py +240 -0
- p3lib/netif.py +226 -0
- p3lib/netplotly.py +223 -0
- p3lib/ngt.py +841 -0
- p3lib/pconfig.py +874 -0
- p3lib/ssh.py +935 -0
- p3lib/table_plot.py +675 -0
- p3lib/uio.py +574 -0
- p3lib-1.1.108.dist-info/LICENSE +21 -0
- p3lib-1.1.108.dist-info/METADATA +34 -0
- p3lib-1.1.108.dist-info/RECORD +24 -0
- p3lib-1.1.108.dist-info/WHEEL +4 -0
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
|
+
"""
|