mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 22:01:42 +03:00
Database Visualizations (https://github.com/enso-org/ide/pull/1335)
Original commit: 132ae3e2fe
This commit is contained in:
parent
1205a5dcda
commit
6aeff8f8a4
@ -11,6 +11,11 @@
|
|||||||
will be lost. In this build we added notification in statusbar to signalize
|
will be lost. In this build we added notification in statusbar to signalize
|
||||||
that the connection was lost and IDE must be restarted. In future IDE will try
|
that the connection was lost and IDE must be restarted. In future IDE will try
|
||||||
to automatically reconnect.
|
to automatically reconnect.
|
||||||
|
- [Database Visualizations][1335]. Visualizations for the Database library have
|
||||||
|
been added. The Table visualization now automatically executes the underlying
|
||||||
|
query to display its results in a table. In addition, the SQL Query
|
||||||
|
visualization allows the user to see the query that is going to be run against
|
||||||
|
the database.
|
||||||
|
|
||||||
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
|
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
|
||||||
|
|
||||||
|
@ -20,6 +20,16 @@ pub fn table_visualization() -> visualization::java_script::FallibleDefinition {
|
|||||||
visualization::java_script::Definition::new_builtin(source)
|
visualization::java_script::Definition::new_builtin(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a `JavaScript` SQL visualization.
|
||||||
|
pub fn sql_visualization() -> visualization::java_script::FallibleDefinition {
|
||||||
|
let loading_scripts = include_str!("java_script/helpers/loading.js");
|
||||||
|
let scrollable = include_str!("java_script/helpers/scrollable.js");
|
||||||
|
let source = include_str!("java_script/sql.js");
|
||||||
|
let source = format!("{}{}{}",loading_scripts,scrollable,source);
|
||||||
|
|
||||||
|
visualization::java_script::Definition::new_builtin(source)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return a `JavaScript` Scatter plot visualization.
|
/// Return a `JavaScript` Scatter plot visualization.
|
||||||
pub fn scatter_plot_visualization() -> visualization::java_script::FallibleDefinition {
|
pub fn scatter_plot_visualization() -> visualization::java_script::FallibleDefinition {
|
||||||
let loading_scripts = include_str!("java_script/helpers/loading.js");
|
let loading_scripts = include_str!("java_script/helpers/loading.js");
|
||||||
|
@ -0,0 +1,440 @@
|
|||||||
|
/** SQL visualization. */
|
||||||
|
|
||||||
|
// =================
|
||||||
|
// === Constants ===
|
||||||
|
// =================
|
||||||
|
|
||||||
|
/** The qualified name of the Text type. */
|
||||||
|
const textType = 'Builtins.Main.Text'
|
||||||
|
|
||||||
|
/** The module prefix added for unknown SQL types. */
|
||||||
|
const customSqlTypePrefix = 'Standard.Database.Data.Sql.Sql_Type.'
|
||||||
|
|
||||||
|
/** Specifies opacity of interpolation background color. */
|
||||||
|
const interpolationBacgroundOpacity = 0.3
|
||||||
|
|
||||||
|
/** The CSS styles for the visualization. */
|
||||||
|
const visualizationStyle = `
|
||||||
|
<style>
|
||||||
|
.sql {
|
||||||
|
font-family: DejaVuSansMonoBook, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 7px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.interpolation {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding:1px 2px 1px 2px;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.mismatch-parent {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.mismatch-mouse-area {
|
||||||
|
display: inline;
|
||||||
|
position: absolute;
|
||||||
|
width: 150%;
|
||||||
|
height: 150%;
|
||||||
|
align-self: center;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.mismatch {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.modulepath {
|
||||||
|
color: rgba(150, 150, 150, 0.9);
|
||||||
|
}
|
||||||
|
.tooltip {
|
||||||
|
font-family: DejaVuSansMonoBook, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: rgba(249, 249, 249, 1);
|
||||||
|
box-shadow: 0 0 16px rgba(0, 0, 0, 0.16);
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// === Script Initialisation ===
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
loadScript('https://cdnjs.cloudflare.com/ajax/libs/sql-formatter/4.0.2/sql-formatter.min.js')
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// === Table visualization ===
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A visualization that pretty-prints generated SQL code and displays type hints related to
|
||||||
|
* interpolated query parameters.
|
||||||
|
*/
|
||||||
|
class SqlVisualization extends Visualization {
|
||||||
|
// TODO Change the type below once #837 is done:
|
||||||
|
// 'Standard.Database.Data.Table.Table | Standard.Database.Data.Column.Column'
|
||||||
|
static inputType = 'Any'
|
||||||
|
static label = 'SQL Query'
|
||||||
|
|
||||||
|
constructor(api) {
|
||||||
|
super(api)
|
||||||
|
this.setPreprocessorModule('Standard.Visualization.Sql.Visualization')
|
||||||
|
this.setPreprocessorCode(`x -> here.prepare_visualization x`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataReceived(data) {
|
||||||
|
this.removeAllChildren()
|
||||||
|
|
||||||
|
let parsedData = data
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
parsedData = JSON.parse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
let visHtml = visualizationStyle
|
||||||
|
if (parsedData.error !== undefined) {
|
||||||
|
visHtml += parsedData.error
|
||||||
|
} else {
|
||||||
|
const params = parsedData.interpolations.map(param =>
|
||||||
|
renderInterpolationParameter(this.theme, param)
|
||||||
|
)
|
||||||
|
|
||||||
|
let language = 'sql'
|
||||||
|
if (parsedData.dialect == 'postgresql') {
|
||||||
|
language = 'postgresql'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = sqlFormatter.format(parsedData.code, {
|
||||||
|
params: params,
|
||||||
|
language: language,
|
||||||
|
})
|
||||||
|
|
||||||
|
const codeRepresentation = '<pre class="sql">' + formatted + '</pre>'
|
||||||
|
visHtml += codeRepresentation
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = this.createContainers()
|
||||||
|
const parentContainer = containers.parent
|
||||||
|
containers.scrollable.innerHTML = visHtml
|
||||||
|
this.dom.appendChild(parentContainer)
|
||||||
|
|
||||||
|
const tooltip = new Tooltip(parentContainer)
|
||||||
|
const baseMismatches = this.dom.getElementsByClassName('mismatch')
|
||||||
|
const extendedMismatchAreas = this.dom.getElementsByClassName('mismatch-mouse-area')
|
||||||
|
setupMouseInteractionForMismatches(tooltip, baseMismatches)
|
||||||
|
setupMouseInteractionForMismatches(tooltip, extendedMismatchAreas)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all children of this visualization's DOM.
|
||||||
|
*
|
||||||
|
* May be used to reset the visualization's content.
|
||||||
|
*/
|
||||||
|
removeAllChildren() {
|
||||||
|
while (this.dom.firstChild) {
|
||||||
|
this.dom.removeChild(this.dom.lastChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates containers for the visualization.
|
||||||
|
*/
|
||||||
|
createContainers() {
|
||||||
|
const parentContainer = document.createElement('div')
|
||||||
|
parentContainer.setAttributeNS(null, 'style', 'position: relative;')
|
||||||
|
const width = this.dom.getAttributeNS(null, 'width')
|
||||||
|
const height = this.dom.getAttributeNS(null, 'height')
|
||||||
|
const scrollable = document.createElement('div')
|
||||||
|
scrollable.setAttributeNS(null, 'id', 'vis-sql-view')
|
||||||
|
scrollable.setAttributeNS(null, 'class', 'scrollable')
|
||||||
|
scrollable.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height)
|
||||||
|
scrollable.setAttributeNS(null, 'width', '100%')
|
||||||
|
scrollable.setAttributeNS(null, 'height', '100%')
|
||||||
|
const viewStyle = `width: ${width - 5}px;
|
||||||
|
height: ${height - 5}px;
|
||||||
|
overflow: scroll;
|
||||||
|
padding:2.5px;`
|
||||||
|
scrollable.setAttributeNS(null, 'style', viewStyle)
|
||||||
|
parentContainer.appendChild(scrollable)
|
||||||
|
return {
|
||||||
|
parent: parentContainer,
|
||||||
|
scrollable: scrollable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(size) {
|
||||||
|
this.dom.setAttributeNS(null, 'width', size[0])
|
||||||
|
this.dom.setAttributeNS(null, 'height', size[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Handling Colors ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a 4-element array representing a color into a CSS-compatible rgba string.
|
||||||
|
*/
|
||||||
|
function convertColorToRgba(color) {
|
||||||
|
const r = 255 * color.red
|
||||||
|
const g = 255 * color.green
|
||||||
|
const b = 255 * color.blue
|
||||||
|
const a = color.alpha
|
||||||
|
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replaces the alpha component of a color (represented as a 4-element array),
|
||||||
|
* returning a new color.
|
||||||
|
*/
|
||||||
|
function replaceAlpha(color, newAlpha) {
|
||||||
|
return {
|
||||||
|
red: color.red,
|
||||||
|
green: color.green,
|
||||||
|
blue: color.blue,
|
||||||
|
alpha: newAlpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === HTML Rendering Helpers ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a HTML representation of a message to be displayed in a tooltip,
|
||||||
|
* which explains a type mismatch.
|
||||||
|
*/
|
||||||
|
function renderTypeHintMessage(
|
||||||
|
receivedTypeName,
|
||||||
|
expectedTypeName,
|
||||||
|
receivedTypeColor,
|
||||||
|
expectedTypeColor
|
||||||
|
) {
|
||||||
|
const received = new QualifiedTypeName(receivedTypeName)
|
||||||
|
const expected = new QualifiedTypeName(expectedTypeName)
|
||||||
|
|
||||||
|
let receivedPrefix = ''
|
||||||
|
if (received.moduleName !== null) {
|
||||||
|
receivedPrefix = '<span class="modulepath">' + received.moduleName + '.</span>'
|
||||||
|
}
|
||||||
|
const receivedStyledSpan = '<span style="color: ' + convertColorToRgba(receivedTypeColor) + '">'
|
||||||
|
const receivedSuffix = receivedStyledSpan + received.name + '</span>'
|
||||||
|
|
||||||
|
let expectedPrefix = ''
|
||||||
|
if (expected.moduleName !== null) {
|
||||||
|
expectedPrefix = '<span class="modulepath">' + expected.moduleName + '.</span>'
|
||||||
|
}
|
||||||
|
const expectedStyledSpan = '<span style="color: ' + convertColorToRgba(expectedTypeColor) + '">'
|
||||||
|
const expectedSuffix = expectedStyledSpan + expected.name + '</span>'
|
||||||
|
|
||||||
|
let message = 'Received ' + receivedPrefix + receivedSuffix + '<br>'
|
||||||
|
message += 'Expected ' + expectedPrefix + expectedSuffix + '<br>'
|
||||||
|
message += 'The database may perform an auto conversion.'
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a qualified type name.
|
||||||
|
*
|
||||||
|
* The `moduleName` field is the name of the module the type is from. It may be null if the original
|
||||||
|
* type name did not contain a module name.
|
||||||
|
* The `name` field is the simple name of the type itself.
|
||||||
|
*/
|
||||||
|
class QualifiedTypeName {
|
||||||
|
/** Creates a QualifiedTypeName instance from a string representation. */
|
||||||
|
constructor(typeName) {
|
||||||
|
let ix = typeName.lastIndexOf('.')
|
||||||
|
if (ix < 0) {
|
||||||
|
this.moduleName = null
|
||||||
|
this.name = typeName
|
||||||
|
} else {
|
||||||
|
this.moduleName = typeName.substr(0, ix)
|
||||||
|
this.name = typeName.substr(ix + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders HTML for displaying an Enso parameter that is interpolated into the SQL code.
|
||||||
|
*/
|
||||||
|
function renderInterpolationParameter(theme, param) {
|
||||||
|
const actualType = param.actual_type
|
||||||
|
let value = param.value
|
||||||
|
|
||||||
|
if (actualType == textType) {
|
||||||
|
value = "'" + value.replaceAll("'", "''") + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualTypeColor = theme.getColorForType(actualType)
|
||||||
|
const fgColor = actualTypeColor
|
||||||
|
let bgColor = replaceAlpha(fgColor, interpolationBacgroundOpacity)
|
||||||
|
const expectedEnsoType = param.expected_enso_type
|
||||||
|
|
||||||
|
if (actualType == expectedEnsoType) {
|
||||||
|
return renderRegularInterpolation(value, fgColor, bgColor)
|
||||||
|
} else {
|
||||||
|
let expectedType = expectedEnsoType
|
||||||
|
if (expectedType === null) {
|
||||||
|
expectedType = customSqlTypePrefix + param.expected_sql_type
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTypeColor = theme.getColorForType(expectedType)
|
||||||
|
const hoverBgColor = expectedTypeColor
|
||||||
|
bgColor = replaceAlpha(hoverBgColor, interpolationBacgroundOpacity)
|
||||||
|
const hoverFgColor = theme.getForegroundColorForType(expectedType)
|
||||||
|
|
||||||
|
const message = renderTypeHintMessage(
|
||||||
|
actualType,
|
||||||
|
expectedType,
|
||||||
|
actualTypeColor,
|
||||||
|
expectedTypeColor
|
||||||
|
)
|
||||||
|
|
||||||
|
return renderMismatchedInterpolation(
|
||||||
|
value,
|
||||||
|
message,
|
||||||
|
fgColor,
|
||||||
|
bgColor,
|
||||||
|
hoverFgColor,
|
||||||
|
hoverBgColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper that renders the HTML representation of a regular SQL interpolation.
|
||||||
|
*/
|
||||||
|
function renderRegularInterpolation(value, fgColor, bgColor) {
|
||||||
|
let html =
|
||||||
|
'<div class="interpolation" style="color:' +
|
||||||
|
convertColorToRgba(fgColor) +
|
||||||
|
';background-color:' +
|
||||||
|
convertColorToRgba(bgColor) +
|
||||||
|
';">'
|
||||||
|
html += value
|
||||||
|
html += '</div>'
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper that renders the HTML representation of a type-mismatched SQL interpolation.
|
||||||
|
*
|
||||||
|
* This only prepares the HTML code, to setup the interactions, `setupMouseInteractionForMismatches`
|
||||||
|
* must be called after these HTML elements are added to the DOM.
|
||||||
|
*/
|
||||||
|
function renderMismatchedInterpolation(
|
||||||
|
value,
|
||||||
|
message,
|
||||||
|
fgColor,
|
||||||
|
bgColor,
|
||||||
|
hoverFgColor,
|
||||||
|
hoverBgColor
|
||||||
|
) {
|
||||||
|
let html = '<div class="mismatch-parent">'
|
||||||
|
html += '<div class="mismatch-mouse-area"></div>'
|
||||||
|
html += '<div class="interpolation mismatch"'
|
||||||
|
let style = 'color:' + convertColorToRgba(fgColor) + ';'
|
||||||
|
style += 'background-color:' + convertColorToRgba(bgColor) + ';'
|
||||||
|
html += ' style="' + style + '"'
|
||||||
|
html += ' data-fgColor="' + convertColorToRgba(fgColor) + '"'
|
||||||
|
html += ' data-bgColor="' + convertColorToRgba(bgColor) + '"'
|
||||||
|
html += ' data-fgColorHover="' + convertColorToRgba(hoverFgColor) + '"'
|
||||||
|
html += ' data-bgColorHover="' + convertColorToRgba(hoverBgColor) + '"'
|
||||||
|
html += ' data-message="' + encodeURIComponent(message) + '"'
|
||||||
|
html += '>'
|
||||||
|
html += value
|
||||||
|
html += '</div>'
|
||||||
|
html += '</div>'
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Tooltip ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hint tooltip that can be displayed above elements.
|
||||||
|
*/
|
||||||
|
class Tooltip {
|
||||||
|
constructor(container) {
|
||||||
|
this.tooltip = document.createElement('div')
|
||||||
|
this.tooltip.setAttributeNS(null, 'class', 'tooltip')
|
||||||
|
container.appendChild(this.tooltip)
|
||||||
|
this.tooltipOwner = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the tooltip.
|
||||||
|
*
|
||||||
|
* The actor parameter specifies who is initiating the hiding.
|
||||||
|
* If this method is called but the tooltip has got a new owner in the meantime, the call is
|
||||||
|
* ignored.
|
||||||
|
*/
|
||||||
|
hide(actor) {
|
||||||
|
if (this.tooltipOwner === null || this.tooltipOwner == actor) {
|
||||||
|
this.tooltipOwner = null
|
||||||
|
this.tooltip.style.opacity = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the tooltip above the element represented by `actor`.
|
||||||
|
*
|
||||||
|
* Tooltip content is specified by the `message` which can include arbitrary HTML.
|
||||||
|
*/
|
||||||
|
show(actor, message) {
|
||||||
|
this.tooltipOwner = actor
|
||||||
|
this.tooltip.innerHTML = message
|
||||||
|
this.tooltip.style.opacity = 1
|
||||||
|
|
||||||
|
const interpolantContainer = actor.parentElement
|
||||||
|
const codeContainer = interpolantContainer.parentElement
|
||||||
|
const scrollElement = codeContainer.parentElement
|
||||||
|
|
||||||
|
const scrollOffsetX = scrollElement.scrollLeft
|
||||||
|
const scrollOffsetY = scrollElement.scrollTop + scrollElement.offsetHeight
|
||||||
|
|
||||||
|
const interpolantOffsetX = interpolantContainer.offsetLeft
|
||||||
|
const interpolantOffsetY = interpolantContainer.offsetTop
|
||||||
|
|
||||||
|
const centeringOffset = (interpolantContainer.offsetWidth - this.tooltip.offsetWidth) / 2
|
||||||
|
const belowPadding = 3
|
||||||
|
const belowOffset = interpolantContainer.offsetHeight + belowPadding
|
||||||
|
|
||||||
|
const x = interpolantOffsetX - scrollOffsetX + centeringOffset
|
||||||
|
const y = interpolantOffsetY - scrollOffsetY + belowOffset
|
||||||
|
|
||||||
|
this.tooltip.style.transform = 'translate(' + x + 'px, ' + y + 'px)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up mouse events for the interpolated parameters that have a type mismatch.
|
||||||
|
*/
|
||||||
|
function setupMouseInteractionForMismatches(tooltip, elements) {
|
||||||
|
function interpolationMouseEnter(event) {
|
||||||
|
const target = this.parentElement.getElementsByClassName('mismatch')[0]
|
||||||
|
const fg = target.getAttribute('data-fgColorHover')
|
||||||
|
const bg = target.getAttribute('data-bgColorHover')
|
||||||
|
const message = decodeURIComponent(target.getAttribute('data-message'))
|
||||||
|
tooltip.show(target, message)
|
||||||
|
target.style.color = fg
|
||||||
|
target.style.backgroundColor = bg
|
||||||
|
}
|
||||||
|
function interpolationMouseLeave(event) {
|
||||||
|
const target = this.parentElement.getElementsByClassName('mismatch')[0]
|
||||||
|
const fg = target.getAttribute('data-fgColor')
|
||||||
|
const bg = target.getAttribute('data-bgColor')
|
||||||
|
target.style.color = fg
|
||||||
|
target.style.backgroundColor = bg
|
||||||
|
tooltip.hide(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < elements.length; ++i) {
|
||||||
|
elements[i].addEventListener('mouseenter', interpolationMouseEnter)
|
||||||
|
elements[i].addEventListener('mouseleave', interpolationMouseLeave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SqlVisualization
|
@ -16,16 +16,8 @@ class TableVisualization extends Visualization {
|
|||||||
|
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
super(data)
|
super(data)
|
||||||
this.setPreprocessorModule('Standard.Table')
|
this.setPreprocessorModule('Standard.Visualization.Table.Visualization')
|
||||||
this.setPreprocessorCode(`
|
this.setPreprocessorCode(`x -> here.prepare_visualization x 1000`)
|
||||||
x -> case x of
|
|
||||||
Table.Table _ ->
|
|
||||||
header = ["header", x.columns.map .name]
|
|
||||||
data = ["data", x.columns.map .to_vector . map (x -> x.take_start 2000) ]
|
|
||||||
pairs = [header,data]
|
|
||||||
Json.from_pairs pairs . to_text
|
|
||||||
_ -> x . to_json . to_text
|
|
||||||
`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDataReceived(data) {
|
onDataReceived(data) {
|
||||||
@ -132,9 +124,13 @@ class TableVisualization extends Visualization {
|
|||||||
|
|
||||||
function genGenericTable(data, level) {
|
function genGenericTable(data, level) {
|
||||||
let result = ''
|
let result = ''
|
||||||
|
if (Array.isArray(data)) {
|
||||||
data.forEach((point, ix) => {
|
data.forEach((point, ix) => {
|
||||||
result += '<tr><th>' + ix + '</th>' + toTableCell(point, level) + '</tr>'
|
result += '<tr><th>' + ix + '</th>' + toTableCell(point, level) + '</tr>'
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
result += '<tr>' + toTableCell(data, level) + '</tr>'
|
||||||
|
}
|
||||||
return tableOf(result, level)
|
return tableOf(result, level)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,17 +170,69 @@ class TableVisualization extends Visualization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function genDataframe(parsedData) {
|
||||||
|
let result = ''
|
||||||
|
function addHeader(content) {
|
||||||
|
result += '<th>' + content + '</th>'
|
||||||
|
}
|
||||||
|
function addCell(content) {
|
||||||
|
result += '<td class="plaintext">' + content + '</td>'
|
||||||
|
}
|
||||||
|
result += '<tr>'
|
||||||
|
parsedData.indices_header.forEach(addHeader)
|
||||||
|
parsedData.header.forEach(addHeader)
|
||||||
|
result += '</tr>'
|
||||||
|
let rows = 0
|
||||||
|
if (parsedData.data.length > 0) {
|
||||||
|
rows = parsedData.data[0].length
|
||||||
|
} else if (parsedData.indices.length > 0) {
|
||||||
|
rows = parsedData.indices[0].length
|
||||||
|
}
|
||||||
|
for (let i = 0; i < rows; ++i) {
|
||||||
|
result += '<tr>'
|
||||||
|
parsedData.indices.forEach(ix => addHeader(ix[i]))
|
||||||
|
parsedData.data.forEach(col => addCell(col[i]))
|
||||||
|
result += '</tr>'
|
||||||
|
}
|
||||||
|
return tableOf(result, 0)
|
||||||
|
}
|
||||||
|
|
||||||
while (this.dom.firstChild) {
|
while (this.dom.firstChild) {
|
||||||
this.dom.removeChild(this.dom.lastChild)
|
this.dom.removeChild(this.dom.lastChild)
|
||||||
}
|
}
|
||||||
|
|
||||||
const style_dark = `
|
const style_dark = `
|
||||||
<style>
|
<style>
|
||||||
table {
|
table, .hiddenrows {
|
||||||
font-family: DejaVuSansMonoBook, sans-serif;
|
font-family: DejaVuSansMonoBook, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 1px;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:first-child > th:first-child,
|
||||||
|
table > tbody > tr:first-child > td:first-child {
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:first-child > th:last-child,
|
||||||
|
table > tbody > tr:first-child > td:last-child {
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:last-child > th:first-child,
|
||||||
|
table > tbody > tr:last-child > td:first-child {
|
||||||
|
border-bottom-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:last-child > th:last-child,
|
||||||
|
table > tbody > tr:last-child > td:last-child {
|
||||||
|
border-bottom-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -201,25 +249,58 @@ class TableVisualization extends Visualization {
|
|||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th, .hiddenrows {
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td {
|
||||||
th {
|
|
||||||
background-color: rgba(255, 255, 255, 0.03);
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: rgba(255, 255, 200, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hiddenrows {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
`
|
`
|
||||||
|
|
||||||
const style_light = `
|
const style_light = `
|
||||||
<style>
|
<style>
|
||||||
table {
|
table, .hiddenrows {
|
||||||
font-family: DejaVuSansMonoBook, sans-serif;
|
font-family: DejaVuSansMonoBook, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 1px;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:first-child > th:first-child,
|
||||||
|
table > tbody > tr:first-child > td:first-child {
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:first-child > th:last-child,
|
||||||
|
table > tbody > tr:first-child > td:last-child {
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:last-child > th:first-child,
|
||||||
|
table > tbody > tr:last-child > td:first-child {
|
||||||
|
border-bottom-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:last-child > th:last-child,
|
||||||
|
table > tbody > tr:last-child > td:last-child {
|
||||||
|
border-bottom-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
color: rgba(0, 0, 0, 0.7);
|
color: rgba(0, 0, 0, 0.7);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -236,15 +317,23 @@ class TableVisualization extends Visualization {
|
|||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th, .hiddenrows {
|
||||||
color: rgba(0, 0, 0, 0.9);
|
color: rgba(0, 0, 0, 0.9);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td {
|
||||||
th {
|
|
||||||
background-color: rgba(0, 0, 0, 0.025);
|
background-color: rgba(0, 0, 0, 0.025);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: rgba(30, 30, 20, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hiddenrows {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
</style>`
|
</style>`
|
||||||
|
|
||||||
const width = this.dom.getAttributeNS(null, 'width')
|
const width = this.dom.getAttributeNS(null, 'width')
|
||||||
@ -255,8 +344,8 @@ class TableVisualization extends Visualization {
|
|||||||
tabElem.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height)
|
tabElem.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height)
|
||||||
tabElem.setAttributeNS(null, 'width', '100%')
|
tabElem.setAttributeNS(null, 'width', '100%')
|
||||||
tabElem.setAttributeNS(null, 'height', '100%')
|
tabElem.setAttributeNS(null, 'height', '100%')
|
||||||
const tblViewStyle = `width: ${width - 10}px;
|
const tblViewStyle = `width: ${width - 5}px;
|
||||||
height: ${height - 10}px;
|
height: ${height - 5}px;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
padding:2.5px;`
|
padding:2.5px;`
|
||||||
tabElem.setAttributeNS(null, 'style', tblViewStyle)
|
tabElem.setAttributeNS(null, 'style', tblViewStyle)
|
||||||
@ -271,8 +360,34 @@ class TableVisualization extends Visualization {
|
|||||||
if (document.getElementById('root').classList.contains('dark-theme')) {
|
if (document.getElementById('root').classList.contains('dark-theme')) {
|
||||||
style = style_dark
|
style = style_dark
|
||||||
}
|
}
|
||||||
const table = genTable(parsedData.data || parsedData, 0, parsedData.header)
|
|
||||||
|
if (parsedData.error !== undefined) {
|
||||||
|
tabElem.innerHTML = 'Error: ' + parsedData.error
|
||||||
|
} else if (parsedData.json !== undefined) {
|
||||||
|
const table = genTable(parsedData.json, 0, undefined)
|
||||||
tabElem.innerHTML = style + table
|
tabElem.innerHTML = style + table
|
||||||
|
} else {
|
||||||
|
const table = genDataframe(parsedData)
|
||||||
|
let suffix = ''
|
||||||
|
const allRowsCount = parsedData.all_rows_count
|
||||||
|
if (allRowsCount !== undefined) {
|
||||||
|
const includedRowsCount = parsedData.data.length > 0 ? parsedData.data[0].length : 0
|
||||||
|
const hiddenCount = allRowsCount - includedRowsCount
|
||||||
|
if (hiddenCount > 0) {
|
||||||
|
let rows = 'rows'
|
||||||
|
if (hiddenCount == 1) {
|
||||||
|
rows = 'row'
|
||||||
|
}
|
||||||
|
suffix =
|
||||||
|
'<span class="hiddenrows">… and ' +
|
||||||
|
hiddenCount +
|
||||||
|
' more ' +
|
||||||
|
rows +
|
||||||
|
'.</span>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabElem.innerHTML = style + table + suffix
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSize(size) {
|
setSize(size) {
|
||||||
|
@ -86,6 +86,7 @@ impl Registry {
|
|||||||
self.try_add_java_script(builtin::visualization::java_script::scatter_plot_visualization());
|
self.try_add_java_script(builtin::visualization::java_script::scatter_plot_visualization());
|
||||||
self.try_add_java_script(builtin::visualization::java_script::histogram_visualization());
|
self.try_add_java_script(builtin::visualization::java_script::histogram_visualization());
|
||||||
self.try_add_java_script(builtin::visualization::java_script::table_visualization());
|
self.try_add_java_script(builtin::visualization::java_script::table_visualization());
|
||||||
|
self.try_add_java_script(builtin::visualization::java_script::sql_visualization());
|
||||||
self.try_add_java_script(builtin::visualization::java_script::geo_map_visualization());
|
self.try_add_java_script(builtin::visualization::java_script::geo_map_visualization());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user