well-log-toolkit 0.1.115__py3-none-any.whl → 0.1.117__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.
@@ -2466,6 +2466,9 @@ class WellDataManager:
2466
2466
  depth_range: Optional[tuple[float, float]] = None,
2467
2467
  show_colorbar: bool = True,
2468
2468
  show_legend: bool = True,
2469
+ show_regression_legend: bool = True,
2470
+ show_regression_equation: bool = True,
2471
+ show_regression_r2: bool = True,
2469
2472
  regression: Optional[Union[str, dict]] = None,
2470
2473
  regression_by_color: Optional[Union[str, dict]] = None,
2471
2474
  regression_by_group: Optional[Union[str, dict]] = None,
@@ -2534,14 +2537,21 @@ class WellDataManager:
2534
2537
  Show colorbar when using color mapping. Default: True
2535
2538
  show_legend : bool, optional
2536
2539
  Show legend. Default: True
2540
+ show_regression_legend : bool, optional
2541
+ Show separate legend for regression lines in lower right corner. Default: True
2542
+ show_regression_equation : bool, optional
2543
+ Include regression equation in regression legend labels. Default: True
2544
+ show_regression_r2 : bool, optional
2545
+ Include R² value in regression legend labels. Default: True
2537
2546
  regression : str or dict, optional
2538
2547
  Regression type to apply to all data points. Can be a string (e.g., "linear") or
2539
2548
  dict with keys: type, line_color, line_width, line_style, line_alpha, x_range.
2540
2549
  Default: None
2541
2550
  regression_by_color : str or dict, optional
2542
- Regression type to apply separately for each color group. Creates separate
2543
- regression lines for each unique color value. Accepts string or dict format.
2544
- Default: None
2551
+ Regression type to apply separately for each color group in the plot. Creates
2552
+ separate regression lines based on what determines colors in the visualization:
2553
+ explicit color mapping if specified, otherwise shape groups (e.g., wells when
2554
+ shape='well'). Accepts string or dict format. Default: None
2545
2555
  regression_by_group : str or dict, optional
2546
2556
  Regression type to apply separately for each well. Creates separate
2547
2557
  regression lines for each well. Accepts string or dict format.
@@ -2623,6 +2633,9 @@ class WellDataManager:
2623
2633
  depth_range=depth_range,
2624
2634
  show_colorbar=show_colorbar,
2625
2635
  show_legend=show_legend,
2636
+ show_regression_legend=show_regression_legend,
2637
+ show_regression_equation=show_regression_equation,
2638
+ show_regression_r2=show_regression_r2,
2626
2639
  regression=regression,
2627
2640
  regression_by_color=regression_by_color,
2628
2641
  regression_by_group=regression_by_group,
@@ -2822,14 +2822,21 @@ class Crossplot:
2822
2822
  Show colorbar when using color mapping. Default: True
2823
2823
  show_legend : bool, optional
2824
2824
  Show legend when using shape/well mapping. Default: True
2825
+ show_regression_legend : bool, optional
2826
+ Show separate legend for regression lines in lower right. Default: True
2827
+ show_regression_equation : bool, optional
2828
+ Show equations in regression legend. Default: True
2829
+ show_regression_r2 : bool, optional
2830
+ Show R² values in regression legend. Default: True
2825
2831
  regression : str or dict, optional
2826
2832
  Regression type to apply to all data points. Can be a string (e.g., "linear") or
2827
2833
  dict with keys: type, line_color, line_width, line_style, line_alpha, x_range.
2828
2834
  Default: None
2829
2835
  regression_by_color : str or dict, optional
2830
- Regression type to apply separately for each color group. Creates separate
2831
- regression lines for each unique color value. Accepts string or dict format.
2832
- Default: None
2836
+ Regression type to apply separately for each color group in the plot. Creates
2837
+ separate regression lines based on what determines colors in the visualization:
2838
+ explicit color mapping if specified, otherwise shape groups (e.g., wells when
2839
+ shape='well'). Accepts string or dict format. Default: None
2833
2840
  regression_by_group : str or dict, optional
2834
2841
  Regression type to apply separately for each group (well or shape). Creates
2835
2842
  separate regression lines for each well or shape category. Accepts string or dict.
@@ -2911,9 +2918,12 @@ class Crossplot:
2911
2918
  depth_range: Optional[tuple[float, float]] = None,
2912
2919
  show_colorbar: bool = True,
2913
2920
  show_legend: bool = True,
