valyte 0.1.8__py3-none-any.whl → 0.1.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
valyte/dos_plot.py CHANGED
@@ -1,10 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- DOS Plotting Module
4
- ===================
5
-
6
- Handles Density of States (DOS) plotting with gradient fills and smart legend.
7
- """
2
+ """DOS plotting utilities."""
8
3
 
9
4
  import os
10
5
  import numpy as np
@@ -19,128 +14,75 @@ from pymatgen.io.vasp import Vasprun
19
14
  from pymatgen.electronic_structure.core import Spin
20
15
 
21
16
 
22
- # ===============================================================
23
- # Gradient fill aesthetic
24
- # ===============================================================
25
17
  def gradient_fill(x, y, ax=None, color=None, xlim=None, **kwargs):
26
- """
27
- Fills the area under a curve with a vertical gradient.
28
-
29
- Args:
30
- x (array-like): X-axis data (Energy).
31
- y (array-like): Y-axis data (DOS).
32
- ax (matplotlib.axes.Axes, optional): The axes to plot on. Defaults to current axes.
33
- color (str, optional): The base color for the gradient.
34
- xlim (tuple, optional): X-axis limits to restrict gradient fill.
35
- **kwargs: Additional arguments passed to ax.plot.
36
-
37
- Returns:
38
- matplotlib.lines.Line2D: The line object representing the curve.
39
- """
18
+ """Fill area under a curve with a vertical gradient."""
40
19
  if ax is None:
41
20
  ax = plt.gca()
42
-
43
- # Don't filter by xlim - use full data range for better appearance
21
+
44
22
  if len(x) == 0 or len(y) == 0:
45
23
  return None
46
-
47
- # Plot the main line
24
+
48
25
  line, = ax.plot(x, y, color=color, lw=2, **kwargs)
49
-
50
- # Determine fill color and alpha
26
+
51
27
  fill_color = line.get_color() if color is None else color
52
28
  alpha = line.get_alpha() or 1.0
53
29
  zorder = line.get_zorder()
54
30
 
55
- # Create a gradient image with more aggressive alpha
56
31
  z = np.empty((100, 1, 4))
57
32
  rgb = mcolors.to_rgb(fill_color)
58
33
  z[:, :, :3] = rgb
59
-
60
- # Gradient Logic based on relative height
61
- # We want opacity to be proportional to height relative to the max visible value (ymax_ref)
62
-
63
- # Create normalized alpha gradient (0 to 1)
64
- # We map y-values to alpha values.
65
- # Since imshow fills a rectangle, we create a vertical gradient
66
- # and clip it later.
67
-
68
- # Opacity range: 0.05 (at axis) to 0.95 (at max visible height)
69
- # This ensures "Darker colour at the top"
34
+
70
35
  min_alpha = 0.05
71
36
  max_alpha = 0.95
72
-
73
- # Create the gradient array (vertical)
74
- # 0 is bottom, 1 is top
75
37
  gradient_vector = np.linspace(min_alpha, max_alpha, 100)
76
-
77
- # IMPORTANT: Restore alpha scaling so total DOS (alpha=0.15) stays faint
78
38
  gradient_vector *= alpha
79
-
80
- # If data is negative (Spin Down), we want opaque at bottom (peak) and transparent at top (axis)
39
+
81
40
  if np.mean(y) < 0:
82
41
  gradient_vector = gradient_vector[::-1]
83
-
42
+
84
43
  z[:, :, -1] = gradient_vector[:, None]
85
-
44
+
86
45
  xmin, xmax = x.min(), x.max()
87
-
88
- # Determine extent based on LOCAL curve limits
89
- # User requested "each curve should have its own gradient"
90
- # So we scale from 0 to curve.max()
91
-
46
+
92
47
  local_ymax = max(y.max(), abs(y.min()))
93
- if local_ymax == 0: local_ymax = 1.0 # Prevent singular extent
94
-
48
+ if local_ymax == 0:
49
+ local_ymax = 1.0
50
+
95
51
  if np.mean(y) < 0:
96
- # Spin Down: Extent from -local_ymax to 0
97
52
  extent_ymin = -local_ymax
98
53
  extent_ymax = 0
99
54
  else:
100
- # Spin Up: Extent from 0 to local_ymax
101
55
  extent_ymin = 0
102
56
  extent_ymax = local_ymax
103
-
104
- # Display the gradient image
105
- # We use the explicit extent calculated above
106
- im = ax.imshow(z, aspect="auto", extent=[xmin, xmax, extent_ymin, extent_ymax],
107
- origin="lower", zorder=zorder)
108
-
109
- # Clip the gradient to the area under the curve
110
- # We need to close the polygon at y=0
57
+
58
+ im = ax.imshow(
59
+ z,
60
+ aspect="auto",
61
+ extent=[xmin, xmax, extent_ymin, extent_ymax],
62
+ origin="lower",
63
+ zorder=zorder,
64
+ )
65
+
111
66
  xy = np.column_stack([x, y])
112
-
113
- # Construct polygon vertices:
114
- # Start at (xmin, 0), go along curve (x, y), end at (xmax, 0), close back to start
115
67
  verts = np.vstack([[x[0], 0], xy, [x[-1], 0], [x[0], 0]])
116
-
68
+
117
69
  clip = Polygon(verts, lw=0, facecolor="none", closed=True)
118
70
  ax.add_patch(clip)
119
71
  im.set_clip_path(clip)
120
-
72
+
121
73
  return line
122
74
 
123
75
 
124
- # ===============================================================
125
- # Data container
126
- # ===============================================================
127
76
  class ValyteDos:
128
- """
129
- Container for Total DOS data.
130
-
131
- Attributes:
132
- energies (np.ndarray): Array of energy values (shifted by Fermi energy).
133
- densities (dict): Dictionary of {Spin: np.ndarray} for total DOS.
134
- efermi (float): Fermi energy.
135
- """
77
+ """Container for total DOS data."""
78
+
136
79
  def __init__(self, energies, densities, efermi):
137
80
  self.energies = np.array(energies)
138
81
  self.densities = densities
139
82
  self.efermi = float(efermi)
140
-
83
+
141
84
  @property
142
85
  def total(self):
143
- """Returns the sum of all spin channels."""
144
86
  tot = np.zeros_like(self.energies)
145
87
  for spin in self.densities:
146
88
  tot += self.densities[spin]
@@ -155,144 +97,91 @@ class ValyteDos:
155
97
  return self.densities.get(Spin.down, np.zeros_like(self.energies))
156
98
 
157
99
 
158
- # ===============================================================
159
- # Load DOS
160
- # ===============================================================
161
100
  def load_dos(vasprun, elements=None, **_):
162
- """
163
- Loads DOS data from a vasprun.xml file using pymatgen.
164
-
165
- Args:
166
- vasprun (str): Path to the vasprun.xml file or directory containing it.
167
- elements (list or dict, optional): Specific elements to extract PDOS for.
168
-
169
- Returns:
170
- tuple: (ValyteDos object, dict of PDOS data)
171
- """
172
-
173
- # Handle directory input
101
+ """Load total and projected DOS from a vasprun.xml file."""
174
102
  if os.path.isdir(vasprun):
175
103
  vasprun = os.path.join(vasprun, "vasprun.xml")
176
-
104
+
177
105
  if not os.path.exists(vasprun):
178
106
  raise FileNotFoundError(f"{vasprun} not found")
179
107
 
180
- # Parse VASP output
181
108
  vr = Vasprun(vasprun)
182
109
  dos = vr.complete_dos
183
-
184
- # Get Fermi Energy
185
110
  efermi = dos.efermi
186
-
187
- # Attempt to align VBM to 0 for insulators/semiconductors
111
+
188
112
  try:
189
- # Try using BandStructure first (more robust)
190
113
  bs = vr.get_band_structure()
191
114
  if not bs.is_metal():
192
115
  efermi = bs.get_vbm()["energy"]
193
116
  except Exception:
194
- # Fallback to DOS-based detection
195
117
  try:
196
118
  cbm, vbm = dos.get_cbm_vbm()
197
- if cbm - vbm > 0.01: # Band gap detected
119
+ if cbm - vbm > 0.01:
198
120
  efermi = vbm
199
121
  except Exception:
200
122
  pass
201
-
202
- # Shift energies to set reference at 0
203
- energies = dos.energies - efermi
204
123
 
205
- # Extract Projected DOS
124
+ energies = dos.energies - efermi
206
125
  pdos = get_pdos(dos, elements)
207
-
126
+
208
127
  return ValyteDos(energies, dos.densities, efermi), pdos
209
128
 
210
129
 
211
- # ===============================================================
212
- # Extract PDOS
213
- # ===============================================================
214
130
  def get_pdos(dos, elements=None):
215
- """
216
- Extracts Projected DOS (PDOS) for specified elements.
217
-
218
- Args:
219
- dos (pymatgen.electronic_structure.dos.CompleteDos): The complete DOS object.
220
- elements (list or dict, optional): Elements to extract. If None, extracts all.
221
-
222
- Returns:
223
- dict: A dictionary where keys are element symbols and values are dicts of orbital DOS.
224
- pdos[element][orbital] = {Spin.up: array, Spin.down: array}
225
- """
131
+ """Extract projected DOS for specified elements."""
226
132
  structure = dos.structure
227
133
  symbols = [str(site.specie) for site in structure]
228
-
229
- # If no elements specified, use all unique elements in the structure
134
+
230
135
  if not elements:
231
136
  unique = sorted(set(symbols))
232
137
  elements = {el: () for el in unique}
233
138
  else:
234
- # Ensure elements is a dict if passed as list
235
139
  if isinstance(elements, list):
236
- elements = {el: () for el in elements}
140
+ elements = {el: () for el in elements}
237
141
 
238
142
  pdos = {}
239
143
  for el in elements:
240
- # Find all sites corresponding to this element
241
144
  el_sites = [s for s in structure if str(s.specie) == el]
242
145
  el_pdos = {}
243
-
146
+
244
147
  for site in el_sites:
245
148
  try:
246
149
  site_dos = dos.get_site_spd_dos(site)
247
150
  except Exception:
248
151
  continue
249
-
250
- # Sum up contributions from orbitals (s, p, d, f)
152
+
251
153
  for orb, orb_dos in site_dos.items():
252
- label = orb.name[0] # e.g., 's', 'p', 'd'
253
-
254
- # Initialize dictionaries for spins if not exists
154
+ label = orb.name[0]
255
155
  if label not in el_pdos:
256
156
  el_pdos[label] = {}
257
-
157
+
258
158
  for spin in orb_dos.densities:
259
159
  if spin not in el_pdos[label]:
260
160
  el_pdos[label][spin] = np.zeros_like(dos.energies)
261
161
  el_pdos[label][spin] += orb_dos.densities[spin]
262
-
162
+
263
163
  pdos[el] = el_pdos
264
164
  return pdos
265
165
 
266
166
 
267
- # ===============================================================
268
- # Plotting with smart legend & font control (Valyte theme)
269
- # ===============================================================
270
- def plot_dos(dos, pdos, out="valyte_dos.png",
271
- xlim=(-6, 6), ylim=None, figsize=(5, 4),
272
- dpi=400, legend_loc="auto", font="Arial",
273
- show_fermi=False, show_total=True, plotting_config=None,
274
- legend_cutoff=0.10, scale_factor=1.0):
275
- """
276
- Plots the Total and Projected DOS with the Valyte visual style.
277
-
278
- Args:
279
- dos (ValyteDos): The total DOS data.
280
- pdos (dict): The projected DOS data.
281
- out (str): Output filename.
282
- xlim (tuple): Energy range (min, max).
283
- ylim (tuple, optional): DOS range (min, max).
284
- figsize (tuple): Figure size in inches.
285
- dpi (int): Resolution of the output image.
286
- legend_loc (str): Legend location strategy.
287
- font (str): Font family to use.
288
- show_fermi (bool): Whether to draw a dashed line at the Fermi level (E=0).
289
- show_total (bool): Whether to plot the Total DOS.
290
- plotting_config (list): List of (Element, Orbital) tuples to plot.
291
- legend_cutoff (float): Threshold (as fraction) for showing items in legend (default: 0.10).
292
- scale_factor (float): Factor to scale the Y-axis limits (zoom in).
293
- """
294
-
295
- # --- Font configuration ---
167
+ def plot_dos(
168
+ dos,
169
+ pdos,
170
+ out="valyte_dos.png",
171
+ xlim=(-6, 6),
172
+ ylim=None,
173
+ figsize=(5, 4),
174
+ dpi=400,
175
+ legend_loc="auto",
176
+ font="Arial",
177
+ show_fermi=False,
178
+ show_total=True,
179
+ plotting_config=None,
180
+ legend_cutoff=0.10,
181
+ scale_factor=1.0,
182
+ ):
183
+ """Plot total and projected DOS with the Valyte style."""
184
+
296
185
  font_map = {
297
186
  "arial": "Arial",
298
187
  "helvetica": "Helvetica",
@@ -307,235 +196,177 @@ def plot_dos(dos, pdos, out="valyte_dos.png",
307
196
 
308
197
  plt.style.use("default")
309
198
  fig, ax = plt.subplots(figsize=figsize)
310
-
311
- # Check if spin-polarized
199
+
312
200
  is_spin_polarized = Spin.down in dos.densities
313
-
314
- # Fermi level line (optional)
201
+
315
202
  if show_fermi:
316
203
  ax.axvline(0, color="k", lw=0.8, ls="--", alpha=0.7)
317
-
318
- # Zero line for spin polarized plots
204
+
319
205
  if is_spin_polarized:
320
206
  ax.axhline(0, color="k", lw=0.5, alpha=1.0)
321
207
 
322
- # Color palette for elements
323
- # Expanded color palette for better distinction
324
- # Reordered to maximize contrast between consecutive items
325
208
  palette = [
326
- "#4b0082", # Indigo
327
- "#0096c7", # Cyan
328
- "#e63946", # Red
329
- "#023e8a", # Royal Blue
330
- "#ffb703", # Yellow
331
- "#2a9d8f", # Teal
332
- "#8e44ad", # Purple
333
- "#118ab2", # Light Blue
334
- "#d62828", # Dark Red
335
- "#00b4d8", # Sky Blue
336
- "#f4a261", # Orange
337
- "#003049", # Dark Blue
338
- "#6a994e", # Green
339
- "#48cae4", # Light Cyan
340
- "#0077b6", # Blue
341
- "#90e0ef", # Pale Blue
342
- "#ade8f4", # Very Pale Blue
343
- "#caf0f8" # White Blue
209
+ "#4b0082",
210
+ "#0096c7",
211
+ "#e63946",
212
+ "#023e8a",
213
+ "#ffb703",
214
+ "#2a9d8f",
215
+ "#8e44ad",
216
+ "#118ab2",
217
+ "#d62828",
218
+ "#00b4d8",
219
+ "#f4a261",
220
+ "#003049",
221
+ "#6a994e",
222
+ "#48cae4",
223
+ "#0077b6",
224
+ "#90e0ef",
225
+ "#ade8f4",
226
+ "#caf0f8",
344
227
  ]
345
228
  lines, labels = [], []
346
-
347
- # Determine mask for visible x-range (used for scaling and legend)
229
+
348
230
  x_mask = (dos.energies >= xlim[0]) & (dos.energies <= xlim[1])
349
231
 
350
- # Determine what to plot
351
232
  if plotting_config:
352
233
  items_to_plot = plotting_config
353
234
  else:
354
- # Default: Plot all orbitals for each loaded element
355
235
  items_to_plot = []
356
236
  for el, el_pdos in pdos.items():
357
237
  for orb in el_pdos.keys():
358
238
  items_to_plot.append((el, orb))
359
239
 
360
- # Plot PDOS
361
- max_visible_y = 0 # Track maximum Y value in visible range
362
- min_visible_y = 0 # Track minimum Y value (for spin down)
363
-
240
+ max_visible_y = 0
241
+ min_visible_y = 0
242
+
364
243
  for i, (el, orb) in enumerate(items_to_plot):
365
244
  if el not in pdos:
366
245
  continue
367
-
368
- # Assign unique color for each orbital contribution
246
+
369
247
  c = palette[i % len(palette)]
370
-
371
- # Prepare data for plotting
372
- # y_data_up and y_data_down
373
-
374
- if orb == 'total':
375
- # Sum all orbitals for this element
248
+
249
+ if orb == "total":
376
250
  y_up = np.zeros_like(dos.energies)
377
251
  y_down = np.zeros_like(dos.energies)
378
-
379
252
  for o_data in pdos[el].values():
380
253
  y_up += o_data.get(Spin.up, np.zeros_like(dos.energies))
381
254
  y_down += o_data.get(Spin.down, np.zeros_like(dos.energies))
382
-
383
255
  label = el
384
256
  else:
385
- if orb in pdos[el]:
386
- y_up = pdos[el][orb].get(Spin.up, np.zeros_like(dos.energies))
387
- y_down = pdos[el][orb].get(Spin.down, np.zeros_like(dos.energies))
388
- label = f"{el}({orb})"
389
- else:
257
+ if orb not in pdos[el]:
390
258
  continue
391
-
392
- # Invert spin down
259
+ y_up = pdos[el][orb].get(Spin.up, np.zeros_like(dos.energies))
260
+ y_down = pdos[el][orb].get(Spin.down, np.zeros_like(dos.energies))
261
+ label = f"{el}({orb})"
262
+
393
263
  y_down = -y_down
394
-
395
- # Check contribution in visible range
264
+
396
265
  visible_y_up = y_up[x_mask]
397
266
  visible_y_down = y_down[x_mask]
398
-
267
+
399
268
  has_visible_data = False
400
269
  current_max_y = 0
401
-
270
+
402
271
  if len(visible_y_up) > 0:
403
272
  max_y = np.max(visible_y_up)
404
273
  max_visible_y = max(max_visible_y, max_y)
405
274
  current_max_y = max(current_max_y, max_y)
406
- if max_y > 1e-6: has_visible_data = True
407
-
275
+ if max_y > 1e-6:
276
+ has_visible_data = True
277
+
408
278
  if is_spin_polarized and len(visible_y_down) > 0:
409
279
  min_y = np.min(visible_y_down)
410
280
  min_visible_y = min(min_visible_y, min_y)
411
281
  current_max_y = max(current_max_y, abs(min_y))
412
- if abs(min_y) > 1e-6: has_visible_data = True
282
+ if abs(min_y) > 1e-6:
283
+ has_visible_data = True
413
284
 
414
- # Store for later threshold check and plotting
415
- # We plot a dummy line for the legend
416
285
  line, = ax.plot(dos.energies, y_up, lw=1.5, color=c, label=label, alpha=0)
417
- lines.append({
418
- 'line': line,
419
- 'y_up': y_up,
420
- 'y_down': y_down,
421
- 'max_y': current_max_y,
422
- 'color': c,
423
- 'label': label
424
- })
425
-
426
- # Calculate threshold (legend_cutoff of max visible)
427
- # Use the overall max absolute value found
286
+ lines.append(
287
+ {
288
+ "line": line,
289
+ "y_up": y_up,
290
+ "y_down": y_down,
291
+ "max_y": current_max_y,
292
+ "color": c,
293
+ "label": label,
294
+ "has_visible": has_visible_data,
295
+ }
296
+ )
297
+
428
298
  global_max = max(max_visible_y, abs(min_visible_y))
429
299
  threshold = legend_cutoff * global_max
430
-
431
- # Auto-scale Y-axis calculation (to determine ymax_ref)
432
- if ylim:
433
- pass # ymax_ref = ylim[1] (Unused)
434
- else:
435
- # Determine likely ymax based on logic later in the function
436
- pass
437
300
 
438
- # Filter legend items but keep all plot lines
439
301
  final_lines = []
440
302
  final_labels = []
441
-
303
+
442
304
  for item in lines:
443
- line = item['line']
444
- y_up = item['y_up']
445
- y_down = item['y_down']
446
- c = item['color']
447
- label = item['label']
448
- max_y = item['max_y']
449
-
450
- # Always plot the line (make it visible)
305
+ line = item["line"]
306
+ y_up = item["y_up"]
307
+ y_down = item["y_down"]
308
+ c = item["color"]
309
+ label = item["label"]
310
+ max_y = item["max_y"]
311
+ has_visible = item["has_visible"]
312
+
451
313
  line.set_alpha(1.0)
452
-
453
- # Apply gradient fill for visible lines
454
- # Spin Up
314
+
455
315
  gradient_fill(dos.energies, y_up, ax=ax, color=c, alpha=0.9)
456
-
457
- # Spin Down
458
316
  if is_spin_polarized:
459
- gradient_fill(dos.energies, y_down, ax=ax, color=c, alpha=0.9)
460
-
461
- # Only add to legend if above main threshold
462
- if max_y >= threshold:
317
+ gradient_fill(dos.energies, y_down, ax=ax, color=c, alpha=0.9)
318
+
319
+ if has_visible and max_y >= threshold:
463
320
  final_lines.append(line)
464
321
  final_labels.append(label)
465
-
466
- # Update lines and labels for legend creation
322
+
467
323
  lines = final_lines
468
324
  labels = final_labels
469
325
 
470
- # Plot Total DOS
471
326
  if show_total:
472
327
  y_total_up = dos.spin_up
473
328
  y_total_down = -dos.spin_down
474
-
329
+
475
330
  ax.plot(dos.energies, y_total_up, color="k", lw=1.2, label="Total DOS")
476
- # Use ymax_ref here too, assuming we want scaling relative to frame too
477
- # But Total DOS is often much larger. Maybe keep it separate max?
478
- # User said "Maximum value after scaling". So consistent reference is good.
479
331
  gradient_fill(dos.energies, y_total_up, ax=ax, color="k", alpha=0.15)
480
-
332
+
481
333
  if is_spin_polarized:
482
334
  ax.plot(dos.energies, y_total_down, color="k", lw=1.2)
483
335
  gradient_fill(dos.energies, y_total_down, ax=ax, color="k", alpha=0.15)
484
-
485
- # Update max/min range for auto-scaling
336
+
486
337
  visible_total_up = y_total_up[x_mask]
487
338
  visible_total_down = y_total_down[x_mask]
488
339
  if len(visible_total_up) > 0:
489
340
  max_visible_y = max(max_visible_y, np.max(visible_total_up))
490
341
  if len(visible_total_down) > 0:
491
342
  min_visible_y = min(min_visible_y, np.min(visible_total_down))
492
-
493
- # Auto-scale Y-axis based on visible range if ylim not provided
343
+
494
344
  if not ylim:
495
345
  if max_visible_y > 0 or min_visible_y < 0:
496
- # Apply scaling factor to the limit (zoom in)
497
346
  upper_limit = (max_visible_y * 1.1) / scale_factor
498
347
  lower_limit = (min_visible_y * 1.1) / scale_factor if is_spin_polarized else 0
499
-
500
- # If spin polarized, maybe make symmetric if user wants?
501
- # For now, let's just use the data range.
502
- # But often symmetric is nicer. Let's stick to data range for now.
503
-
504
348
  ax.set_ylim(lower_limit, upper_limit)
505
349
  else:
506
350
  ax.set_ylim(*ylim)
507
351
 
508
- # Axis settings
509
352
  ax.set_xlim(*xlim)
510
353
  ax.set_xlabel("Energy (eV)", fontsize=14, weight="bold", labelpad=6)
511
354
  ax.set_ylabel("Density of States", fontsize=14, weight="bold", labelpad=6)
512
-
513
- # Set x-ticks with 1 eV spacing
355
+
514
356
  xticks = np.arange(np.ceil(xlim[0]), np.floor(xlim[1]) + 1, 1)
515
357
  ax.set_xticks(xticks)
516
- # Format tick labels: show integers without .0
517
- tick_labels = [f'{int(x)}' if x == int(x) else f'{x}' for x in xticks]
358
+ tick_labels = [f"{int(x)}" if x == int(x) else f"{x}" for x in xticks]
518
359
  ax.set_xticklabels(tick_labels, fontweight="bold")
519
360
  ax.set_yticks([])
520
361
 
521
- # --- Smart legend visibility ---
522
- # Only show legend if there are items to display
523
- show_legend = len(lines) > 0
524
-
525
- if show_legend:
526
- # Check for overlap to decide legend position
527
- # Simplified overlap check for now
528
- loc, ncol = "upper right", 1
529
-
530
- # If spin polarized, upper right might cover spin up data
531
- # But usually it's fine.
532
-
362
+ if len(lines) > 0:
533
363
  legend = ax.legend(
534
- lines, labels,
364
+ lines,
365
+ labels,
535
366
  frameon=False,
536
367
  fontsize=13,
537
- loc=loc,
538
- ncol=ncol,
368
+ loc="upper right" if legend_loc == "auto" else legend_loc,
369
+ ncol=1,
539
370
  handlelength=1.5,
540
371
  columnspacing=0.8,
541
372
  handletextpad=0.6,