well-log-toolkit 0.1.117__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.117 → well_log_toolkit-0.1.118}/PKG-INFO +1 -1
  2. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/pyproject.toml +1 -1
  3. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/visualization.py +115 -69
  4. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/PKG-INFO +1 -1
  5. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/README.md +0 -0
  6. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/setup.cfg +0 -0
  7. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/__init__.py +0 -0
  8. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/exceptions.py +0 -0
  9. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/las_file.py +0 -0
  10. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/manager.py +0 -0
  11. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/operations.py +0 -0
  12. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/property.py +0 -0
  13. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/regression.py +0 -0
  14. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/statistics.py +0 -0
  15. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/utils.py +0 -0
  16. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/well.py +0 -0
  17. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
  18. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
  19. {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/requires.txt +0 -0
  20. {well_log_toolkit-0.1.117 → 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.117
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.117"
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"
@@ -3139,6 +3139,67 @@ class Crossplot:
3139
3139
  self._regressions[reg_type] = {}
3140
3140
  self._regressions[reg_type][identifier] = regression_obj
3141
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
+
3142
3203
  def _format_regression_label(self, name: str, reg, include_equation: bool = None, include_r2: bool = None) -> str:
3143
3204
  """Format a modern, compact regression label.
3144
3205
 
@@ -3156,26 +3217,20 @@ class Crossplot:
3156
3217
  if include_r2 is None:
3157
3218
  include_r2 = self.show_regression_r2
3158
3219
 
3159
- # Start with name
3160
- parts = [name]
3161
-
3162
- # Add equation and R² on same line if both shown, more compact
3163
- metrics = []
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
3164
3223
  if include_equation:
3165
3224
  eq = reg.equation()
3166
- # Shorten equation format for compactness
3167
- eq = eq.replace(' ', '') # Remove spaces
3168
- metrics.append(eq)
3225
+ eq = eq.replace(' ', '') # Remove spaces for compactness
3226
+ # Add equation in parentheses (will be styled grey later)
3227
+ first_line = f"{name} ({eq})"
3169
3228
 
3229
+ # Add R² on second line if requested (will be styled grey later)
3170
3230
  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)
3231
+ return f"{first_line}\nR² = {reg.r_squared:.3f}"
3232
+ else:
3233
+ return first_line
3179
3234
 
3180
3235
  def _update_regression_legend(self) -> None:
3181
3236
  """Create or update the separate regression legend with smart placement."""
@@ -3199,61 +3254,43 @@ class Crossplot:
3199
3254
  regression_labels.append(line.get_label())
3200
3255
 
3201
3256
  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
- )
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
+ )
3252
3282
 
3253
- # Modern styling
3283
+ # Modern styling with grey text for equation and R²
3254
3284
  self.regression_legend.get_frame().set_linewidth(0.8)
3255
3285
  self.regression_legend.get_title().set_fontweight('600')
3256
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
+
3257
3294
  def _add_automatic_regressions(self, data: pd.DataFrame) -> None:
3258
3295
  """Add automatic regressions based on initialization parameters."""
3259
3296
  if not any([self.regression, self.regression_by_color, self.regression_by_group]):
@@ -3672,17 +3709,26 @@ class Crossplot:
3672
3709
  if first_scatter is None and self.color:
3673
3710
  first_scatter = scatter
3674
3711
 
3675
- # Add legend
3712
+ # Add legend with smart placement
3676
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
+
3677
3720
  legend = self.ax.legend(
3678
3721
  title=group_label,
3679
- loc='best',
3722
+ loc=primary_loc,
3680
3723
  frameon=True,
3681
3724
  framealpha=0.9,
3682
3725
  edgecolor='black'
3683
3726
  )
3684
3727
  legend.get_title().set_fontweight('bold')
3685
3728
 
3729
+ # Store the primary legend so it persists when regression legend is added
3730
+ self.ax.add_artist(legend)
3731
+
3686
3732
  # Add colorbar if using color mapping
3687
3733
  if self.color and self.show_colorbar and first_scatter:
3688
3734
  self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.117
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