2914
- regression: Optional[str] = None,
2915
- regression_by_color: Optional[str] = None,
2916
- regression_by_group: Optional[str] = None,
2921
+ show_regression_legend: bool = True,
2922
+ show_regression_equation: bool = True,
2923
+ show_regression_r2: bool = True,
2924
+ regression: Optional[Union[str, dict]] = None,
2925
+ regression_by_color: Optional[Union[str, dict]] = None,
2926
+ regression_by_group: Optional[Union[str, dict]] = None,
2917
2927
  ):
2918
2928
  # Store wells as list
2919
2929
  if not isinstance(wells, list):
@@ -2947,6 +2957,9 @@ class Crossplot:
2947
2957
  self.depth_range = depth_range
2948
2958
  self.show_colorbar = show_colorbar
2949
2959
  self.show_legend = show_legend
2960
+ self.show_regression_legend = show_regression_legend
2961
+ self.show_regression_equation = show_regression_equation
2962
+ self.show_regression_r2 = show_regression_r2
2950
2963
  self.regression = regression
2951
2964
  self.regression_by_color = regression_by_color
2952
2965
  self.regression_by_group = regression_by_group
@@ -2960,6 +2973,7 @@ class Crossplot:
2960
2973
  # Regression storage - nested structure: {type: {identifier: regression_obj}}
2961
2974
  self._regressions = {}
2962
2975
  self.regression_lines = {}
2976
+ self.regression_legend = None # Separate legend for regressions
2963
2977
 
2964
2978
  # Pending regressions (added before plot() is called)
2965
2979
  self._pending_regressions = []
@@ -3125,6 +3139,121 @@ class Crossplot:
3125
3139
  self._regressions[reg_type] = {}
3126
3140
  self._regressions[reg_type][identifier] = regression_obj
3127
3141
 
3142
+ def _format_regression_label(self, name: str, reg, include_equation: bool = None, include_r2: bool = None) -> str:
3143
+ """Format a modern, compact regression label.
3144
+
3145
+ Args:
3146
+ name: Name of the regression
3147
+ reg: Regression object
3148
+ include_equation: Whether to include equation (uses self.show_regression_equation if None)
3149
+ include_r2: Whether to include R² (uses self.show_regression_r2 if None)
3150
+
3151
+ Returns:
3152
+ Formatted label string
3153
+ """
3154
+ if include_equation is None:
3155
+ include_equation = self.show_regression_equation
3156
+ if include_r2 is None:
3157
+ include_r2 = self.show_regression_r2
3158
+
3159
+ # Start with name
3160
+ parts = [name]
3161
+
3162
+ # Add equation and R² on same line if both shown, more compact
3163
+ metrics = []
3164
+ if include_equation:
3165
+ eq = reg.equation()
3166
+ # Shorten equation format for compactness
3167
+ eq = eq.replace(' ', '') # Remove spaces
3168
+ metrics.append(eq)
3169
+
3170
+ if include_r2:
3171
+ # Use superscript 2 for R²
3172
+ metrics.append(f"R²={reg.r_squared:.3f}")
3173
+
3174
+ if metrics:
3175
+ # Join equation and R² with pipe separator for clarity
3176
+ parts.append(" | ".join(metrics))
3177
+
3178
+ return "\n".join(parts)
3179
+
3180
+ def _update_regression_legend(self) -> None:
3181
+ """Create or update the separate regression legend with smart placement."""
3182
+ if not self.show_regression_legend or not self.regression_lines:
3183
+ return
3184
+
3185
+ if self.ax is None:
3186
+ return
3187
+
3188
+ # Remove old regression legend if it exists
3189
+ if self.regression_legend is not None:
3190
+ self.regression_legend.remove()
3191
+ self.regression_legend = None
3192
+
3193
+ # Create new regression legend with only regression lines
3194
+ regression_handles = []
3195
+ regression_labels = []
3196
+
3197
+ for line in self.regression_lines.values():
3198
+ regression_handles.append(line)
3199
+ regression_labels.append(line.get_label())
3200
+
3201
+ if regression_handles:
3202
+ # Smart placement: try these locations in priority order
3203
+ # Prefer corners away from main legend and colorbar
3204
+ locations = [
3205
+ 'lower right', # Primary choice
3206
+ 'upper right', # If lower right conflicts with data
3207
+ 'lower left', # If right side has colorbar/main legend
3208
+ 'center right', # Fallback
3209
+ ]
3210
+
3211
+ # If main legend is shown, it's likely in upper left
3212
+ # If colorbar is shown, it's on the right side
3213
+ # Adjust preferences based on what's visible
3214
+ if self.show_legend and self.show_colorbar:
3215
+ # Both legend and colorbar present - lower left might be better
3216
+ locations = ['lower left', 'lower right', 'upper right', 'center left']
3217
+ elif self.show_colorbar:
3218
+ # Colorbar on right - prefer left side
3219
+ locations = ['lower left', 'upper left', 'lower right', 'center left']
3220
+
3221
+ # Try to create legend with best location
3222
+ # Use 'best' as fallback - matplotlib will find optimal position
3223
+ try:
3224
+ self.regression_legend = self.ax.legend(
3225
+ regression_handles,
3226
+ regression_labels,
3227
+ loc=locations[0], # Try first preference
3228
+ frameon=True,
3229
+ framealpha=0.95,
3230
+ edgecolor='#cccccc',
3231
+ fancybox=False,
3232
+ shadow=False,
3233
+ fontsize=9,
3234
+ title='Regressions',
3235
+ title_fontsize=10
3236
+ )
3237
+ except Exception:
3238
+ # Fallback to 'best' if specific location fails
3239
+ self.regression_legend = self.ax.legend(
3240
+ regression_handles,
3241
+ regression_labels,
3242
+ loc='best',
3243
+ frameon=True,
3244
+ framealpha=0.95,
3245
+ edgecolor='#cccccc',
3246
+ fancybox=False,
3247
+ shadow=False,
3248
+ fontsize=9,
3249
+ title='Regressions',
3250
+ title_fontsize=10
3251
+ )
3252
+
3253
+ # Modern styling
3254
+ self.regression_legend.get_frame().set_linewidth(0.8)
3255
+ self.regression_legend.get_title().set_fontweight('600')
3256
+
3128
3257
  def _add_automatic_regressions(self, data: pd.DataFrame) -> None:
