Create Indicators With ReactiveHTML#
In this guide we will show you how to build custom indicators using ReactiveHTML
.
Basic Progress Indicator#
Here we create a basic progress indicator
import param
import panel as pn
from panel.reactive import ReactiveHTML
pn.extension()
class CustomProgress(ReactiveHTML):
value = param.Integer(0, bounds=(0,100))
color = param.Color("#007bff")
_template= """
<div id="progress-bar" style="background-color: ${color}; height: 100%; width: ${value}%;"></div>
"""
progress = CustomProgress(
value=55, styles={"border": "2px solid lightgray"}, height=100, sizing_mode="stretch_width"
)
pn.Column(progress, progress.param.value, progress.param.color).servable()
Advanced Progress Indicator#
Here we create an advanced progress indicator
import random
import pandas as pd
import param
import panel as pn
class ArcProgressIndicator(pn.reactive.ReactiveHTML):
progress = param.Number(default=0, bounds=(0, 100))
transition_duration = param.Number(default=0.5, bounds=(0, None))
format_options = param.Dict(
default={
"locale": "en-US",
"style": "percent",
"minimumIntegerDigits": "1",
"maximumIntegerDigits": "3",
"minimumFractionDigits": "1",
"maximumFractionDigits": "1",
}
)
text_style = param.Dict(
default={
"font-size": 4.5,
"text-anchor": "middle",
"letter-spacing": -0.2,
}
)
empty_color = param.Color(default="#e8f6fd")
fill_color = param.Color(default="#2a87d8")
use_gradient = param.Boolean(default=False)
gradient = param.Parameter(
default=[{"stop": 0, "color": "green"}, {"stop": 1, "color": "red"}]
)
annotations = param.Parameter(default=[])
viewbox = param.List(default=[0, -1, 20, 10], constant=True)
_template = """
<div id="container">
<svg id="svg" height="100%" width="100%" viewBox="0 -1 20 10" style="display: block;">
<defs>
<linearGradient id="grad">
<stop offset="0" style="stop-color:black" />
<stop offset="1" style="stop-color:magenta" />
</linearGradient>
</defs>
</svg>
</div>
"""
_scripts = {
"render": """
state.initialized = false
state.GradientReader = function(colorStops) {
const canvas = document.createElement('canvas'); // create canvas element
const ctx = canvas.getContext('2d'); // get context
const gr = ctx.createLinearGradient(0, 0, 101, 0); // create a gradient
canvas.width = 101; // 101 pixels incl.
canvas.height = 1; // as the gradient
for (const { stop, color } of colorStops) { // add color stops
gr.addColorStop(stop, color);
}
ctx.fillStyle = gr; // set as fill style
ctx.fillRect(0, 0, 101, 1); // draw a single line
// method to get color of gradient at % position [0, 100]
return {
getColor: (pst) => {
const color_array = ctx.getImageData(pst|0, 0, 1, 1).data
return `rgb(${color_array[0]}, ${color_array[1]}, ${color_array[2]})`
}
};
}
const empty_path = document.createElementNS("http://www.w3.org/2000/svg", "path")
empty_path.setAttribute("d", "M1 9 A 8 8 0 1 1 19 9")
empty_path.setAttribute("fill", "none")
empty_path.setAttribute("stroke-width", "1.5")
state.empty_path = empty_path
const fill_path = empty_path.cloneNode()
state.fill_path = fill_path
text = document.createElementNS("http://www.w3.org/2000/svg", "text")
text.setAttribute("y","8.9")
text.setAttribute("x","10")
self.text_style()
state.text = text
//path used to
const external_path = document.createElementNS("http://www.w3.org/2000/svg", "path")
external_path.setAttribute("d", "M0.25 9 A 8.75 8.75 0 1 1 19.75 9")
state.external_path = external_path
svg.appendChild(empty_path)
svg.appendChild(fill_path)
svg.appendChild(text)
self.viewbox()
self.transition_duration()
self.empty_color()
self.fill_color()
self.format_options()
self.gradient()
self.progress()
self.annotations()
state.initialized = true
""",
"annotations": """
const path_len = state.empty_path.getTotalLength()
const tot_len = state.external_path.getTotalLength()
svg.querySelectorAll(".ArcProgressIndicator_annotation").forEach((node) => node.remove())
const annotations = data.annotations
annotations.forEach((annotation) => {
const {progress, text, tick_width, text_size} = annotation
const annotation_position = state.external_path.getPointAtLength(tot_len * progress/100);
const annot_tick = state.empty_path.cloneNode()
annot_tick.setAttribute("class", "ArcProgressIndicator_annotation")
annot_tick.setAttribute("stroke-dasharray", `${tick_width} ${path_len}`)
annot_tick.setAttribute("stroke-dashoffset", `${-(path_len * progress/100 - tick_width/2)}`)
annot_tick.setAttribute("stroke", "black")
const annot_text = document.createElementNS("http://www.w3.org/2000/svg", "text")
annot_text.setAttribute("class", "ArcProgressIndicator_annotation")
annot_text.setAttribute("x",annotation_position.x)
annot_text.setAttribute("y",annotation_position.y)
annot_text.setAttribute("style",`font-size:${text_size};text-anchor:${progress>50 ? "start" : "end"}`)
const textNode = document.createTextNode(text)
annot_text.appendChild(textNode)
svg.appendChild(annot_tick)
svg.appendChild(annot_text)
})
""",
"progress": """
const textNode = document.createTextNode(`${state.formatter.format(data.progress / (state.formatter.resolvedOptions().style=="percent" ? 100 : 1))}`)
if(state.text.firstChild)
state.text.firstChild.replaceWith(textNode)
else
text.appendChild(textNode)
const path_len = state.empty_path.getTotalLength()
state.fill_path.setAttribute("stroke-dasharray", `${path_len * data.progress/100} ${path_len}`)
const current_color = data.use_gradient ? state.gr.getColor(data.progress) : data.fill_color
if(!state.text_style || !("fill" in state.text_style))
state.text.setAttribute("fill", current_color)
""",
"transition_duration": """
state.fill_path.setAttribute("style", `transition: stroke-dasharray ${data.transition_duration}s`)
""",
"format_options": """
state.formatter = new Intl.NumberFormat(data.format_options.locale, data.format_options)
if (state.initialized)
self.progress()
""",
"text_style": """
text.setAttribute("style", Object.entries(data.text_style).map(([k, v]) => `${k}:${v}`).join(';'))
""",
"empty_color": """
state.empty_path.setAttribute("stroke", data.empty_color)
""",
"fill_color": """
if (data.use_gradient)
state.fill_path.setAttribute("stroke", `url(#grad-${data.id}`)
else
state.fill_path.setAttribute("stroke", data.fill_color)
""",
"use_gradient": """
self.fill_color()
if (state.initialized)
self.progress()
""",
"gradient": """
const gradientNode = container.querySelector("linearGradient")
gradientNode.querySelectorAll("stop").forEach((stop) => gradientNode.removeChild(stop))
const list_gradient_values = data.gradient
list_gradient_values.forEach((elem) => {
const stopNode = document.createElementNS("http://www.w3.org/2000/svg", "stop")
stopNode.setAttribute("offset", `${elem.stop}`)
stopNode.setAttribute("stop-color", `${elem.color}`)
gradientNode.appendChild(stopNode)
})
state.gr = new state.GradientReader(data.gradient)
if (state.initialized)
self.progress()
""",
"viewbox": """
svg.setAttribute("viewBox", data.viewbox.join(" "))
""",
}
def __init__(self, **params):
if "text_style" in params:
default_text_style = dict(self.param.text_style.default)
default_text_style.update(params.get("text_style"))
params["text_style"] = default_text_style
if "format_options" in params:
default_format_options = dict(self.param.format_options.default)
default_format_options.update(params.get("format_options"))
params["format_options"] = default_format_options
super().__init__(**params)
self._on_use_gradient_change()
@pn.depends("use_gradient", watch=True)
def _on_use_gradient_change(self):
if self.use_gradient:
self.param.fill_color.precedence = -1
self.param.gradient.precedence = 1
else:
self.param.fill_color.precedence = 1
self.param.gradient.precedence = -1
indicator = ArcProgressIndicator(
progress=10,
styles={"background": "#efebeb"},
use_gradient=True,
text_style={"fill": "gray"},
format_options={"style": "percent"},
viewbox=[-2, -2, 24, 11],
annotations=[
{"progress": 0, "text": "0%", "tick_width": 0.2, "text_size": 0.8},
{"progress": 10, "text": "10%", "tick_width": 0.1, "text_size": 1},
{"progress": 100, "text": "100%", "tick_width": 0.2, "text_size": 0.8},
],
)
pn.Row(indicator.controls()[0], indicator).servable()