from math import pi
from typing import Any, Literal
from bokeh.core.properties import expr, value
from bokeh.document import Document
from bokeh.embed import file_html
from bokeh.models import (Arc, Circle, ColumnDataSource, Plot,
PolarTransform, Range1d, Ray, Text)
from bokeh.util.browser import view
xdr = Range1d(start=-1.25, end=1.25)
ydr = Range1d(start=-1.25, end=1.25)
plot = Plot(x_range=xdr, y_range=ydr, width=600, height=600)
plot.toolbar_location = None
plot.outline_line_color = None
start_angle = pi + pi/4
end_angle = -pi/4
max_kmh = 250
max_mph = max_kmh*0.621371
major_step, minor_step = 25, 5
plot.add_glyph(Circle(x=0, y=0, radius=1.00, fill_color="white", line_color="black"))
plot.add_glyph(Circle(x=0, y=0, radius=0.05, fill_color="gray", line_color="black"))
plot.add_glyph(Text(x=0, y=+0.15, text=value("km/h"), text_color="red", text_align="center", text_baseline="bottom", text_font_style="bold"))
plot.add_glyph(Text(x=0, y=-0.15, text=value("mph"), text_color="blue", text_align="center", text_baseline="top", text_font_style="bold"))
def data(val: float):
"""Shorthand to override default units with "data", for e.g. `Ray.length`. """
return value(val, units="data")
def speed_to_angle(speed: float, units: str) -> float:
max_speed = max_kmh if units == "kmh" else max_mph
speed = min(max(speed, 0), max_speed)
total_angle = start_angle - end_angle
angle = total_angle*float(speed)/max_speed
return start_angle - angle
def add_needle(speed: float, units: str) -> None:
angle = speed_to_angle(speed, units)
plot.add_glyph(Ray(x=0, y=0, length=data(0.75), angle=angle, line_color="black", line_width=3))
plot.add_glyph(Ray(x=0, y=0, length=data(0.10), angle=angle-pi, line_color="black", line_width=3))
def add_gauge(radius: float, max_value: float, length: float, direction: Literal[-1, 1], color: Any, major_step: int, minor_step: int) -> None:
major_angles, minor_angles = [], []
total_angle = start_angle - end_angle
major_angle_step = float(major_step)/max_value*total_angle
minor_angle_step = float(minor_step)/max_value*total_angle
major_angle = 0
while major_angle <= total_angle:
major_angles.append(start_angle - major_angle)
major_angle += major_angle_step
minor_angle = 0
while minor_angle <= total_angle:
minor_angles.append(start_angle - minor_angle)
minor_angle += minor_angle_step
major_labels = [ major_step*i for i, _ in enumerate(major_angles) ]
n = major_step/minor_step
minor_angles = [ x for i, x in enumerate(minor_angles) if i % n != 0 ]
glyph = Arc(x=0, y=0, radius=radius, start_angle=start_angle, end_angle=end_angle, direction="clock", line_color=color, line_width=2)
plot.add_glyph(glyph)
rotation = 0 if direction == 1 else -pi
angles = [ angle + rotation for angle in major_angles ]
source = ColumnDataSource(dict(major_angles=major_angles, angle=angles))
t = PolarTransform(radius=radius, angle="major_angles")
glyph = Ray(x=expr(t.x), y=expr(t.y), length=data(length), angle="angle", line_color=color, line_width=2)
plot.add_glyph(source, glyph)
angles = [ angle + rotation for angle in minor_angles ]
source = ColumnDataSource(dict(minor_angles=minor_angles, angle=angles))
t = PolarTransform(radius=radius, angle="minor_angles")
glyph = Ray(x=expr(t.x), y=expr(t.y), length=data(length/2), angle="angle", line_color=color, line_width=1)
plot.add_glyph(source, glyph)
text_angles = [ angle - pi/2 for angle in major_angles ]
source = ColumnDataSource(dict(major_angles=major_angles, angle=text_angles, text=major_labels))
t = PolarTransform(radius=radius + 2*length*direction, angle="major_angles")
glyph = Text(x=expr(t.x), y=expr(t.y), angle="angle", text="text", text_align="center", text_baseline="middle")
plot.add_glyph(source, glyph)
add_gauge(0.75, max_kmh, 0.05, +1, "red", major_step, minor_step)
add_gauge(0.70, max_mph, 0.05, -1, "blue", major_step, minor_step)
add_needle(55, "kmh")
doc = Document()
doc.add_root(plot)
if __name__ == "__main__":
doc.validate()
filename = "gauges.html"
with open(filename, "w") as f:
f.write(file_html(doc, title="Gauges"))
print(f"Wrote {filename}")
view(filename)