import {HTMLBox, HTMLBoxView} from "./layout"
import {build_view} from "@bokehjs/core/build_views"
import {Plot} from "@bokehjs/models/plots"
import {Line, Step, VArea, VBar} from "@bokehjs/models/glyphs"
import type * as p from "@bokehjs/core/properties"
import {div} from "@bokehjs/core/dom"
import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source"
import {BasicTickFormatter, NumeralTickFormatter, TickFormatter} from "@bokehjs/models/formatters"
const red: string = "#d9534f"
const green: string = "#5cb85c"
const blue: string = "#428bca"
export class TrendIndicatorView extends HTMLBoxView {
declare model: TrendIndicator
containerDiv: HTMLDivElement
textDiv: HTMLDivElement
titleDiv: HTMLDivElement
valueDiv: HTMLDivElement
value2Div: HTMLDivElement
changeDiv: HTMLElement
plotDiv: HTMLDivElement
plot: Plot
_value_format: string
_value_change_format: string
override initialize(): void {
super.initialize()
this.containerDiv = div({style: {height: "100%", width: "100%"}})
this.titleDiv = div({style: {font_size: "1em", word_wrap: "break-word"}})
this.valueDiv = div({style: {font_size: "2em"}})
this.value2Div = div({style: {font_size: "1em", opacity: "0.5", display: "inline"}})
this.changeDiv = div({style: {font_size: "1em", opacity: "0.5", display: "inline"}})
this.textDiv = div({}, this.titleDiv, this.valueDiv, div({}, this.changeDiv, this.value2Div))
this.updateTitle()
this.updateValue()
this.updateValue2()
this.updateValueChange()
this.updateTextFontSize()
this.plotDiv = div({})
this.containerDiv = div({style: {height: "100%", width: "100%"}}, this.textDiv, this.plotDiv)
this.updateLayout()
}
override connect_signals(): void {
super.connect_signals()
const {
pos_color, neg_color, plot_color, plot_type, width,
height, sizing_mode, title, value, value_change, layout,
} = this.model.properties
this.on_change([pos_color, neg_color], () => this.updateValueChange())
this.on_change([plot_color, plot_type, width, height, sizing_mode], () => this.render())
this.on_change(title, () => this.updateTitle(true))
this.on_change(value, () => this.updateValue(true))
this.on_change(value_change, () => this.updateValue2(true))
this.on_change(layout, () => this.updateLayout())
}
override async render(): Promise {
super.render()
this.shadow_el.appendChild(this.containerDiv)
await this.setPlot()
}
private async setPlot() {
this.plot = new Plot({
background_fill_color: null,
border_fill_color: null,
outline_line_color: null,
min_border: 0,
sizing_mode: "stretch_both",
toolbar_location: null,
})
const source = this.model.source
if (this.model.plot_type === "line") {
const line = new Line({
x: {field: this.model.plot_x},
y: {field: this.model.plot_y},
line_width: 4,
line_color: this.model.plot_color,
})
this.plot.add_glyph(line, source)
} else if (this.model.plot_type === "step") {
const step = new Step({
x: {field: this.model.plot_x},
y: {field: this.model.plot_y},
line_width: 3,
line_color: this.model.plot_color,
})
this.plot.add_glyph(step, source)
} else if (this.model.plot_type === "area") {
const varea = new VArea({
x: {field: this.model.plot_x},
y1: {field: this.model.plot_y},
y2: 0,
fill_color: this.model.plot_color,
fill_alpha: 0.5,
})
this.plot.add_glyph(varea, source)
const line = new Line({
x: {field: this.model.plot_x},
y: {field: this.model.plot_y},
line_width: 3,
line_color: this.model.plot_color,
})
this.plot.add_glyph(line, source)
} else {
const vbar = new VBar({
x: {field: this.model.plot_x},
top: {field: this.model.plot_y},
width: 0.9,
line_color: null,
fill_color: this.model.plot_color,
})
this.plot.add_glyph(vbar, source)
}
const view = await build_view(this.plot)
this.plotDiv.innerHTML = ""
view.render_to(this.plotDiv)
}
override after_layout(): void {
super.after_layout()
this.updateTextFontSize()
}
updateTextFontSize(): void {
this.updateTextFontSizeColumn()
}
updateTextFontSizeColumn(): void {
let elWidth = this.containerDiv.clientWidth
let elHeight = this.containerDiv.clientHeight
if (this.model.layout === "column") {
elHeight = Math.round(elHeight/2)
} else {
elWidth = Math.round(elWidth/2)
}
const widthTitle = this.model.title.length
const widthValue = 2*this._value_format.length
const widthValue2 = this._value_change_format.length+1
const widthConstraint1 = elWidth/widthTitle*2.0
const widthConstraint2 = elWidth/widthValue*1.8
const widthConstraint3 = elWidth/widthValue2*2.0
const heightConstraint = elHeight/6
const fontSize = Math.min(widthConstraint1, widthConstraint2, widthConstraint3, heightConstraint)
this.textDiv.style.fontSize = `${Math.trunc(fontSize) }px`
this.textDiv.style.lineHeight = "1.3"
}
updateTitle(update_fontsize: boolean = false): void {
this.titleDiv.innerText = this.model.title
if (update_fontsize) {
this.updateTextFontSize()
}
}
updateValue(update_fontsize: boolean = false): void {
this._value_format = this.model.formatter.doFormat([this.model.value], {loc: 0})[0]
this.valueDiv.innerText = this._value_format
if (update_fontsize) {
this.updateTextFontSize()
}
}
updateValue2(update_fontsize: boolean = false): void {
this._value_change_format = this.model.change_formatter.doFormat([this.model.value_change], {loc: 0})[0]
this.value2Div.innerText = this._value_change_format
this.updateValueChange()
if (update_fontsize) {
this.updateTextFontSize()
}
}
updateValueChange(): void {
if (this.model.value_change > 0) {
this.changeDiv.innerHTML = "▲"
this.changeDiv.style.color = this.model.pos_color
} else if (this.model.value_change < 0) {
this.changeDiv.innerHTML = "▼"
this.changeDiv.style.color = this.model.neg_color
} else {
this.changeDiv.innerHTML = " "
this.changeDiv.style.color = "inherit"
}
}
updateLayout(): void {
if (this.model.layout === "column") {
this.containerDiv.style.display = "block"
this.textDiv.style.height = "50%"
this.textDiv.style.width = "100%"
this.plotDiv.style.height = "50%"
this.plotDiv.style.width = "100%"
} else {
this.containerDiv.style.display = "flex"
this.textDiv.style.height = "100%"
this.textDiv.style.width = ""
this.plotDiv.style.height = "100%"
this.plotDiv.style.width = ""
this.textDiv.style.flex = "1"
this.plotDiv.style.flex = "1"
}
if (this._has_finished) {
this.invalidate_layout()
}
}
}
export namespace TrendIndicator {
export type Attrs = p.AttrsOf
export type Props = HTMLBox.Props & {
change_formatter: p.Property
description: p.Property
formatter: p.Property
layout: p.Property
source: p.Property
plot_x: p.Property
plot_y: p.Property
plot_color: p.Property
plot_type: p.Property
pos_color: p.Property
neg_color: p.Property
title: p.Property
value: p.Property
value_change: p.Property
}
}
export interface TrendIndicator extends TrendIndicator.Attrs { }
export class TrendIndicator extends HTMLBox {
declare properties: TrendIndicator.Props
constructor(attrs?: Partial) {
super(attrs)
}
static override __module__ = "panel.models.trend"
static {
this.prototype.default_view = TrendIndicatorView
this.define(({Float, Str, Ref}) => ({
description: [ Str, "" ],
formatter: [ Ref(TickFormatter), () => new BasicTickFormatter() ],
change_formatter: [ Ref(TickFormatter), () => new NumeralTickFormatter() ],
layout: [ Str, "column" ],
source: [ Ref(ColumnDataSource) ],
plot_x: [ Str, "x" ],
plot_y: [ Str, "y" ],
plot_color: [ Str, blue ],
plot_type: [ Str, "bar" ],
pos_color: [ Str, green ],
neg_color: [ Str, red ],
title: [ Str, "" ],
value: [ Float, 0 ],
value_change: [ Float, 0 ],
}))
}
}