Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 100 additions & 45 deletions plots/slope-basic/implementations/python/bokeh.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
""" pyplots.ai
""" anyplot.ai
slope-basic: Basic Slope Chart (Slopegraph)
Library: bokeh 3.8.1 | Python 3.13.11
Quality: 91/100 | Created: 2025-12-23
Library: bokeh 3.9.0 | Python 3.13.13
Quality: 84/100 | Updated: 2026-04-30
"""

import os

from bokeh.io import export_png, save
from bokeh.models import Label
from bokeh.models import ColumnDataSource, HoverTool, Label
from bokeh.plotting import figure
from bokeh.resources import CDN


# Data - Product sales comparison Q1 vs Q4 (10 products)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

INCREASE_COLOR = "#009E73" # Okabe-Ito position 1 (brand green)
DECREASE_COLOR = "#D55E00" # Okabe-Ito position 2 (vermillion)

products = [
"Product A",
"Product B",
Expand All @@ -26,66 +36,115 @@
q1_sales = [85, 72, 91, 45, 68, 53, 78, 62, 40, 88]
q4_sales = [92, 65, 88, 71, 74, 48, 95, 58, 67, 82]

# Determine direction for color coding
colors = []
for start, end in zip(q1_sales, q4_sales, strict=True):
if end > start:
colors.append("#306998") # Python Blue - increase
elif end < start:
colors.append("#FFD43B") # Python Yellow - decrease
else:
colors.append("#888888") # Gray - no change

# Create figure
colors = [INCREASE_COLOR if end > start else DECREASE_COLOR for start, end in zip(q1_sales, q4_sales, strict=True)]


def spread_labels(ys, min_gap=4.5):
"""Shift label y-positions apart so dense clusters don't overlap."""
n = len(ys)
order = sorted(range(n), key=lambda i: ys[i])
adjusted = [float(ys[i]) for i in order]
for _ in range(30):
changed = False
for i in range(1, n):
if adjusted[i] - adjusted[i - 1] < min_gap:
mid = (adjusted[i] + adjusted[i - 1]) / 2
adjusted[i - 1] = mid - min_gap / 2
adjusted[i] = mid + min_gap / 2
changed = True
if not changed:
break
result = [0.0] * n
for new_i, orig_i in enumerate(order):
result[orig_i] = adjusted[new_i]
return result


left_y = spread_labels(q1_sales)
right_y = spread_labels(q4_sales)

p = figure(
width=4800,
height=2700,
title="slope-basic · bokeh · pyplots.ai",
title="slope-basic · bokeh · anyplot.ai",
x_range=(-0.5, 1.5),
y_range=(25, 105),
y_range=(25, 112),
toolbar_location=None,
)

# Style title and axes
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG
p.outline_line_color = None

p.title.text_font_size = "32pt"
p.title.align = "center"
p.title.text_color = INK

# Remove x axis tick labels and set custom labels for time points
p.xaxis.visible = False
p.yaxis.axis_label = "Sales (thousands)"
p.yaxis.axis_label_text_font_size = "22pt"
p.yaxis.axis_label_text_color = INK
p.yaxis.major_label_text_font_size = "18pt"
p.yaxis.major_label_text_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT

p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = INK_SOFT
p.ygrid.grid_line_alpha = 0.10

# Add time point labels at bottom
p.add_layout(Label(x=0, y=32, text="Q1", text_font_size="28pt", text_align="center", text_baseline="top"))
p.add_layout(Label(x=1, y=32, text="Q4", text_font_size="28pt", text_align="center", text_baseline="top"))
# Time point labels
for x_pos, label in [(0, "Q1"), (1, "Q4")]:
p.add_layout(
Label(
x=x_pos, y=27, text=label, text_font_size="28pt", text_align="center", text_baseline="top", text_color=INK
)
)

# Direction legend in upper-center (above data range)
for y_pos, legend_text, color in [(109, "— Increase", INCREASE_COLOR), (103, "— Decrease", DECREASE_COLOR)]:
p.add_layout(
Label(
x=0.5,
y=y_pos,
text=legend_text,
text_font_size="20pt",
text_align="center",
text_baseline="middle",
text_color=color,
)
)

# Draw slope lines connecting Q1 to Q4 for each product
# ColumnDataSource for scatter enables HoverTool
scatter_data: dict[str, list] = {"x": [], "y": [], "color": [], "product": [], "period": [], "value": []}
for product, start, end, color in zip(products, q1_sales, q4_sales, colors, strict=True):
# Draw the connecting line
p.line(x=[0, 1], y=[start, end], line_width=4, line_color=color, line_alpha=0.8)
scatter_data["x"].extend([0, 1])
scatter_data["y"].extend([start, end])
scatter_data["color"].extend([color, color])
scatter_data["product"].extend([product, product])
scatter_data["period"].extend(["Q1", "Q4"])
scatter_data["value"].extend([start, end])

# Add markers at both endpoints
p.scatter(x=[0, 1], y=[start, end], size=18, color=color, alpha=0.9)
source = ColumnDataSource(data=scatter_data)

# Add labels at start (Q1) - left aligned
# Draw slope lines and endpoint labels
for i, (product, start, end, color) in enumerate(zip(products, q1_sales, q4_sales, colors, strict=True)):
p.line(x=[0, 1], y=[start, end], line_width=4, line_color=color, line_alpha=0.8)
p.add_layout(
Label(
x=-0.05,
y=start,
y=left_y[i],
text=f"{product}: {start}",
text_font_size="18pt",
text_align="right",
text_baseline="middle",
text_color=color,
)
)

# Add labels at end (Q4) - right aligned
p.add_layout(
Label(
x=1.05,
y=end,
y=right_y[i],
text=f"{end}: {product}",
text_font_size="18pt",
text_align="left",
Expand All @@ -94,16 +153,12 @@
)
)

# Style grid
p.xgrid.grid_line_color = None
p.ygrid.grid_line_alpha = 0.3
p.ygrid.grid_line_dash = "dashed"

# Style outline
p.outline_line_color = None

# Save as PNG
export_png(p, filename="plot.png")
dots = p.scatter(x="x", y="y", size=18, color="color", source=source, alpha=0.9)
p.add_tools(
HoverTool(
renderers=[dots], tooltips=[("Product", "@product"), ("Period", "@period"), ("Sales", "@value{0} thousand")]
)
)

# Also save as HTML for interactive viewing
save(p, filename="plot.html", resources=CDN, title="slope-basic · bokeh · pyplots.ai")
export_png(p, filename=f"plot-{THEME}.png")
save(p, filename=f"plot-{THEME}.html", resources=CDN, title="slope-basic · bokeh · anyplot.ai")
Loading
Loading