|
| 1 | +""" pyplots.ai |
| 2 | +column-stratigraphic: Stratigraphic Column with Lithology Patterns |
| 3 | +Library: highcharts unknown | Python 3.14.3 |
| 4 | +Quality: 90/100 | Created: 2026-03-15 |
| 5 | +""" |
| 6 | + |
| 7 | +import json |
| 8 | +import tempfile |
| 9 | +import time |
| 10 | +import urllib.request |
| 11 | +from collections import OrderedDict |
| 12 | +from pathlib import Path |
| 13 | + |
| 14 | +from highcharts_core.chart import Chart |
| 15 | +from selenium import webdriver |
| 16 | +from selenium.webdriver.chrome.options import Options |
| 17 | + |
| 18 | + |
| 19 | +# Data - Synthetic sedimentary section (Colorado Plateau stratigraphy) |
| 20 | +layers = [ |
| 21 | + {"top": 0, "bottom": 18, "lithology": "sandstone", "formation": "Dakota Sandstone", "age": "Cretaceous"}, |
| 22 | + {"top": 18, "bottom": 42, "lithology": "shale", "formation": "Morrison Formation", "age": "Jurassic"}, |
| 23 | + {"top": 42, "bottom": 58, "lithology": "limestone", "formation": "Sundance Formation", "age": "Jurassic"}, |
| 24 | + {"top": 58, "bottom": 68, "lithology": "siltstone", "formation": "Carmel Formation", "age": "Jurassic"}, |
| 25 | + {"top": 68, "bottom": 100, "lithology": "sandstone", "formation": "Navajo Sandstone", "age": "Jurassic"}, |
| 26 | + {"top": 100, "bottom": 118, "lithology": "conglomerate", "formation": "Kayenta Formation", "age": "Triassic"}, |
| 27 | + {"top": 118, "bottom": 142, "lithology": "shale", "formation": "Chinle Formation", "age": "Triassic"}, |
| 28 | + {"top": 142, "bottom": 162, "lithology": "limestone", "formation": "Kaibab Limestone", "age": "Permian"}, |
| 29 | + {"top": 162, "bottom": 185, "lithology": "sandstone", "formation": "Coconino Sandstone", "age": "Permian"}, |
| 30 | + {"top": 185, "bottom": 200, "lithology": "shale", "formation": "Hermit Formation", "age": "Permian"}, |
| 31 | +] |
| 32 | + |
| 33 | +# Lithology pattern definitions (for Highcharts pattern-fill module) |
| 34 | +lithology_config = { |
| 35 | + "sandstone": { |
| 36 | + "name": "Sandstone", |
| 37 | + "color": { |
| 38 | + "pattern": { |
| 39 | + "path": { |
| 40 | + "d": "M 3 3 a 1.5 1.5 0 1 0 0.01 0 M 9 8 a 1.5 1.5 0 1 0 0.01 0 " |
| 41 | + "M 6 1 a 1 1 0 1 0 0.01 0 M 1 10 a 1 1 0 1 0 0.01 0 " |
| 42 | + "M 11 4 a 1.2 1.2 0 1 0 0.01 0", |
| 43 | + "stroke": "#8B7355", |
| 44 | + "strokeWidth": 2.5, |
| 45 | + "fill": "#8B7355", |
| 46 | + }, |
| 47 | + "width": 14, |
| 48 | + "height": 14, |
| 49 | + "backgroundColor": "#F5DEB3", |
| 50 | + } |
| 51 | + }, |
| 52 | + }, |
| 53 | + "shale": { |
| 54 | + "name": "Shale", |
| 55 | + "color": { |
| 56 | + "pattern": { |
| 57 | + "path": { |
| 58 | + "d": "M 0 4 L 18 4 M 2 9 L 11 9 M 13 9 L 18 9 M 0 14 L 7 14 M 9 14 L 16 14", |
| 59 | + "stroke": "#555555", |
| 60 | + "strokeWidth": 1.8, |
| 61 | + }, |
| 62 | + "width": 18, |
| 63 | + "height": 18, |
| 64 | + "backgroundColor": "#B8B8B8", |
| 65 | + } |
| 66 | + }, |
| 67 | + }, |
| 68 | + "limestone": { |
| 69 | + "name": "Limestone", |
| 70 | + "color": { |
| 71 | + "pattern": { |
| 72 | + "path": { |
| 73 | + "d": "M 0 0 L 20 0 M 0 10 L 20 10 M 10 0 L 10 10 M 0 10 L 0 20 M 20 10 L 20 20", |
| 74 | + "stroke": "#4A7A8C", |
| 75 | + "strokeWidth": 2, |
| 76 | + }, |
| 77 | + "width": 20, |
| 78 | + "height": 20, |
| 79 | + "backgroundColor": "#B8D4E3", |
| 80 | + } |
| 81 | + }, |
| 82 | + }, |
| 83 | + "siltstone": { |
| 84 | + "name": "Siltstone", |
| 85 | + "color": { |
| 86 | + "pattern": { |
| 87 | + "path": { |
| 88 | + "d": "M 1 4 L 6 4 M 9 4 L 13 4 M 3 9 L 8 9 M 10 9 L 15 9 M 0 14 L 5 14 M 7 14 L 11 14", |
| 89 | + "stroke": "#7A6B54", |
| 90 | + "strokeWidth": 1.8, |
| 91 | + }, |
| 92 | + "width": 16, |
| 93 | + "height": 18, |
| 94 | + "backgroundColor": "#D2C4A5", |
| 95 | + } |
| 96 | + }, |
| 97 | + }, |
| 98 | + "conglomerate": { |
| 99 | + "name": "Conglomerate", |
| 100 | + "color": { |
| 101 | + "pattern": { |
| 102 | + "path": { |
| 103 | + "d": "M 8 8 m -5 0 a 5 5 0 1 0 10 0 a 5 5 0 1 0 -10 0 " |
| 104 | + "M 20 18 m -3.5 0 a 3.5 3.5 0 1 0 7 0 a 3.5 3.5 0 1 0 -7 0 " |
| 105 | + "M 19 5 m -2.5 0 a 2.5 2.5 0 1 0 5 0 a 2.5 2.5 0 1 0 -5 0", |
| 106 | + "stroke": "#5C3A6E", |
| 107 | + "strokeWidth": 2, |
| 108 | + "fill": "none", |
| 109 | + }, |
| 110 | + "width": 26, |
| 111 | + "height": 26, |
| 112 | + "backgroundColor": "#C4A0D4", |
| 113 | + } |
| 114 | + }, |
| 115 | + }, |
| 116 | +} |
| 117 | + |
| 118 | +# Group layers by lithology for legend |
| 119 | +lithology_groups = OrderedDict() |
| 120 | +for layer in layers: |
| 121 | + lith = layer["lithology"] |
| 122 | + if lith not in lithology_groups: |
| 123 | + lithology_groups[lith] = [] |
| 124 | + lithology_groups[lith].append(layer) |
| 125 | + |
| 126 | +# Age boundaries for geological period labels |
| 127 | +age_bands = [ |
| 128 | + {"from": 0, "to": 18, "label": "Cretaceous", "color": "rgba(76, 175, 80, 0.06)"}, |
| 129 | + {"from": 18, "to": 100, "label": "Jurassic", "color": "rgba(33, 150, 243, 0.06)"}, |
| 130 | + {"from": 100, "to": 142, "label": "Triassic", "color": "rgba(255, 152, 0, 0.06)"}, |
| 131 | + {"from": 142, "to": 200, "label": "Permian", "color": "rgba(156, 39, 176, 0.06)"}, |
| 132 | +] |
| 133 | + |
| 134 | +# Build chart configuration |
| 135 | +chart_config = { |
| 136 | + "chart": { |
| 137 | + "type": "columnrange", |
| 138 | + "width": 4800, |
| 139 | + "height": 2700, |
| 140 | + "backgroundColor": "#FAFAF8", |
| 141 | + "marginLeft": 340, |
| 142 | + "marginRight": 480, |
| 143 | + "marginTop": 200, |
| 144 | + "marginBottom": 130, |
| 145 | + "style": {"fontFamily": "'Segoe UI', Helvetica, Arial, sans-serif"}, |
| 146 | + }, |
| 147 | + "title": { |
| 148 | + "text": "column-stratigraphic \u00b7 highcharts \u00b7 pyplots.ai", |
| 149 | + "style": {"fontSize": "52px", "fontWeight": "bold", "color": "#2C3E50"}, |
| 150 | + }, |
| 151 | + "subtitle": { |
| 152 | + "text": "Synthetic Sedimentary Section \u2014 Colorado Plateau Stratigraphy", |
| 153 | + "style": {"fontSize": "34px", "color": "#7F8C8D"}, |
| 154 | + }, |
| 155 | + "xAxis": {"visible": False, "min": -0.25, "max": 1.3}, |
| 156 | + "yAxis": { |
| 157 | + "reversed": True, |
| 158 | + "title": { |
| 159 | + "text": "Depth (m)", |
| 160 | + "style": {"fontSize": "42px", "color": "#2C3E50", "fontWeight": "600"}, |
| 161 | + "margin": 30, |
| 162 | + }, |
| 163 | + "labels": {"style": {"fontSize": "32px", "color": "#444444"}, "format": "{value}"}, |
| 164 | + "min": 0, |
| 165 | + "max": 204, |
| 166 | + "tickInterval": 20, |
| 167 | + "gridLineWidth": 1, |
| 168 | + "gridLineColor": "rgba(0, 0, 0, 0.06)", |
| 169 | + "lineWidth": 2, |
| 170 | + "lineColor": "#CCCCCC", |
| 171 | + "plotBands": [ |
| 172 | + { |
| 173 | + "from": band["from"], |
| 174 | + "to": band["to"], |
| 175 | + "color": band["color"], |
| 176 | + "label": { |
| 177 | + "text": band["label"], |
| 178 | + "align": "left", |
| 179 | + "x": -160, |
| 180 | + "verticalAlign": "middle", |
| 181 | + "rotation": 270, |
| 182 | + "style": {"fontSize": "30px", "fontWeight": "bold", "color": "#555555"}, |
| 183 | + }, |
| 184 | + } |
| 185 | + for band in age_bands |
| 186 | + ], |
| 187 | + "plotLines": [ |
| 188 | + {"value": boundary, "color": "rgba(0, 0, 0, 0.35)", "width": 3, "dashStyle": "Dash", "zIndex": 5} |
| 189 | + for boundary in [18, 100, 142] |
| 190 | + ], |
| 191 | + }, |
| 192 | + "legend": { |
| 193 | + "enabled": True, |
| 194 | + "layout": "vertical", |
| 195 | + "align": "right", |
| 196 | + "verticalAlign": "middle", |
| 197 | + "x": -30, |
| 198 | + "y": 0, |
| 199 | + "itemStyle": {"fontSize": "30px", "color": "#333333"}, |
| 200 | + "symbolHeight": 28, |
| 201 | + "symbolWidth": 28, |
| 202 | + "itemMarginBottom": 18, |
| 203 | + "title": {"text": "Lithology", "style": {"fontSize": "34px", "fontWeight": "bold", "color": "#2C3E50"}}, |
| 204 | + "backgroundColor": "rgba(255, 255, 255, 0.7)", |
| 205 | + "borderRadius": 8, |
| 206 | + "padding": 20, |
| 207 | + }, |
| 208 | + "tooltip": { |
| 209 | + "style": {"fontSize": "26px"}, |
| 210 | + "headerFormat": "", |
| 211 | + "pointFormat": ( |
| 212 | + "<b>{point.custom.formation}</b><br/>" |
| 213 | + "{series.name}<br/>" |
| 214 | + "Depth: {point.low} \u2013 {point.high} m<br/>" |
| 215 | + "Thickness: {point.custom.thickness} m" |
| 216 | + ), |
| 217 | + }, |
| 218 | + "credits": {"enabled": False}, |
| 219 | + "plotOptions": {"columnrange": {"grouping": False, "borderRadius": 0}}, |
| 220 | + "series": [], |
| 221 | +} |
| 222 | + |
| 223 | +# Build series - one per lithology type |
| 224 | +for lith, layer_list in lithology_groups.items(): |
| 225 | + config = lithology_config[lith] |
| 226 | + data_points = [] |
| 227 | + for layer in layer_list: |
| 228 | + thickness = layer["bottom"] - layer["top"] |
| 229 | + data_points.append( |
| 230 | + { |
| 231 | + "x": 0, |
| 232 | + "low": layer["top"], |
| 233 | + "high": layer["bottom"], |
| 234 | + "custom": {"formation": layer["formation"], "thickness": thickness}, |
| 235 | + } |
| 236 | + ) |
| 237 | + |
| 238 | + chart_config["series"].append( |
| 239 | + { |
| 240 | + "type": "columnrange", |
| 241 | + "name": config["name"], |
| 242 | + "data": data_points, |
| 243 | + "color": config["color"], |
| 244 | + "borderColor": "#2C3E50", |
| 245 | + "borderWidth": 2.5, |
| 246 | + "pointWidth": 2200, |
| 247 | + } |
| 248 | + ) |
| 249 | + |
| 250 | +# Add formation name labels as a scatter series positioned to the right of the column |
| 251 | +formation_labels = [] |
| 252 | +for layer in layers: |
| 253 | + mid_depth = (layer["top"] + layer["bottom"]) / 2 |
| 254 | + formation_labels.append({"x": 0.68, "y": mid_depth, "name": layer["formation"]}) |
| 255 | + |
| 256 | +chart_config["series"].append( |
| 257 | + { |
| 258 | + "type": "scatter", |
| 259 | + "name": "Formations", |
| 260 | + "data": formation_labels, |
| 261 | + "showInLegend": False, |
| 262 | + "enableMouseTracking": False, |
| 263 | + "marker": {"enabled": False}, |
| 264 | + "dataLabels": { |
| 265 | + "enabled": True, |
| 266 | + "format": "{point.name}", |
| 267 | + "align": "left", |
| 268 | + "x": 10, |
| 269 | + "y": 4, |
| 270 | + "style": {"fontSize": "30px", "fontWeight": "600", "color": "#2C3E50", "textOutline": "3px #FAFAF8"}, |
| 271 | + "overflow": "allow", |
| 272 | + "crop": False, |
| 273 | + }, |
| 274 | + } |
| 275 | +) |
| 276 | + |
| 277 | +# Generate JavaScript - using raw JSON config because highcharts-core cannot |
| 278 | +# serialize SVG pattern path objects needed for lithology fills |
| 279 | +chart = Chart(container="container") |
| 280 | +js_config = json.dumps(chart_config, ensure_ascii=False) |
| 281 | +js_code = f"Highcharts.chart('{chart.container}', {js_config});" |
| 282 | + |
| 283 | +# Load Highcharts JS modules |
| 284 | +highcharts_base = Path(__file__).resolve().parents[3] / "node_modules" / "highcharts" |
| 285 | +modules = {} |
| 286 | +for name, filename in [ |
| 287 | + ("highcharts", "highcharts.js"), |
| 288 | + ("more", "highcharts-more.js"), |
| 289 | + ("pattern", "modules/pattern-fill.js"), |
| 290 | +]: |
| 291 | + local_path = highcharts_base / filename |
| 292 | + if local_path.exists(): |
| 293 | + modules[name] = local_path.read_text(encoding="utf-8") |
| 294 | + else: |
| 295 | + url = f"https://code.highcharts.com/{filename}" |
| 296 | + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) |
| 297 | + with urllib.request.urlopen(req, timeout=30) as response: |
| 298 | + modules[name] = response.read().decode("utf-8") |
| 299 | + |
| 300 | +html_content = f"""<!DOCTYPE html> |
| 301 | +<html> |
| 302 | +<head> |
| 303 | + <meta charset="utf-8"> |
| 304 | + <script>{modules["highcharts"]}</script> |
| 305 | + <script>{modules["more"]}</script> |
| 306 | + <script>{modules["pattern"]}</script> |
| 307 | +</head> |
| 308 | +<body style="margin:0;"> |
| 309 | + <div id="container" style="width: 4800px; height: 2700px;"></div> |
| 310 | + <script>{js_code}</script> |
| 311 | +</body> |
| 312 | +</html>""" |
| 313 | + |
| 314 | +# Screenshot with headless Chrome |
| 315 | +with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: |
| 316 | + f.write(html_content) |
| 317 | + temp_path = f.name |
| 318 | + |
| 319 | +chrome_options = Options() |
| 320 | +chrome_options.add_argument("--headless") |
| 321 | +chrome_options.add_argument("--no-sandbox") |
| 322 | +chrome_options.add_argument("--disable-dev-shm-usage") |
| 323 | +chrome_options.add_argument("--disable-gpu") |
| 324 | +chrome_options.add_argument("--window-size=4800,2700") |
| 325 | + |
| 326 | +driver = webdriver.Chrome(options=chrome_options) |
| 327 | +driver.get(f"file://{temp_path}") |
| 328 | +time.sleep(5) |
| 329 | + |
| 330 | +container = driver.find_element("id", "container") |
| 331 | +container.screenshot("plot.png") |
| 332 | +driver.quit() |
| 333 | + |
| 334 | +Path(temp_path).unlink() |
| 335 | + |
| 336 | +# Save interactive HTML |
| 337 | +with open("plot.html", "w", encoding="utf-8") as f: |
| 338 | + interactive_html = f"""<!DOCTYPE html> |
| 339 | +<html> |
| 340 | +<head> |
| 341 | + <meta charset="utf-8"> |
| 342 | + <script src="https://code.highcharts.com/highcharts.js"></script> |
| 343 | + <script src="https://code.highcharts.com/highcharts-more.js"></script> |
| 344 | + <script src="https://code.highcharts.com/modules/pattern-fill.js"></script> |
| 345 | +</head> |
| 346 | +<body style="margin:0;"> |
| 347 | + <div id="container" style="width: 100%; height: 100vh;"></div> |
| 348 | + <script>{js_code}</script> |
| 349 | +</body> |
| 350 | +</html>""" |
| 351 | + f.write(interactive_html) |
0 commit comments