well-log-toolkit 0.1.116__tar.gz → 0.1.118__tar.gz

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.
Files changed (20) hide show
  1. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/PKG-INFO +1 -1
  2. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/pyproject.toml +1 -1
  3. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/manager.py +12 -0
  4. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/visualization.py +199 -27
  5. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/PKG-INFO +1 -1
  6. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/README.md +0 -0
  7. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/setup.cfg +0 -0
  8. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/__init__.py +0 -0
  9. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/exceptions.py +0 -0
  10. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/las_file.py +0 -0
  11. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/operations.py +0 -0
  12. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/property.py +0 -0
  13. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/regression.py +0 -0
  14. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/statistics.py +0 -0
  15. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/utils.py +0 -0
  16. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit/well.py +0 -0
  17. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
  18. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
  19. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/requires.txt +0 -0
  20. {well_log_toolkit-0.1.116 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.116
3
+ Version: 0.1.118
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "well-log-toolkit"
7
- version = "0.1.116"
7
+ version = "0.1.118"
8
8
  description = "Fast LAS file processing with lazy loading and filtering for well log analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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,6 +2537,12 @@ 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.
@@ -2624,6 +2633,9 @@ class WellDataManager:
2624
2633
  depth_range=depth_range,
2625
2634
  show_colorbar=show_colorbar,
2626
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,
2627
2639
  regression=regression,
2628
2640
  regression_by_color=regression_by_color,
2629
2641
  regression_by_group=regression_by_group,
@@ -2822,6 +2822,12 @@ 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.
@@ -2912,9 +2918,12 @@ class Crossplot:
2912
2918
  depth_range: Optional[tuple[float, float]] = None,
2913
2919
  show_colorbar: bool = True,
2914
2920
  show_legend: bool = True,
2915
- regression: Optional[str] = None,
2916
- regression_by_color: Optional[str] = None,
2917
- 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,
2918
2927
  ):
2919
2928
  # Store wells as list
2920
2929
  if not isinstance(wells, list):
@@ -2948,6 +2957,9 @@ class Crossplot:
2948
2957
  self.depth_range = depth_range
2949
2958
  self.show_colorbar = show_colorbar
2950
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
2951
2963
  self.regression = regression
2952
2964
  self.regression_by_color = regression_by_color
2953
2965
  self.regression_by_group = regression_by_group
@@ -2961,6 +2973,7 @@ class Crossplot:
2961
2973
  # Regression storage - nested structure: {type: {identifier: regression_obj}}
2962
2974
  self._regressions = {}
2963
2975
  self.regression_lines = {}
2976
+ self.regression_legend = None # Separate legend for regressions
2964
2977
 
2965
2978
  # Pending regressions (added before plot() is called)
2966
2979
  self._pending_regressions = []
@@ -3126,6 +3139,158 @@ class Crossplot:
3126
3139
  self._regressions[reg_type] = {}
3127
3140
  self._regressions[reg_type][identifier] = regression_obj
3128
3141
 
