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.
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/visualization.py +115 -69
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/README.md +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/setup.cfg +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/manager.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/property.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/regression.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "well-log-toolkit"
|
|
7
|
-
version = "0.1.
|
|
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
|
-
#
|
|
3160
|
-
|
|
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
|
-
#
|
|
3167
|
-
|
|
3168
|
-
|
|
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
|
-
|
|
3172
|
-
|
|
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
|
-
#
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
'lower
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
#
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
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=
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.117 → well_log_toolkit-0.1.118}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|