3129
3258
  """Add automatic regressions based on initialization parameters."""
3130
3259
  if not any([self.regression, self.regression_by_color, self.regression_by_group]):
@@ -3162,53 +3291,70 @@ class Crossplot:
3162
3291
  config = self._parse_regression_config(self.regression_by_color)
3163
3292
  reg_type = config['type']
3164
3293
 
3165
- if not self.color:
3166
- warnings.warn("regression_by_color specified but no color mapping defined, skipping")
3294
+ # Determine grouping column based on what's being used for colors in the plot
3295
+ group_column = None
3296
+ group_label = None
3297
+
3298
+ if self.color and 'color_val' in data.columns:
3299
+ # User specified explicit color mapping
3300
+ group_column = 'color_val'
3301
+ group_label = self.color
3302
+ elif self.shape == "well" and 'well' in data.columns:
3303
+ # When shape="well", each well gets a different color in the plot
3304
+ group_column = 'well'
3305
+ group_label = 'well'
3306
+ elif self.shape and self.shape != "well" and 'shape_val' in data.columns:
3307
+ # When shape is a property, each shape group gets a different color
3308
+ group_column = 'shape_val'
3309
+ group_label = self.shape
3310
+
3311
+ if group_column is None:
3312
+ warnings.warn(
3313
+ "regression_by_color specified but no color grouping detected in plot. "
3314
+ "Use color=<property>, shape='well', or shape=<property> parameter."
3315
+ )
3167
3316
  else:
