Source code for shyft.dashboard.time_series.tools.table_tools
import abc
from typing import Any, Optional, List, Dict
import bokeh
from bokeh.layouts import column
from bokeh.models import Button, PreText, ColumnDataSource, CustomJS
from shyft.dashboard.base import constants
from shyft.dashboard.base.app import Widget, LayoutComponents
from shyft.dashboard.time_series.tools.base import BaseTool
[docs]
class TableTool(BaseTool):
"""
This object represents the base class of all figure tools
"""
[docs]
def __init__(self, logger=None):
"""
Parameters
----------
logger:
optional logger
"""
super().__init__(logger=logger)
[docs]
class ExportTableDataButton(TableTool, Widget):
"""
Tool to download all data shown in all figures where this tool is attached to!
Two obstacles:
- each obj/renderer in a figure has its own data source with own amount of time, value tuples
- js callbacks and python callbacks in bokeh are not sync
Therefore:
- for each renderer one separate csv file is downloaded
- we create a bokeh PreText obj in addition to the download button, to which we attach a js callback for the download
after the download the callback is removed again
"""
[docs]
def __init__(self,
label: str = 'Download Table Data',
height: int = 50,
width: int = 150,
padding: Optional[int] = None,
sizing_mode: Optional[str] = None,
logger=None):
"""
Parameters
----------
logger:
optional logger
"""
TableTool.__init__(self, logger=logger)
Widget.__init__(self, logger=logger)
padding = padding or constants.widget_padding
sizing_mode = sizing_mode or constants.sizing_mode
self.download_button = Button(label=label, width=width, height=height)
self.download_button.on_click(self.on_click)
self.download_text = PreText(text="")
self._layout = column(self.download_text, self.download_button, width=width + padding, height=height,
sizing_mode=sizing_mode)
[docs]
def on_click(self) -> None:
sources = []
names = []
column_name_maps = []
dummy_dict_for_sorting = []
for parent in self.parents:
sources.append(parent.bokeh_data_source)
names.append(parent.title)
column_name_maps.append({tc.field: tc.title for tc in parent.table_columns.values()})
dummy_dict_for_sorting.append({str(i): tc.field for i, tc in enumerate(parent.table_columns.values())})
# add js callback
self.download_text.js_on_change("text", self.js_callback(sources=sources,
names=names,
column_name_maps=column_name_maps,
map_of_name_map=dummy_dict_for_sorting))
# trigger js callback
self.download_text.text = 'downloading'
# remove js callback
for k, v in self.download_text.js_property_callbacks.items():
if 'text' in k:
self.download_text.js_property_callbacks[k] = []
# reset download text
self.download_text.text = ''
[docs]
@staticmethod
def js_callback(*,
sources: List[ColumnDataSource],
names: List[str],
column_name_maps: List[Dict[str, str]],
map_of_name_map: List[Dict[str, str]]) -> str:
"""Creates js to download all data from multiple data sources to multiple csv files"""
java_script = """
function table_to_csv(source, column_name_map, map_of_name_map) {
/*
Depending on the browser used, the keys of an Object may or may not be sorted alphabetically/numerically.
To ensure that the columns in the downloaded csv file are ordered as the user sees the table in the app,
we use keys 0,1,2,3, ... (in map_of_name_map) to extract the real keys needed to get match the proper titles
of the columns with the data of the columns.
*/
const sort_keys = Object.keys(map_of_name_map)
const nrows = source.get_length()
let row = []
for (let j = 0; j < sort_keys.length; j++) {
const column = map_of_name_map[sort_keys[j]]
console.log(column)
row.push(column_name_map[column].toString())
}
const lines = [row.join(';')]
for (let i = 0; i < nrows; i++) {
let multi = false;
let nrows_multi = 0;
for (let j = 0; j < sort_keys.length; j++) {
const column = map_of_name_map[sort_keys[j]]
if (Array.isArray(source.data[column][i])) {
multi = true
nrows_multi = source.data[column][i].length
break
}
}
if (multi) {
for (let k = 0; k < nrows_multi; k++) {
let row = []
for (let j = 0; j < sort_keys.length; j++) {
const column = map_of_name_map[sort_keys[j]]
let data = source.data[column][i]
if (Array.isArray(data) || typeof(data) === 'object'){
row.push(data[k].toString())
}
else {
row.push(data.toString())
}
}
lines.push(row.join(';'))
}
} else {
let row = [];
for (let j = 0; j < sort_keys.length; j++) {
const column = map_of_name_map[sort_keys[j]]
row.push(source.data[column][i].toString())
}
lines.push(row.join(';'))
}
}
return lines.join('\\n').concat('\\n')
}
for (var i = 0; i < sources.length; i++){
var source = sources[i]
var name = names[i]
var column_name_map = column_name_maps[i]
var map_of_name_map = map_of_name_map[i]
try {
const filename = "data-"+name+".csv";
var filetext = table_to_csv(source, column_name_map, map_of_name_map)
const blob = new Blob([filetext], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.target = '_blank'
link.style.visibility = 'hidden'
link.dispatchEvent(new MouseEvent('click'))
} catch (error) {
console.error(error)
}
}
"""
return CustomJS(args=dict(sources=sources, names=names, column_name_maps=column_name_maps, map_of_name_map=map_of_name_map), code=java_script)
@property
def layout_components(self) -> LayoutComponents:
return {'widgets': [self.download_text, self.download_button], 'figures': []}
@property
def layout(self) -> bokeh.models.LayoutDOM:
return self._layout