|
1 | 1 | """ pyplots.ai |
2 | 2 | dendrogram-basic: Basic Dendrogram |
3 | | -Library: highcharts 1.10.3 | Python 3.13.11 |
4 | | -Quality: 93/100 | Created: 2025-12-17 |
| 3 | +Library: highcharts 1.10.3 | Python 3.14.3 |
| 4 | +Quality: 84/100 | Created: 2026-04-05 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import tempfile |
|
66 | 66 | # Compute linkage matrix using Ward's method |
67 | 67 | linkage_matrix = linkage(data, method="ward") |
68 | 68 |
|
69 | | -# Get dendrogram structure |
70 | | -dend = dendrogram(linkage_matrix, labels=labels, no_plot=True) |
71 | | - |
72 | | -# Extract coordinates for drawing |
73 | | -icoord = dend["icoord"] # x coordinates |
74 | | -dcoord = dend["dcoord"] # y coordinates (distances) |
75 | | -ivl = dend["ivl"] # leaf labels in order |
76 | | - |
77 | | -# Create line series data for each U-shape in the dendrogram |
78 | | -line_series_data = [] |
79 | | -for xs, ys in zip(icoord, dcoord, strict=True): |
80 | | - # Each U-shape has 4 points |
81 | | - points = [[xs[j], ys[j]] for j in range(4)] |
82 | | - line_series_data.append( |
83 | | - {"data": points, "color": "#306998", "lineWidth": 3, "marker": {"enabled": False}, "enableMouseTracking": False} |
84 | | - ) |
| 69 | +# Get dendrogram structure with color threshold to show clusters |
| 70 | +dend = dendrogram(linkage_matrix, labels=labels, no_plot=True, color_threshold=0.7 * max(linkage_matrix[:, 2])) |
| 71 | + |
| 72 | +# Extract coordinates |
| 73 | +icoord = dend["icoord"] |
| 74 | +dcoord = dend["dcoord"] |
| 75 | +ivl = dend["ivl"] |
| 76 | +color_list = dend["color_list"] |
| 77 | + |
| 78 | +# Map scipy default colors to a cohesive palette anchored on Python Blue |
| 79 | +color_map = { |
| 80 | + "C0": "#306998", # Python Blue - primary cluster |
| 81 | + "C1": "#E07A3A", # Warm orange - secondary cluster |
| 82 | + "C2": "#5BA05B", # Muted green - tertiary cluster |
| 83 | + "C3": "#8B6EB8", # Soft purple |
| 84 | + "C4": "#C75A5A", # Muted red |
| 85 | + "b": "#306998", # Default blue mapped to Python Blue |
| 86 | +} |
85 | 87 |
|
86 | 88 | # Create chart |
87 | 89 | chart = Chart(container="container") |
88 | 90 | chart.options = HighchartsOptions() |
89 | 91 |
|
90 | | -# Configure chart |
91 | 92 | chart.options.chart = { |
92 | 93 | "type": "line", |
93 | 94 | "width": 4800, |
94 | 95 | "height": 2700, |
95 | 96 | "backgroundColor": "#ffffff", |
96 | | - "marginBottom": 300, |
97 | | - "spacingBottom": 50, |
| 97 | + "marginBottom": 280, |
| 98 | + "spacingTop": 30, |
| 99 | + "spacingLeft": 80, |
| 100 | + "spacingRight": 80, |
98 | 101 | } |
99 | 102 |
|
100 | 103 | # Title |
101 | 104 | chart.options.title = { |
102 | | - "text": "dendrogram-basic · highcharts · pyplots.ai", |
103 | | - "style": {"fontSize": "56px", "fontWeight": "bold"}, |
| 105 | + "text": "Iris Species Clustering \u00b7 dendrogram-basic \u00b7 highcharts \u00b7 pyplots.ai", |
| 106 | + "style": {"fontSize": "44px", "fontWeight": "600", "color": "#2c3e50"}, |
| 107 | + "margin": 40, |
104 | 108 | } |
105 | 109 |
|
106 | 110 | # X-axis with sample labels |
107 | | -# Dendrogram x-coords are at 5, 15, 25, ... for leaves |
108 | 111 | x_positions = [(i * 10) + 5 for i in range(len(ivl))] |
109 | 112 | chart.options.x_axis = { |
110 | | - "title": {"text": "Sample", "style": {"fontSize": "44px"}}, |
| 113 | + "title": {"text": "Sample", "style": {"fontSize": "32px", "color": "#555"}}, |
111 | 114 | "tickPositions": x_positions, |
112 | | - "labels": {"style": {"fontSize": "24px"}, "rotation": 45}, |
| 115 | + "labels": {"style": {"fontSize": "26px", "color": "#444"}, "rotation": 45}, |
113 | 116 | "min": 0, |
114 | 117 | "max": len(ivl) * 10, |
115 | 118 | "gridLineWidth": 0, |
116 | 119 | "lineWidth": 2, |
| 120 | + "lineColor": "#cccccc", |
| 121 | + "tickWidth": 0, |
117 | 122 | } |
118 | 123 |
|
119 | 124 | # Y-axis for distance |
120 | 125 | chart.options.y_axis = { |
121 | | - "title": {"text": "Distance (Ward)", "style": {"fontSize": "44px"}}, |
122 | | - "labels": {"style": {"fontSize": "32px"}}, |
123 | | - "gridLineColor": "rgba(0,0,0,0.1)", |
| 126 | + "title": {"text": "Distance (Ward)", "style": {"fontSize": "32px", "color": "#555"}}, |
| 127 | + "labels": {"style": {"fontSize": "24px", "color": "#444"}}, |
| 128 | + "gridLineColor": "rgba(0,0,0,0.08)", |
124 | 129 | "gridLineWidth": 1, |
| 130 | + "lineWidth": 2, |
| 131 | + "lineColor": "#cccccc", |
125 | 132 | "min": 0, |
| 133 | + "tickInterval": 1, |
126 | 134 | } |
127 | 135 |
|
128 | 136 | # Hide legend |
129 | 137 | chart.options.legend = {"enabled": False} |
130 | 138 |
|
131 | | -# Plot options for lines |
| 139 | +# Plot options |
132 | 140 | chart.options.plot_options = { |
133 | 141 | "line": {"lineWidth": 4, "marker": {"enabled": False}, "states": {"hover": {"enabled": False}}} |
134 | 142 | } |
135 | 143 |
|
136 | | -# Add each line segment as a series |
137 | | -for series_data in line_series_data: |
| 144 | +# Add each dendrogram branch as a colored series |
| 145 | +for xs, ys, color_key in zip(icoord, dcoord, color_list, strict=True): |
138 | 146 | series = LineSeries() |
139 | | - series.data = series_data["data"] |
140 | | - series.color = series_data["color"] |
| 147 | + series.data = [[xs[j], ys[j]] for j in range(4)] |
| 148 | + series.color = color_map.get(color_key, "#306998") |
141 | 149 | series.line_width = 4 |
142 | 150 | series.marker = {"enabled": False} |
143 | 151 | series.enable_mouse_tracking = False |
144 | 152 | chart.add_series(series) |
145 | 153 |
|
146 | 154 | # Download Highcharts JS for inline embedding |
147 | | -highcharts_url = "https://code.highcharts.com/highcharts.js" |
| 155 | +highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js" |
148 | 156 | with urllib.request.urlopen(highcharts_url, timeout=30) as response: |
149 | 157 | highcharts_js = response.read().decode("utf-8") |
150 | 158 |
|
151 | | -# Generate HTML with inline scripts |
| 159 | +# Generate JavaScript literal |
152 | 160 | html_str = chart.to_js_literal() |
153 | 161 |
|
154 | | -# Create JavaScript formatter function for x-axis labels |
| 162 | +# Create formatter function for x-axis labels |
155 | 163 | labels_js = str(ivl).replace("'", '"') |
156 | 164 | formatter_js = f"""function() {{ |
157 | 165 | var labels = {labels_js}; |
158 | 166 | var idx = Math.round((this.value - 5) / 10); |
159 | 167 | return (idx >= 0 && idx < labels.length) ? labels[idx] : ''; |
160 | 168 | }}""" |
161 | 169 |
|
162 | | -# Inject formatter into the xAxis labels configuration |
163 | | -# Find the rotation property in labels and add formatter after it |
164 | | -html_str = html_str.replace( |
165 | | - "rotation: 45,", |
166 | | - "rotation: 45,\nformatter: " + formatter_js + ",", |
167 | | - 1, # Replace only first occurrence |
168 | | -) |
| 170 | +# Inject formatter into xAxis labels configuration |
| 171 | +html_str = html_str.replace("rotation: 45,", "rotation: 45,\nformatter: " + formatter_js + ",", 1) |
169 | 172 |
|
170 | 173 | html_content = f"""<!DOCTYPE html> |
171 | 174 | <html> |
|
179 | 182 | </body> |
180 | 183 | </html>""" |
181 | 184 |
|
182 | | -# Save HTML file |
| 185 | +# Save HTML |
183 | 186 | with open("plot.html", "w", encoding="utf-8") as f: |
184 | 187 | f.write(html_content) |
185 | 188 |
|
186 | | -# Write temp HTML and take screenshot |
| 189 | +# Screenshot via headless Chrome |
187 | 190 | with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
188 | 191 | f.write(html_content) |
189 | 192 | temp_path = f.name |
|
199 | 202 | driver.get(f"file://{temp_path}") |
200 | 203 | time.sleep(5) |
201 | 204 |
|
202 | | -# Get container element and screenshot it for exact dimensions |
203 | 205 | container = driver.find_element("id", "container") |
204 | 206 | container.screenshot("plot.png") |
205 | 207 | driver.quit() |
|
0 commit comments