3168
- # Get unique color groups
3169
- if 'color_val' in data.columns:
3170
- # For continuous color values, we need to bin them or use unique values
3171
- # Check if color is categorical (well, shape) or continuous
3172
- if self.color == 'depth' or pd.api.types.is_numeric_dtype(data['color_val']):
3173
- # For continuous values, we can't create separate regressions
3174
- warnings.warn(
3175
- f"regression_by_color requires categorical color mapping, "
3176
- f"but '{self.color}' is continuous. Use regression_by_group instead."
3317
+ # Check if color is categorical (not continuous like depth)
3318
+ if group_column == 'color_val' and (self.color == 'depth' or pd.api.types.is_numeric_dtype(data[group_column])):
3319
+ # For continuous values, we can't create separate regressions
3320
+ warnings.warn(
3321
+ f"regression_by_color requires categorical color mapping, "
3322
+ f"but '{self.color}' is continuous. Use regression_by_group instead."
3323
+ )
3324
+ else:
3325
+ # Categorical values - group and create regressions
3326
+ color_groups = data.groupby(group_column)
3327
+ n_groups = len(color_groups)
3328
+
3329
+ # Validate regression count
3330
+ if regression_count + n_groups > total_points / 2:
3331
+ raise ValueError(
3332
+ f"Too many regression lines requested: {regression_count + n_groups} lines "
3333
+ f"for {total_points} data points (average < 2 points per line). "
3334
+ f"Reduce the number of groups or use a different regression strategy."
3177
3335
  )
3178
- else:
3179
- # Categorical color values
3180
- color_groups = data.groupby('color_val')
3181
- n_groups = len(color_groups)
3182
-
3183
- # Validate regression count
3184
- if regression_count + n_groups > total_points / 2:
3185
- raise ValueError(
3186
- f"Too many regression lines requested: {regression_count + n_groups} lines "
3187
- f"for {total_points} data points (average < 2 points per line). "
3188
- f"Reduce the number of groups or use a different regression strategy."
3189
- )
3190
3336
 
3191
- for idx, (group_name, group_data) in enumerate(color_groups):
3192
- x_vals = group_data['x'].values
3193
- y_vals = group_data['y'].values
3194
- mask = np.isfinite(x_vals) & np.isfinite(y_vals)
3195
- if np.sum(mask) >= 2:
3196
- # Copy config and set default line color if not specified
3197
- group_config = config.copy()
3198
- if 'line_color' not in group_config:
3199
- group_config['line_color'] = regression_colors[color_idx % len(regression_colors)]
3200
-
3201
- # Skip legend update for all but last regression
3202
- is_last = (idx == n_groups - 1)
3203
- self._add_group_regression(
3204
- x_vals[mask], y_vals[mask],
3205
- reg_type,
3206
- name=f"{self.color}={group_name}",
3207
- config=group_config,
3208
- update_legend=is_last
3209
- )
3210
- regression_count += 1
3211
- color_idx += 1
3337
+ for idx, (group_name, group_data) in enumerate(color_groups):
3338
+ x_vals = group_data['x'].values
3339
+ y_vals = group_data['y'].values
3340
+ mask = np.isfinite(x_vals) & np.isfinite(y_vals)
3341
+ if np.sum(mask) >= 2:
3342
+ # Copy config and set default line color if not specified
3343
+ group_config = config.copy()
3344
+ if 'line_color' not in group_config:
3345
+ group_config['line_color'] = regression_colors[color_idx % len(regression_colors)]
3346
+
3347
+ # Skip legend update for all but last regression
3348
+ is_last = (idx == n_groups - 1)
3349
+ self._add_group_regression(
3350
+ x_vals[mask], y_vals[mask],
3351
+ reg_type,
3352
+ name=f"{group_label}={group_name}",
3353
+ config=group_config,
3354
+ update_legend=is_last
3355
+ )
3356
+ regression_count += 1
3357
+ color_idx += 1
3212
3358
 
3213
3359
  # Add regression by groups (well or shape)
3214
3360
  if self.regression_by_group:
@@ -3295,8 +3441,8 @@ class Crossplot:
3295
3441
  warnings.warn(f"Could not generate plot data for {name} regression: {e}")
3296
3442
  return
3297
3443
 
3298
- # Create label with equation and R²
3299
- label = f"{name}\n{reg.equation()}\nR² = {reg.r_squared:.4f}"
3444
+ # Create label using formatter
3445
+ label = self._format_regression_label(name, reg)
3300
3446
 
3301
3447
  # Plot line with config parameters
3302
3448
  line = self.ax.plot(
@@ -3310,9 +3456,9 @@ class Crossplot:
3310
3456
 
3311
3457
  self.regression_lines[name] = line
3312
3458
 
3313
- # Update legend if requested (skipped during batch operations for performance)
3459
+ # Update regression legend if requested (skipped during batch operations for performance)
3314
3460
  if update_legend and self.ax is not None:
3315
- self.ax.legend(loc='best', frameon=True, framealpha=0.9, edgecolor='black')
3461
+ self._update_regression_legend()
3316
3462
 
3317
3463
  def plot(self) -> 'Crossplot':
3318
3464
  """Generate the crossplot figure."""
@@ -3376,13 +3522,12 @@ class Crossplot:
3376
3522
  warnings.warn(f"Could not generate plot data for {reg_type} regression: {e}")
3377
3523
  continue
3378
3524
 
3379
- # Create label
3380
- label_parts = [reg_name]
3381
- if pending['show_equation']:
3382
- label_parts.append(reg.equation())
3383
- if pending['show_r2']:
3384
- label_parts.append(f"R² = {reg.r_squared:.4f}")
3385
- label = "\n".join(label_parts)
3525
+ # Create label using formatter
3526
+ label = self._format_regression_label(
3527
+ reg_name, reg,
3528
+ include_equation=pending['show_equation'],
3529
+ include_r2=pending['show_r2']
3530
+ )
3386
3531
 
3387
3532
  # Plot line
3388
3533
  line = self.ax.plot(
@@ -3396,9 +3541,9 @@ class Crossplot:
3396
3541
 
3397
3542
  self.regression_lines[reg_name] = line
3398
3543
 
3399
- # Update legend once after all pending regressions
3544
+ # Update regression legend once after all pending regressions
3400
3545
  if self.ax is not None:
3401
- self.ax.legend(loc='best', frameon=True, framealpha=0.9, edgecolor='black')
3546
+ self._update_regression_legend()
3402
3547
 
3403
3548
  # Clear pending list
3404
3549
  self._pending_regressions = []
@@ -3632,13 +3777,12 @@ class Crossplot:
3632
3777
  warnings.warn(f"Could not generate plot data for {regression_type} regression: {e}")
3633
3778
  return self
3634
3779
 
3635
- # Create label
3636
- label_parts = [reg_name]
3637
- if show_equation:
3638
- label_parts.append(reg.equation())
3639
- if show_r2:
3640
- label_parts.append(f"R² = {reg.r_squared:.4f}")
3641
- label = "\n".join(label_parts)
3780
+ # Create label using formatter
3781
+ label = self._format_regression_label(
3782
+ reg_name, reg,
3783
+ include_equation=show_equation,
3784
+ include_r2=show_r2
3785
+ )
3642
3786
 
3643
3787
  # Plot line
3644
3788
  line = self.ax.plot(
@@ -3652,8 +3796,8 @@ class Crossplot:
3652
3796
 
3653
3797
  self.regression_lines[reg_name] = line
3654
3798
 
3655
- # Update legend
3656
- self.ax.legend(loc='best', frameon=True, framealpha=0.9, edgecolor='black')
3799
+ # Update regression legend
3800
+ self._update_regression_legend()
3657
3801
  else:
3658
3802
  # Store for later when plot() is called
3659
3803
  self._pending_regressions.append({
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.115
3
+ Version: 0.1.117
4
4
  Summary: Fast LAS file processing with lazy loading and filtering for well log analysis
5
5
  Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
6
6
  License: MIT
@@ -1,15 +1,15 @@
1
1
  well_log_toolkit/__init__.py,sha256=ilJAIIhh68pYfD9I3V53juTEJpoMN8oHpcpEFNpuXAQ,3793
2
2
  well_log_toolkit/exceptions.py,sha256=X_fzC7d4yaBFO9Vx74dEIB6xmI9Agi6_bTU3MPxn6ko,985
3
3
  well_log_toolkit/las_file.py,sha256=yRCIiVbdoqFzoXKRKfx7Lt11INmwB_bXdpHpwVaNpjk,52156
4
- well_log_toolkit/manager.py,sha256=gajHbQc1zI7L0gSmcZOUg1_aSje4SZoqSff3uu3C7Hg,101585
4
+ well_log_toolkit/manager.py,sha256=Mc_zgC9pgbYq82msiAc0KMVmPFbCX90SZK5JfwGY4H4,102422
5
5
  well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
6
6
  well_log_toolkit/property.py,sha256=WOzoNQcmHCQ8moIKsnSyLgVC8s4LBu2x5IBXtFzmMe8,76236
7
7
  well_log_toolkit/regression.py,sha256=19wpiQQdGxuyIbjfzIY_yLzUUGT_ampWE_km1bg3o3c,24527
8
8
  well_log_toolkit/statistics.py,sha256=_huPMbv2H3o9ezunjEM94mJknX5wPK8V4nDv2lIZZRw,16814
9
9
  well_log_toolkit/utils.py,sha256=O2KPq4htIoUlL74V2zKftdqqTjRfezU9M-568zPLme0,6866
10
- well_log_toolkit/visualization.py,sha256=GlSR8KeYQED21bn2SmhHUj9zfnDb1ePgFIJBD-1XF5Q,147956
10
+ well_log_toolkit/visualization.py,sha256=22-JGXCPF1OpPiIQ_tgZCpDrnZYMnv75db6fP02rCtU,153801
11
11
  well_log_toolkit/well.py,sha256=jv8xPQi-y5dLRQ7WIyBsr8DIMIAiFt4UJ9Rum7xTx7o,90170
12
- well_log_toolkit-0.1.115.dist-info/METADATA,sha256=22pPqzD7QtvbX526X79rCUM7tkTvv3jYvSOv4YjG-xc,59737
13
- well_log_toolkit-0.1.115.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- well_log_toolkit-0.1.115.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.115.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.117.dist-info/METADATA,sha256=k9moLAwhtg53LkAv1q0gk-OlLDAFOykG2uT1hnXqDvg,59737
13
+ well_log_toolkit-0.1.117.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ well_log_toolkit-0.1.117.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.117.dist-info/RECORD,,