204 lines
5.5 KiB
TypeScript
204 lines
5.5 KiB
TypeScript
|
|
/*
|
||
|
|
This file was adapted from https://github.com/uber/deck.gl/ the LICENSE
|
||
|
|
below is preserved to comply with the original license.
|
||
|
|
|
||
|
|
Copyright (c) 2015 - 2017 Uber Technologies, Inc.
|
||
|
|
|
||
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
|
of this software and associated documentation files (the "Software"), to deal
|
||
|
|
in the Software without restriction, including without limitation the rights
|
||
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
|
copies of the Software, and to permit persons to whom the Software is
|
||
|
|
furnished to do so, subject to the following conditions:
|
||
|
|
|
||
|
|
The above copyright notice and this permission notice shall be included in
|
||
|
|
all copies or substantial portions of the Software.
|
||
|
|
|
||
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
|
THE SOFTWARE.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {isBoolean, isString, isNumber, isPlainObject} from "@bokehjs/core/util/types"
|
||
|
|
|
||
|
|
/* global document */
|
||
|
|
let lastPickedObject: any
|
||
|
|
let lastTooltip: any
|
||
|
|
|
||
|
|
const DEFAULT_STYLE = {
|
||
|
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||
|
|
display: "flex",
|
||
|
|
flex: "wrap",
|
||
|
|
maxWidth: "500px",
|
||
|
|
flexDirection: "column",
|
||
|
|
zIndex: 2,
|
||
|
|
}
|
||
|
|
|
||
|
|
function getDiv() {
|
||
|
|
return document.createElement("div")
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getTooltipDefault(pickedInfo: any) {
|
||
|
|
if (!pickedInfo.picked) {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
if (pickedInfo.object === lastPickedObject) {
|
||
|
|
return lastTooltip
|
||
|
|
}
|
||
|
|
const tooltip = {
|
||
|
|
html: tabularize(pickedInfo.object),
|
||
|
|
style: DEFAULT_STYLE,
|
||
|
|
}
|
||
|
|
lastTooltip = tooltip
|
||
|
|
lastPickedObject = pickedInfo.object
|
||
|
|
return tooltip
|
||
|
|
}
|
||
|
|
|
||
|
|
const EXCLUDES = new Set(["position", "index"])
|
||
|
|
|
||
|
|
export function tabularize(json: any) {
|
||
|
|
// Turns a JSON object of picked info into HTML for a tooltip
|
||
|
|
const dataTable = getDiv()
|
||
|
|
|
||
|
|
// Creates rows of two columns for the tooltip
|
||
|
|
for (const key in json) {
|
||
|
|
if (EXCLUDES.has(key)) {
|
||
|
|
continue; // eslint-disable-line
|
||
|
|
}
|
||
|
|
const header = getDiv()
|
||
|
|
header.className = "header"
|
||
|
|
header.textContent = key
|
||
|
|
|
||
|
|
const valueElement = getDiv()
|
||
|
|
valueElement.className = "value"
|
||
|
|
|
||
|
|
valueElement.textContent = toText(json[key])
|
||
|
|
|
||
|
|
const row = getDiv()
|
||
|
|
|
||
|
|
setStyles(row, header, valueElement)
|
||
|
|
|
||
|
|
row.appendChild(header)
|
||
|
|
row.appendChild(valueElement)
|
||
|
|
dataTable.appendChild(row)
|
||
|
|
}
|
||
|
|
|
||
|
|
return dataTable.innerHTML
|
||
|
|
}
|
||
|
|
|
||
|
|
function setStyles(row: any, header: any, value: any) {
|
||
|
|
// Set default tooltip style
|
||
|
|
Object.assign(header.style, {
|
||
|
|
fontWeight: 700,
|
||
|
|
marginRight: "10px",
|
||
|
|
flex: "1 1 0%",
|
||
|
|
})
|
||
|
|
|
||
|
|
Object.assign(value.style, {
|
||
|
|
flex: "none",
|
||
|
|
maxWidth: "250px",
|
||
|
|
overflow: "hidden",
|
||
|
|
whiteSpace: "nowrap",
|
||
|
|
textOverflow: "ellipsis",
|
||
|
|
})
|
||
|
|
|
||
|
|
Object.assign(row.style, {
|
||
|
|
display: "flex",
|
||
|
|
flexDirection: "row",
|
||
|
|
justifyContent: "space-between",
|
||
|
|
alignItems: "stretch",
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
export function toText(jsonValue: any) {
|
||
|
|
// Set contents of table value, trimming for certain types of data
|
||
|
|
let text
|
||
|
|
if (Array.isArray(jsonValue) && jsonValue.length > 4) {
|
||
|
|
text = `Array<${jsonValue.length}>`
|
||
|
|
} else if (isString(jsonValue)) {
|
||
|
|
text = jsonValue
|
||
|
|
} else if (isNumber(jsonValue)) {
|
||
|
|
text = String(jsonValue)
|
||
|
|
} else {
|
||
|
|
try {
|
||
|
|
text = JSON.stringify(jsonValue)
|
||
|
|
} catch (err) {
|
||
|
|
text = "<Non-Serializable Object>"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const MAX_LENGTH = 50
|
||
|
|
if (text.length > MAX_LENGTH) {
|
||
|
|
text = text.slice(0, MAX_LENGTH)
|
||
|
|
}
|
||
|
|
return text
|
||
|
|
}
|
||
|
|
|
||
|
|
export function substituteIn(template: any, json: any) {
|
||
|
|
let output = template
|
||
|
|
for (const key in json) {
|
||
|
|
if (isPlainObject(json[key])) {
|
||
|
|
for (const subkey in json[key]) {
|
||
|
|
output = output.replace(`{${key}.${subkey}}`, json[key][subkey])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
output = output.replace(`{${key}}`, json[key])
|
||
|
|
}
|
||
|
|
return output
|
||
|
|
}
|
||
|
|
|
||
|
|
export function makeTooltip(tooltips: any, layers: any[]) {
|
||
|
|
/*
|
||
|
|
* If explicitly no tooltip passed by user, return null
|
||
|
|
* If a JSON object passed, return a tooltip based on that object
|
||
|
|
* We expect the user has passed a string template that will take pickedInfo keywords
|
||
|
|
* If a boolean passed, return the default tooltip
|
||
|
|
*/
|
||
|
|
if (!tooltips) {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
|
||
|
|
let per_layer = false
|
||
|
|
const layer_tooltips: any = {}
|
||
|
|
for (let i = 0; i < layers.length; i++) {
|
||
|
|
const layer = layers[i]
|
||
|
|
const layer_id = (layer.id as string)
|
||
|
|
if (!isBoolean(tooltips) && (i.toString() in tooltips || layer_id in tooltips)) {
|
||
|
|
layer_tooltips[layer_id] = layer_id in tooltips ? tooltips[layer_id]: tooltips[i.toString()]
|
||
|
|
per_layer = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (tooltips.html || tooltips.text || per_layer) {
|
||
|
|
return (pickedInfo: any) => {
|
||
|
|
if (!pickedInfo.picked) {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
|
||
|
|
const tooltip = (per_layer) ? layer_tooltips[pickedInfo.layer.id]: tooltips
|
||
|
|
if (tooltip == null) {
|
||
|
|
return
|
||
|
|
} else if (isBoolean(tooltip)) {
|
||
|
|
return tooltip ? getTooltipDefault(pickedInfo) : null
|
||
|
|
}
|
||
|
|
|
||
|
|
const formattedTooltip: any = {
|
||
|
|
style: tooltip.style || DEFAULT_STYLE,
|
||
|
|
}
|
||
|
|
|
||
|
|
if (tooltip.html) {
|
||
|
|
formattedTooltip.html = substituteIn(tooltip.html, pickedInfo.object)
|
||
|
|
} else {
|
||
|
|
formattedTooltip.text = substituteIn(tooltip.text, pickedInfo.object)
|
||
|
|
}
|
||
|
|
|
||
|
|
return formattedTooltip
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return getTooltipDefault
|
||
|
|
}
|