3142
+ def _find_best_legend_locations(self, data: pd.DataFrame) -> tuple[str, str]:
3143
+ """Find the two best locations for legends based on data density.
3144
+
3145
+ Divides the plot into a 3x3 grid and finds the two squares with the least data points.
3146
+
3147
+ Args:
3148
+ data: DataFrame with 'x' and 'y' columns
3149
+
3150
+ Returns:
3151
+ Tuple of (primary_location, secondary_location) as matplotlib location strings
3152
+ """
3153
+ # Get x and y bounds
3154
+ x_vals = data['x'].values
3155
+ y_vals = data['y'].values
3156
+
3157
+ # Handle log scales for binning
3158
+ if self.x_log:
3159
+ x_vals = np.log10(x_vals[x_vals > 0])
3160
+ if self.y_log:
3161
+ y_vals = np.log10(y_vals[y_vals > 0])
3162
+
3163
+ x_min, x_max = np.nanmin(x_vals), np.nanmax(x_vals)
3164
+ y_min, y_max = np.nanmin(y_vals), np.nanmax(y_vals)
3165
+
3166
+ # Create 3x3 grid and count points in each square
3167
+ x_bins = np.linspace(x_min, x_max, 4)
3168
+ y_bins = np.linspace(y_min, y_max, 4)
3169
+
3170
+ # Count points in each of 9 squares
3171
+ counts = {}
3172
+ for i in range(3):
3173
+ for j in range(3):
3174
+ x_mask = (x_vals >= x_bins[i]) & (x_vals < x_bins[i+1])
3175
+ y_mask = (y_vals >= y_bins[j]) & (y_vals < y_bins[j+1])
3176
+ counts[(i, j)] = np.sum(x_mask & y_mask)
3177
+
3178
+ # Map grid positions to matplotlib location strings
3179
+ # Grid: (0,2) (1,2) (2,2) -> upper left, upper center, upper right
3180
+ # (0,1) (1,1) (2,1) -> center left, center, center right
3181
+ # (0,0) (1,0) (2,0) -> lower left, lower center, lower right
3182
+ position_map = {
3183
+ (0, 2): 'upper left',
3184
+ (1, 2): 'upper center',
3185
+ (2, 2): 'upper right',
3186
+ (0, 1): 'center left',
3187
+ (1, 1): 'center',
3188
+ (2, 1): 'center right',
3189
+ (0, 0): 'lower left',
3190
+ (1, 0): 'lower center',
3191
+ (2, 0): 'lower right',
3192
+ }
3193
+
3194
+ # Sort squares by count (ascending)
3195
+ sorted_squares = sorted(counts.items(), key=lambda x: x[1])
3196
+
3197
+ # Get two best locations
3198
+ best_pos = position_map[sorted_squares[0][0]]
3199
+ second_best_pos = position_map[sorted_squares[1][0]]
3200
+
3201
+ return best_pos, second_best_pos
3202
+
3203
+ def _format_regression_label(self, name: str, reg, include_equation: bool = None, include_r2: bool = None) -> str:
3204
+ """Format a modern, compact regression label.
3205
+
3206
+ Args:
3207
+ name: Name of the regression
3208
+ reg: Regression object
3209
+ include_equation: Whether to include equation (uses self.show_regression_equation if None)
3210
+ include_r2: Whether to include R² (uses self.show_regression_r2 if None)
3211
+
3212
+ Returns:
3213
+ Formatted label string
3214
+ """
3215
+ if include_equation is None:
3216
+ include_equation = self.show_regression_equation
3217
+ if include_r2 is None:
3218
+ include_r2 = self.show_regression_r2
3219
+
3220
+ # Format: "Name (equation)" with R² on second line
3221
+ # Equation and R² will be colored grey in the legend update method
3222
+ first_line = name
3223
+ if include_equation:
3224
+ eq = reg.equation()
3225
+ eq = eq.replace(' ', '') # Remove spaces for compactness
3226
+ # Add equation in parentheses (will be styled grey later)
3227
+ first_line = f"{name} ({eq})"
3228
+
3229
+ # Add R² on second line if requested (will be styled grey later)
3230
+ if include_r2:
3231
+ return f"{first_line}\nR² = {reg.r_squared:.3f}"
3232
+ else:
3233
+ return first_line
3234
+
3235
+ def _update_regression_legend(self) -> None:
3236
+ """Create or update the separate regression legend with smart placement."""
3237
+ if not self.show_regression_legend or not self.regression_lines:
3238
+ return
3239
+
3240
+ if self.ax is None:
3241
+ return
3242
+
3243
+ # Remove old regression legend if it exists
3244
+ if self.regression_legend is not None:
3245
+ self.regression_legend.remove()
3246
+ self.regression_legend = None
3247
+
3248
+ # Create new regression legend with only regression lines
3249
+ regression_handles = []
3250
+ regression_labels = []
3251
+
3252
+ for line in self.regression_lines.values():
3253
+ regression_handles.append(line)
3254
+ regression_labels.append(line.get_label())
3255
+
3256
+ if regression_handles:
3257
+ # Get smart placement based on data density
3258
+ if self._data is not None:
3259
+ _, secondary_loc = self._find_best_legend_locations(self._data)
3260
+ else:
3261
+ # Fallback if data not available
3262
+ secondary_loc = 'lower right'
3263
+
3264
+ # Import legend from matplotlib
3265
+ from matplotlib.legend import Legend
3266
+
3267
+ # Create regression legend at secondary location
3268
+ self.regression_legend = Legend(
3269
+ self.ax,
3270
+ regression_handles,
3271
+ regression_labels,
3272
+ loc=secondary_loc,
3273
+ frameon=True,
3274
+ framealpha=0.95,
3275
+ edgecolor='#cccccc',
3276
+ fancybox=False,
3277
+ shadow=False,
3278
+ fontsize=9,
3279
+ title='Regressions',
3280
+ title_fontsize=10
3281
+ )
3282
+
3283
+ # Modern styling with grey text for equation and R²
3284
+ self.regression_legend.get_frame().set_linewidth(0.8)
3285
+ self.regression_legend.get_title().set_fontweight('600')
3286
+
3287
+ # Set text color to grey for all labels
3288
+ for text in self.regression_legend.get_texts():
3289
+ text.set_color('#555555')
3290
+
3291
+ # Add as artist to avoid replacing the primary legend
3292
+ self.ax.add_artist(self.regression_legend)
3293
+
3129
3294
  def _add_automatic_regressions(self, data: pd.DataFrame) -> None:
3130
3295
  """Add automatic regressions based on initialization parameters."""
3131
3296
  if not any([self.regression, self.regression_by_color, self.regression_by_group]):
@@ -3313,8 +3478,8 @@ class Crossplot:
3313
3478
  warnings.warn(f"Could not generate plot data for {name} regression: {e}")
