Skip to content

Commit 3c1e329

Browse files
feat(highcharts): implement mosaic-categorical (#3680)
## Implementation: `mosaic-categorical` - highcharts Implements the **highcharts** version of `mosaic-categorical`. **File:** `plots/mosaic-categorical/implementations/highcharts.py` **Parent Issue:** #3650 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20886562386)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 7db058b commit 3c1e329

File tree

2 files changed

+424
-0
lines changed

2 files changed

+424
-0
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
""" pyplots.ai
2+
mosaic-categorical: Mosaic Plot for Categorical Association Analysis
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-11
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
11+
12+
import numpy as np
13+
import pandas as pd
14+
from highcharts_core.chart import Chart
15+
from highcharts_core.options import HighchartsOptions
16+
from PIL import Image
17+
from selenium import webdriver
18+
from selenium.webdriver.chrome.options import Options
19+
20+
21+
# Data - Titanic survival by passenger class (classic contingency table example)
22+
np.random.seed(42)
23+
24+
# Create contingency table data: Class vs Survival
25+
# Realistic proportions based on Titanic data patterns
26+
data = {
27+
"Class": ["First", "First", "Second", "Second", "Third", "Third", "Crew", "Crew"],
28+
"Survival": ["Survived", "Died", "Survived", "Died", "Survived", "Died", "Survived", "Died"],
29+
"Count": [203, 122, 118, 167, 178, 528, 212, 673],
30+
}
31+
df = pd.DataFrame(data)
32+
33+
# Calculate proportions for mosaic plot
34+
total = df["Count"].sum()
35+
class_totals = df.groupby("Class")["Count"].sum()
36+
37+
# Build hierarchical data for treemap (mosaic-like visualization)
38+
# Parent nodes represent categories, children represent survival status
39+
treemap_data = []
40+
41+
# Color palette - colorblind-safe
42+
colors = {
43+
"Survived": "#306998", # Python Blue
44+
"Died": "#FFD43B", # Python Yellow
45+
}
46+
47+
# Define order for consistent layout
48+
class_order = ["First", "Second", "Third", "Crew"]
49+
50+
for cls in class_order:
51+
cls_data = df[df["Class"] == cls]
52+
cls_total = class_totals[cls]
53+
54+
# Add parent node for class
55+
treemap_data.append({"id": cls, "name": cls})
56+
57+
# Add child nodes for each survival status
58+
for _, row in cls_data.iterrows():
59+
treemap_data.append(
60+
{"parent": cls, "name": f"{row['Survival']}", "value": int(row["Count"]), "color": colors[row["Survival"]]}
61+
)
62+
63+
# Create chart
64+
chart = Chart(container="container")
65+
chart.options = HighchartsOptions()
66+
67+
# Chart configuration - optimized margins for better canvas utilization
68+
chart.options.chart = {
69+
"type": "treemap",
70+
"width": 4800,
71+
"height": 2700,
72+
"backgroundColor": "#ffffff",
73+
"marginTop": 200,
74+
"marginBottom": 60,
75+
"marginLeft": 40,
76+
"marginRight": 40,
77+
}
78+
79+
# Title - required format without extra descriptive text
80+
chart.options.title = {
81+
"text": "mosaic-categorical · highcharts · pyplots.ai",
82+
"style": {"fontSize": "72px", "fontWeight": "bold"},
83+
"y": 70,
84+
}
85+
86+
# Subtitle with Titanic context and legend explanation
87+
chart.options.subtitle = {
88+
"text": "Titanic Survival by Passenger Class · Rectangle area proportional to count · "
89+
"<span style='color:#306998'>■</span> Survived · "
90+
"<span style='color:#FFD43B'>■</span> Died",
91+
"style": {"fontSize": "42px", "color": "#555555"},
92+
"useHTML": True,
93+
"y": 140,
94+
}
95+
96+
# Tooltip
97+
chart.options.tooltip = {
98+
"style": {"fontSize": "36px"},
99+
"pointFormat": "<b>{point.name}</b>: {point.value:,.0f} passengers",
100+
}
101+
102+
# Treemap series configuration - stripes layout for mosaic effect
103+
series_config = {
104+
"type": "treemap",
105+
"name": "Passengers",
106+
"layoutAlgorithm": "stripes",
107+
"layoutStartingDirection": "horizontal",
108+
"alternateStartingDirection": True,
109+
"animationLimit": 1000,
110+
"dataLabels": {"enabled": True, "style": {"fontSize": "32px", "fontWeight": "bold", "textOutline": "3px contrast"}},
111+
"levels": [
112+
{
113+
"level": 1,
114+
"dataLabels": {
115+
"enabled": True,
116+
"align": "center",
117+
"verticalAlign": "top",
118+
"style": {"fontSize": "48px", "fontWeight": "bold", "textOutline": "3px contrast"},
119+
"padding": 20,
120+
},
121+
"borderWidth": 6,
122+
"borderColor": "#ffffff",
123+
},
124+
{
125+
"level": 2,
126+
"dataLabels": {
127+
"enabled": True,
128+
"style": {"fontSize": "36px", "fontWeight": "bold", "textOutline": "2px contrast"},
129+
},
130+
"borderWidth": 3,
131+
"borderColor": "#ffffff",
132+
},
133+
],
134+
"data": treemap_data,
135+
}
136+
137+
chart.options.series = [series_config]
138+
139+
# Legend configuration - manual items for Survived/Died colors
140+
chart.options.legend = {"enabled": False}
141+
142+
# Download Highcharts JS and treemap module
143+
highcharts_url = "https://code.highcharts.com/highcharts.js"
144+
treemap_url = "https://code.highcharts.com/modules/treemap.js"
145+
146+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
147+
highcharts_js = response.read().decode("utf-8")
148+
149+
with urllib.request.urlopen(treemap_url, timeout=30) as response:
150+
treemap_js = response.read().decode("utf-8")
151+
152+
# Generate HTML with inline scripts
153+
html_str = chart.to_js_literal()
154+
html_content = f"""<!DOCTYPE html>
155+
<html>
156+
<head>
157+
<meta charset="utf-8">
158+
<script>{highcharts_js}</script>
159+
<script>{treemap_js}</script>
160+
</head>
161+
<body style="margin:0;">
162+
<div id="container" style="width: 4800px; height: 2700px;"></div>
163+
<script>{html_str}</script>
164+
</body>
165+
</html>"""
166+
167+
# Save HTML for interactive version
168+
with open("plot.html", "w", encoding="utf-8") as f:
169+
standalone_html = f"""<!DOCTYPE html>
170+
<html>
171+
<head>
172+
<meta charset="utf-8">
173+
<script src="https://code.highcharts.com/highcharts.js"></script>
174+
<script src="https://code.highcharts.com/modules/treemap.js"></script>
175+
</head>
176+
<body style="margin:0;">
177+
<div id="container" style="width: 100%; height: 100vh;"></div>
178+
<script>{html_str}</script>
179+
</body>
180+
</html>"""
181+
f.write(standalone_html)
182+
183+
# Write temp HTML and take screenshot
184+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
185+
f.write(html_content)
186+
temp_path = f.name
187+
188+
chrome_options = Options()
189+
chrome_options.add_argument("--headless")
190+
chrome_options.add_argument("--no-sandbox")
191+
chrome_options.add_argument("--disable-dev-shm-usage")
192+
chrome_options.add_argument("--disable-gpu")
193+
chrome_options.add_argument("--window-size=4800,2900")
194+
195+
driver = webdriver.Chrome(options=chrome_options)
196+
driver.get(f"file://{temp_path}")
197+
time.sleep(5) # Wait for chart to render
198+
driver.save_screenshot("plot_temp.png")
199+
driver.quit()
200+
201+
# Crop to exact 4800x2700 dimensions
202+
img = Image.open("plot_temp.png")
203+
img_cropped = img.crop((0, 0, 4800, 2700))
204+
img_cropped.save("plot.png")
205+
Path("plot_temp.png").unlink()
206+
207+
Path(temp_path).unlink() # Clean up temp file

0 commit comments

Comments
 (0)