|
| 1 | +""" pyplots.ai |
| 2 | +line-reaction-coordinate: Reaction Coordinate Energy Diagram |
| 3 | +Library: letsplot 4.9.0 | Python 3.14.3 |
| 4 | +Quality: 91/100 | Created: 2026-03-21 |
| 5 | +""" |
| 6 | + |
| 7 | +import numpy as np |
| 8 | +import pandas as pd |
| 9 | +from lets_plot import * # noqa: F403 |
| 10 | +from lets_plot.export import ggsave |
| 11 | + |
| 12 | + |
| 13 | +LetsPlot.setup_html() # noqa: F405 |
| 14 | + |
| 15 | +# Data - Single-step exothermic reaction energy profile |
| 16 | +reactant_energy = 50.0 |
| 17 | +transition_energy = 120.0 |
| 18 | +product_energy = 20.0 |
| 19 | + |
| 20 | +# Build smooth energy curve using Gaussian-based profile |
| 21 | +n_points = 300 |
| 22 | +reaction_coord = np.linspace(0, 1, n_points) |
| 23 | + |
| 24 | +# Piecewise smooth curve: Gaussian peak on sigmoid transition |
| 25 | +peak_pos = 0.45 |
| 26 | +sigma = 0.12 |
| 27 | +gaussian_peak = np.exp(-0.5 * ((reaction_coord - peak_pos) / sigma) ** 2) |
| 28 | + |
| 29 | +# Smooth sigmoid transition from reactant to product level |
| 30 | +transition = 1 / (1 + np.exp(-20 * (reaction_coord - 0.6))) |
| 31 | +base_energy = reactant_energy * (1 - transition) + product_energy * transition |
| 32 | + |
| 33 | +# Add the activation barrier |
| 34 | +barrier_height = transition_energy - ( |
| 35 | + reactant_energy * (1 - 1 / (1 + np.exp(-20 * (peak_pos - 0.6)))) |
| 36 | + + product_energy * (1 / (1 + np.exp(-20 * (peak_pos - 0.6)))) |
| 37 | +) |
| 38 | +energy = base_energy + barrier_height * gaussian_peak |
| 39 | + |
| 40 | +# Flatten plateaus at start and end |
| 41 | +energy[reaction_coord < 0.1] = reactant_energy |
| 42 | +energy[reaction_coord > 0.9] = product_energy |
| 43 | + |
| 44 | +df = pd.DataFrame({"reaction_coordinate": reaction_coord, "energy": energy}) |
| 45 | + |
| 46 | +# Key values |
| 47 | +ea = transition_energy - reactant_energy |
| 48 | +delta_h = product_energy - reactant_energy |
| 49 | + |
| 50 | +# Shaded region under curve for visual richness |
| 51 | +area_df = df.copy() |
| 52 | + |
| 53 | +# Colorblind-safe palette: blue (#306998) and orange (#E67E22) instead of red-green |
| 54 | +ea_color = "#D35400" # deep orange for Ea |
| 55 | +dh_color = "#2471A3" # steel blue for ΔH |
| 56 | +curve_color = "#306998" # Python blue |
| 57 | +label_color = "#2C3E50" # dark slate |
| 58 | + |
| 59 | +# Horizontal reference lines at energy levels |
| 60 | +hline_df = pd.DataFrame( |
| 61 | + { |
| 62 | + "x": [0.0, 0.0], |
| 63 | + "xend": [1.0, 1.0], |
| 64 | + "y": [reactant_energy, product_energy], |
| 65 | + "yend": [reactant_energy, product_energy], |
| 66 | + } |
| 67 | +) |
| 68 | + |
| 69 | +# Ea arrow segments (double-headed) |
| 70 | +ea_arrow_df = pd.DataFrame( |
| 71 | + { |
| 72 | + "x": [0.20, 0.20], |
| 73 | + "y": [reactant_energy + 2, transition_energy - 2], |
| 74 | + "xend": [0.20, 0.20], |
| 75 | + "yend": [transition_energy - 2, reactant_energy + 2], |
| 76 | + } |
| 77 | +) |
| 78 | + |
| 79 | +# ΔH arrow segments (double-headed) |
| 80 | +dh_arrow_df = pd.DataFrame( |
| 81 | + { |
| 82 | + "x": [0.80, 0.80], |
| 83 | + "y": [reactant_energy - 2, product_energy + 2], |
| 84 | + "xend": [0.80, 0.80], |
| 85 | + "yend": [product_energy + 2, reactant_energy - 2], |
| 86 | + } |
| 87 | +) |
| 88 | + |
| 89 | +# Build plot with lets-plot distinctive features |
| 90 | +plot = ( |
| 91 | + ggplot(df, aes(x="reaction_coordinate", y="energy")) # noqa: F405 |
| 92 | + # Shaded area under the energy curve using geom_area |
| 93 | + + geom_area(fill=curve_color, alpha=0.08) # noqa: F405 |
| 94 | + # Horizontal dashed reference lines |
| 95 | + + geom_segment( # noqa: F405 |
| 96 | + data=hline_df, |
| 97 | + mapping=aes(x="x", xend="xend", y="y", yend="yend"), # noqa: F405 |
| 98 | + linetype="dashed", |
| 99 | + color="#B0B0B0", |
| 100 | + size=0.7, |
| 101 | + ) |
| 102 | + # Main energy curve - prominent |
| 103 | + + geom_line(color=curve_color, size=2.5) # noqa: F405 |
| 104 | + # Ea double-headed arrow (orange - colorblind safe) |
| 105 | + + geom_segment( # noqa: F405 |
| 106 | + data=ea_arrow_df, |
| 107 | + mapping=aes(x="x", xend="xend", y="y", yend="yend"), # noqa: F405 |
| 108 | + color=ea_color, |
| 109 | + size=1.3, |
| 110 | + arrow=arrow(length=10, type="open"), # noqa: F405 |
| 111 | + ) |
| 112 | + # ΔH double-headed arrow (steel blue - colorblind safe) |
| 113 | + + geom_segment( # noqa: F405 |
| 114 | + data=dh_arrow_df, |
| 115 | + mapping=aes(x="x", xend="xend", y="y", yend="yend"), # noqa: F405 |
| 116 | + color=dh_color, |
| 117 | + size=1.3, |
| 118 | + arrow=arrow(length=10, type="open"), # noqa: F405 |
| 119 | + ) |
| 120 | + # Labels using geom_label (lets-plot distinctive: label_padding, label_r) |
| 121 | + + geom_label( # noqa: F405 |
| 122 | + data=pd.DataFrame({"x": [0.08], "y": [reactant_energy - 8], "label": ["Reactants\n50 kJ/mol"]}), |
| 123 | + mapping=aes(x="x", y="y", label="label"), # noqa: F405 |
| 124 | + size=15, |
| 125 | + color=label_color, |
| 126 | + fill="#F8F9FA", |
| 127 | + alpha=0.85, |
| 128 | + label_padding=0.4, |
| 129 | + label_r=0.3, |
| 130 | + label_size=0, |
| 131 | + ) |
| 132 | + + geom_label( # noqa: F405 |
| 133 | + data=pd.DataFrame({"x": [peak_pos], "y": [transition_energy + 8], "label": ["Transition State\n120 kJ/mol"]}), |
| 134 | + mapping=aes(x="x", y="y", label="label"), # noqa: F405 |
| 135 | + size=15, |
| 136 | + color=label_color, |
| 137 | + fill="#F8F9FA", |
| 138 | + alpha=0.85, |
| 139 | + label_padding=0.4, |
| 140 | + label_r=0.3, |
| 141 | + label_size=0, |
| 142 | + ) |
| 143 | + + geom_label( # noqa: F405 |
| 144 | + data=pd.DataFrame({"x": [0.92], "y": [product_energy - 8], "label": ["Products\n20 kJ/mol"]}), |
| 145 | + mapping=aes(x="x", y="y", label="label"), # noqa: F405 |
| 146 | + size=15, |
| 147 | + color=label_color, |
| 148 | + fill="#F8F9FA", |
| 149 | + alpha=0.85, |
| 150 | + label_padding=0.4, |
| 151 | + label_r=0.3, |
| 152 | + label_size=0, |
| 153 | + ) |
| 154 | + # Energy annotation labels with colored backgrounds matching their arrows |
| 155 | + + geom_label( # noqa: F405 |
| 156 | + data=pd.DataFrame( |
| 157 | + {"x": [0.20], "y": [(reactant_energy + transition_energy) / 2], "label": [f"Ea = {ea:.0f} kJ/mol"]} |
| 158 | + ), |
| 159 | + mapping=aes(x="x", y="y", label="label"), # noqa: F405 |
| 160 | + size=16, |
| 161 | + color="#FFFFFF", |
| 162 | + fill=ea_color, |
| 163 | + alpha=0.9, |
| 164 | + label_padding=0.5, |
| 165 | + label_r=0.3, |
| 166 | + label_size=0, |
| 167 | + fontface="bold", |
| 168 | + ) |
| 169 | + + geom_label( # noqa: F405 |
| 170 | + data=pd.DataFrame( |
| 171 | + {"x": [0.80], "y": [(reactant_energy + product_energy) / 2], "label": [f"ΔH = {delta_h:.0f} kJ/mol"]} |
| 172 | + ), |
| 173 | + mapping=aes(x="x", y="y", label="label"), # noqa: F405 |
| 174 | + size=16, |
| 175 | + color="#FFFFFF", |
| 176 | + fill=dh_color, |
| 177 | + alpha=0.9, |
| 178 | + label_padding=0.5, |
| 179 | + label_r=0.3, |
| 180 | + label_size=0, |
| 181 | + fontface="bold", |
| 182 | + ) |
| 183 | + # Scales |
| 184 | + + scale_x_continuous( # noqa: F405 |
| 185 | + name="Reaction Coordinate", breaks=[], expand=[0.02, 0.02] |
| 186 | + ) |
| 187 | + + scale_y_continuous( # noqa: F405 |
| 188 | + name="Potential Energy (kJ/mol)", limits=[0, 145] |
| 189 | + ) |
| 190 | + + labs(title="line-reaction-coordinate · letsplot · pyplots.ai") # noqa: F405 |
| 191 | + + coord_cartesian(ylim=[0, 145]) # noqa: F405 |
| 192 | + + ggsize(1600, 900) # noqa: F405 |
| 193 | + # Lets-plot distinctive: flavor for base styling + element_geom for global defaults |
| 194 | + + flavor_high_contrast_light() # noqa: F405 |
| 195 | + + theme( # noqa: F405 |
| 196 | + axis_text=element_text(size=16, color="#555555"), # noqa: F405 |
| 197 | + axis_title=element_text(size=20, color="#333333"), # noqa: F405 |
| 198 | + plot_title=element_text(size=24, hjust=0.5, color="#2C3E50", face="bold"), # noqa: F405 |
| 199 | + axis_text_x=element_blank(), # noqa: F405 |
| 200 | + axis_ticks_x=element_blank(), # noqa: F405 |
| 201 | + axis_line_x=element_line(color="#CCCCCC", size=0.6), # noqa: F405 |
| 202 | + axis_line_y=element_line(color="#CCCCCC", size=0.6), # noqa: F405 |
| 203 | + panel_grid_major_y=element_line(color="#EEEEEE", size=0.3), # noqa: F405 |
| 204 | + panel_grid_minor=element_blank(), # noqa: F405 |
| 205 | + panel_grid_major_x=element_blank(), # noqa: F405 |
| 206 | + legend_position="none", |
| 207 | + plot_background=element_rect(fill="#FFFFFF", color="#FFFFFF"), # noqa: F405 |
| 208 | + panel_background=element_rect(fill="#FAFBFC", color="#FAFBFC"), # noqa: F405 |
| 209 | + ) |
| 210 | +) |
| 211 | + |
| 212 | +# Save |
| 213 | +ggsave(plot, filename="plot.png", path=".", scale=3) |
| 214 | +ggsave(plot, filename="plot.html", path=".") |
0 commit comments