3314
3479
  return
3315
3480
 
3316
- # Create label with equation and R²
3317
- label = f"{name}\n{reg.equation()}\nR² = {reg.r_squared:.4f}"
3481
+ # Create label using formatter
3482
+ label = self._format_regression_label(name, reg)
3318
3483
 
3319
3484
  # Plot line with config parameters
3320
3485
  line = self.ax.plot(
@@ -3328,9 +3493,9 @@ class Crossplot:
3328
3493
 
3329
3494
  self.regression_lines[name] = line
3330
3495
 
3331
- # Update legend if requested (skipped during batch operations for performance)
3496
+ # Update regression legend if requested (skipped during batch operations for performance)
3332
3497
  if update_legend and self.ax is not None:
3333
- self.ax.legend(loc='best', frameon=True, framealpha=0.9, edgecolor='black')
3498
+ self._update_regression_legend()
3334
3499
 
3335
3500
  def plot(self) -> 'Crossplot':
3336
3501
  """Generate the crossplot figure."""
@@ -3394,13 +3559,12 @@ class Crossplot:
3394
3559
  warnings.warn(f"Could not generate plot data for {reg_type} regression: {e}")
3395
3560
  continue
3396
3561
 
3397
- # Create label
3398
- label_parts = [reg_name]
3399
- if pending['show_equation']:
3400
- label_parts.append(reg.equation())
3401
- if pending['show_r2']:
3402
- label_parts.append(f"R² = {reg.r_squared:.4f}")
3403
- label = "\n".join(label_parts)
3562
+ # Create label using formatter
3563
+ label = self._format_regression_label(
3564
+ reg_name, reg,
3565
+ include_equation=pending['show_equation'],
3566
+ include_r2=pending['show_r2']
3567
+ )
3404
3568
 
3405
3569
  # Plot line
3406
3570
  line = self.ax.plot(
@@ -3414,9 +3578,9 @@ class Crossplot:
3414
3578
 
3415
3579
  self.regression_lines[reg_name] = line
3416
3580
 
3417
- # Update legend once after all pending regressions
3581
+ # Update regression legend once after all pending regressions
3418
3582
  if self.ax is not None:
3419
- self.ax.legend(loc='best', frameon=True, framealpha=0.9, edgecolor='black')
3583
+ self._update_regression_legend()
3420
3584
 
3421
3585
  # Clear pending list
3422
3586
  self._pending_regressions = []
@@ -3545,17 +3709,26 @@ class Crossplot:
3545
3709
  if first_scatter is None and self.color:
3546
3710
  first_scatter = scatter
3547
3711
 
3548
- # Add legend
3712
+ # Add legend with smart placement
3549
3713
  if self.show_legend:
3714
+ # Get best location based on data density
3715
+ if self._data is not None:
3716
+ primary_loc, _ = self._find_best_legend_locations(self._data)
3717
+ else:
3718
+ primary_loc = 'best'
3719
+
3550
3720
  legend = self.ax.legend(
3551
3721
  title=group_label,
3552
- loc='best',
3722
+ loc=primary_loc,
3553
3723
  frameon=True,
3554
3724
  framealpha=0.9,
3555
3725
  edgecolor='black'
3556
3726
  )
3557
3727
  legend.get_title().set_fontweight('bold')
3558
3728
 
3729
+ # Store the primary legend so it persists when regression legend is added
3730
+ self.ax.add_artist(legend)
3731
+
3559
3732
  # Add colorbar if using color mapping
3560
3733
  if self.color and self.show_colorbar and first_scatter:
3561
3734
  self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
@@ -3650,13 +3823,12 @@ class Crossplot:
3650
3823
  warnings.warn(f"Could not generate plot data for {regression_type} regression: {e}")
3651
3824
  return self
3652
3825
 
3653
- # Create label
3654
- label_parts = [reg_name]
3655
- if show_equation:
3656
- label_parts.append(reg.equation())
3657
- if show_r2:
3658
- label_parts.append(f"R² = {reg.r_squared:.4f}")
3659
- label = "\n".join(label_parts)
3826
+ # Create label using formatter
3827
+ label = self._format_regression_label(
3828
+ reg_name, reg,
3829
+ include_equation=show_equation,
3830
+ include_r2=show_r2
3831
+ )
3660
3832
 
3661
3833
  # Plot line
3662
3834
  line = self.ax.plot(
@@ -3670,8 +3842,8 @@ class Crossplot:
3670
3842
 
3671
3843
  self.regression_lines[reg_name] = line
3672
3844
 
3673
- # Update legend
3674
- self.ax.legend(loc='best', frameon=True, framealpha=0.9, edgecolor='black')
3845
+ # Update regression legend
3846
+ self._update_regression_legend()
3675
3847
  else:
3676
3848
  # Store for later when plot() is called
3677
3849
  self._pending_regressions.append({
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.116
3
+ Version: 0.1.118
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