well-log-toolkit 0.1.130__py3-none-any.whl → 0.1.132__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.
- well_log_toolkit/property.py +10 -3
- well_log_toolkit/visualization.py +299 -50
- {well_log_toolkit-0.1.130.dist-info → well_log_toolkit-0.1.132.dist-info}/METADATA +1 -1
- {well_log_toolkit-0.1.130.dist-info → well_log_toolkit-0.1.132.dist-info}/RECORD +6 -6
- {well_log_toolkit-0.1.130.dist-info → well_log_toolkit-0.1.132.dist-info}/WHEEL +0 -0
- {well_log_toolkit-0.1.130.dist-info → well_log_toolkit-0.1.132.dist-info}/top_level.txt +0 -0
well_log_toolkit/property.py
CHANGED
|
@@ -642,7 +642,7 @@ class Property(PropertyOperationsMixin):
|
|
|
642
642
|
|
|
643
643
|
def resample(self, target_depth: Union[np.ndarray, 'Property']) -> 'Property':
|
|
644
644
|
"""
|
|
645
|
-
Resample property to a new depth grid using
|
|
645
|
+
Resample property to a new depth grid using appropriate interpolation.
|
|
646
646
|
|
|
647
647
|
This method creates a new Property object with values interpolated to match
|
|
648
648
|
the target depth grid. This is required when combining properties with
|
|
@@ -663,7 +663,10 @@ class Property(PropertyOperationsMixin):
|
|
|
663
663
|
Notes
|
|
664
664
|
-----
|
|
665
665
|
- Uses linear interpolation for continuous data
|
|
666
|
-
- Uses
|
|
666
|
+
- Uses forward-fill (previous) for discrete data - geological zones extend
|
|
667
|
+
from their top/boundary until the next boundary is encountered. For example,
|
|
668
|
+
"Cerisa West top" at 2929.93m remains active until "Cerisa West SST 1 top"
|
|
669
|
+
at 2955.10m is intercepted.
|
|
667
670
|
- Values outside the original depth range are set to NaN
|
|
668
671
|
- NaN values in original data are excluded from interpolation
|
|
669
672
|
|
|
@@ -728,7 +731,11 @@ class Property(PropertyOperationsMixin):
|
|
|
728
731
|
|
|
729
732
|
# Choose interpolation method based on type
|
|
730
733
|
if self.type == 'discrete':
|
|
731
|
-
|
|
734
|
+
# Use 'previous' (forward-fill) for discrete properties
|
|
735
|
+
# This ensures geological zones extend from their top/boundary
|
|
736
|
+
# until the next top is encountered (e.g., "Cerisa West top" at 2929.93
|
|
737
|
+
# remains active until "Cerisa West SST 1 top" at 2955.10)
|
|
738
|
+
kind = 'previous'
|
|
732
739
|
else:
|
|
733
740
|
kind = 'linear'
|
|
734
741
|
|
|
@@ -3010,7 +3010,11 @@ class Crossplot:
|
|
|
3010
3010
|
self.shape = "label"
|
|
3011
3011
|
else:
|
|
3012
3012
|
self.shape = shape
|
|
3013
|
-
|
|
3013
|
+
# Default color to "well" when layers are provided (for multi-well visualization)
|
|
3014
|
+
if color is None and layers is not None and len(self.wells) > 1:
|
|
3015
|
+
self.color = "well"
|
|
3016
|
+
else:
|
|
3017
|
+
self.color = color
|
|
3014
3018
|
self.size = size
|
|
3015
3019
|
self.colortemplate = colortemplate
|
|
3016
3020
|
self.color_range = color_range
|
|
@@ -3068,6 +3072,10 @@ class Crossplot:
|
|
|
3068
3072
|
# Data cache
|
|
3069
3073
|
self._data = None
|
|
3070
3074
|
|
|
3075
|
+
# Discrete property labels storage
|
|
3076
|
+
# Maps property role ('shape', 'color', 'size') to labels dict {0: 'label0', 1: 'label1', ...}
|
|
3077
|
+
self._discrete_labels = {}
|
|
3078
|
+
|
|
3071
3079
|
def add_layer(self, x: str, y: str, label: str):
|
|
3072
3080
|
"""
|
|
3073
3081
|
Add a new data layer to the crossplot.
|
|
@@ -3132,6 +3140,32 @@ class Crossplot:
|
|
|
3132
3140
|
# Only do expensive allclose if needed
|
|
3133
3141
|
return not np.allclose(prop_depth, ref_depth)
|
|
3134
3142
|
|
|
3143
|
+
# Helper function to align property values to target depth grid
|
|
3144
|
+
def align_property(prop, target_depth):
|
|
3145
|
+
"""
|
|
3146
|
+
Align property values to target depth grid.
|
|
3147
|
+
|
|
3148
|
+
Uses appropriate interpolation based on property type:
|
|
3149
|
+
- Discrete properties: forward-fill/previous (geological zones extend from
|
|
3150
|
+
their top/boundary until the next boundary is encountered)
|
|
3151
|
+
- Continuous properties: linear interpolation
|
|
3152
|
+
|
|
3153
|
+
Args:
|
|
3154
|
+
prop: Property object to align
|
|
3155
|
+
target_depth: Target depth array
|
|
3156
|
+
|
|
3157
|
+
Returns:
|
|
3158
|
+
Aligned values array
|
|
3159
|
+
"""
|
|
3160
|
+
if prop.type == 'discrete':
|
|
3161
|
+
# Use Property's resample method which handles discrete properties correctly
|
|
3162
|
+
# (forward-fill to preserve integer codes and geological zone logic)
|
|
3163
|
+
resampled = prop.resample(target_depth)
|
|
3164
|
+
return resampled.values
|
|
3165
|
+
else:
|
|
3166
|
+
# For continuous properties, use linear interpolation
|
|
3167
|
+
return np.interp(target_depth, prop.depth, prop.values, left=np.nan, right=np.nan)
|
|
3168
|
+
|
|
3135
3169
|
# Loop through each layer
|
|
3136
3170
|
for layer in self._layers:
|
|
3137
3171
|
layer_x = layer['x']
|
|
@@ -3149,9 +3183,9 @@ class Crossplot:
|
|
|
3149
3183
|
x_values = x_prop.values
|
|
3150
3184
|
y_values = y_prop.values
|
|
3151
3185
|
|
|
3152
|
-
# Align y values to x depth grid if needed
|
|
3186
|
+
# Align y values to x depth grid if needed using appropriate method
|
|
3153
3187
|
if needs_alignment(y_prop.depth, depths):
|
|
3154
|
-
y_values =
|
|
3188
|
+
y_values = align_property(y_prop, depths)
|
|
3155
3189
|
|
|
3156
3190
|
# Create dataframe for this well and layer
|
|
3157
3191
|
df = pd.DataFrame({
|
|
@@ -3166,13 +3200,20 @@ class Crossplot:
|
|
|
3166
3200
|
if self.color == "label":
|
|
3167
3201
|
# Use layer label for color
|
|
3168
3202
|
df['color_val'] = layer_label
|
|
3203
|
+
elif self.color == "well":
|
|
3204
|
+
# Use well name for color (categorical)
|
|
3205
|
+
df['color_val'] = well.name
|
|
3169
3206
|
elif self.color and self.color != "depth":
|
|
3170
3207
|
try:
|
|
3171
3208
|
color_prop = well.get_property(self.color)
|
|
3172
|
-
|
|
3173
|
-
|
|
3209
|
+
# Store labels if discrete property (only once)
|
|
3210
|
+
if 'color' not in self._discrete_labels:
|
|
3211
|
+
self._store_discrete_labels(color_prop, 'color')
|
|
3212
|
+
# Align to x depth grid using appropriate method for property type
|
|
3174
3213
|
if needs_alignment(color_prop.depth, depths):
|
|
3175
|
-
color_values =
|
|
3214
|
+
color_values = align_property(color_prop, depths)
|
|
3215
|
+
else:
|
|
3216
|
+
color_values = color_prop.values
|
|
3176
3217
|
df['color_val'] = color_values
|
|
3177
3218
|
except (AttributeError, KeyError, PropertyNotFoundError):
|
|
3178
3219
|
# Silently use depth as fallback
|
|
@@ -3187,10 +3228,14 @@ class Crossplot:
|
|
|
3187
3228
|
elif self.size:
|
|
3188
3229
|
try:
|
|
3189
3230
|
size_prop = well.get_property(self.size)
|
|
3190
|
-
|
|
3191
|
-
|
|
3231
|
+
# Store labels if discrete property (only once)
|
|
3232
|
+
if 'size' not in self._discrete_labels:
|
|
3233
|
+
self._store_discrete_labels(size_prop, 'size')
|
|
3234
|
+
# Align to x depth grid using appropriate method for property type
|
|
3192
3235
|
if needs_alignment(size_prop.depth, depths):
|
|
3193
|
-
size_values =
|
|
3236
|
+
size_values = align_property(size_prop, depths)
|
|
3237
|
+
else:
|
|
3238
|
+
size_values = size_prop.values
|
|
3194
3239
|
df['size_val'] = size_values
|
|
3195
3240
|
except (AttributeError, KeyError, PropertyNotFoundError):
|
|
3196
3241
|
# Silently skip if size property not found
|
|
@@ -3203,10 +3248,14 @@ class Crossplot:
|
|
|
3203
3248
|
elif self.shape and self.shape != "well":
|
|
3204
3249
|
try:
|
|
3205
3250
|
shape_prop = well.get_property(self.shape)
|
|
3206
|
-
|
|
3207
|
-
|
|
3251
|
+
# Store labels if discrete property (only once)
|
|
3252
|
+
if 'shape' not in self._discrete_labels:
|
|
3253
|
+
self._store_discrete_labels(shape_prop, 'shape')
|
|
3254
|
+
# Align to x depth grid using appropriate method for property type
|
|
3208
3255
|
if needs_alignment(shape_prop.depth, depths):
|
|
3209
|
-
shape_values =
|
|
3256
|
+
shape_values = align_property(shape_prop, depths)
|
|
3257
|
+
else:
|
|
3258
|
+
shape_values = shape_prop.values
|
|
3210
3259
|
df['shape_val'] = shape_values
|
|
3211
3260
|
except (AttributeError, KeyError, PropertyNotFoundError):
|
|
3212
3261
|
# Silently skip if shape property not found
|
|
@@ -3370,6 +3419,201 @@ class Crossplot:
|
|
|
3370
3419
|
|
|
3371
3420
|
return best_pos, second_best_pos
|
|
3372
3421
|
|
|
3422
|
+
def _store_discrete_labels(self, prop, role: str) -> None:
|
|
3423
|
+
"""
|
|
3424
|
+
Store labels from a discrete property for later use in legends.
|
|
3425
|
+
|
|
3426
|
+
Args:
|
|
3427
|
+
prop: Property object (must have type and labels attributes)
|
|
3428
|
+
role: Property role - 'shape', 'color', or 'size'
|
|
3429
|
+
"""
|
|
3430
|
+
if hasattr(prop, 'type') and prop.type == 'discrete' and hasattr(prop, 'labels') and prop.labels:
|
|
3431
|
+
self._discrete_labels[role] = prop.labels.copy()
|
|
3432
|
+
|
|
3433
|
+
def _get_display_label(self, value, role: str) -> str:
|
|
3434
|
+
"""
|
|
3435
|
+
Get display label for a value, using stored labels for discrete properties.
|
|
3436
|
+
|
|
3437
|
+
For discrete properties with labels, converts integer codes to readable names.
|
|
3438
|
+
For continuous properties or discrete without labels, returns string value.
|
|
3439
|
+
|
|
3440
|
+
Args:
|
|
3441
|
+
value: The value to get label for (could be int, float, or string)
|
|
3442
|
+
role: Property role - 'shape', 'color', or 'size'
|
|
3443
|
+
|
|
3444
|
+
Returns:
|
|
3445
|
+
Display label string
|
|
3446
|
+
|
|
3447
|
+
Examples:
|
|
3448
|
+
>>> # For discrete property with labels {0: 'Agat top', 1: 'Cerisa Main top'}
|
|
3449
|
+
>>> self._get_display_label(0.0, 'shape')
|
|
3450
|
+
'Agat top'
|
|
3451
|
+
|
|
3452
|
+
>>> # For continuous property or no labels
|
|
3453
|
+
>>> self._get_display_label(2.5, 'color')
|
|
3454
|
+
'2.5'
|
|
3455
|
+
"""
|
|
3456
|
+
if role in self._discrete_labels:
|
|
3457
|
+
# Try to convert to integer and look up label
|
|
3458
|
+
try:
|
|
3459
|
+
int_val = int(np.round(float(value)))
|
|
3460
|
+
return self._discrete_labels[role].get(int_val, str(value))
|
|
3461
|
+
except (ValueError, TypeError):
|
|
3462
|
+
return str(value)
|
|
3463
|
+
return str(value)
|
|
3464
|
+
|
|
3465
|
+
def _is_edge_location(self, location: str) -> bool:
|
|
3466
|
+
"""Check if a legend location is on the left or right edge.
|
|
3467
|
+
|
|
3468
|
+
Args:
|
|
3469
|
+
location: Matplotlib location string
|
|
3470
|
+
|
|
3471
|
+
Returns:
|
|
3472
|
+
True if on left or right edge (for vertical stacking)
|
|
3473
|
+
"""
|
|
3474
|
+
edge_locations = ['upper left', 'center left', 'lower left',
|
|
3475
|
+
'upper right', 'center right', 'lower right']
|
|
3476
|
+
return location in edge_locations
|
|
3477
|
+
|
|
3478
|
+
def _create_grouped_legends(self,
|
|
3479
|
+
shape_handles, shape_title: str,
|
|
3480
|
+
color_handles, color_title: str,
|
|
3481
|
+
location: str) -> None:
|
|
3482
|
+
"""Create grouped legends in the same region, stacked or side-by-side.
|
|
3483
|
+
|
|
3484
|
+
When both shape and color legends are needed, this groups them in the same
|
|
3485
|
+
1/9th section without overlap. Stacks vertically on edges, side-by-side elsewhere.
|
|
3486
|
+
|
|
3487
|
+
Args:
|
|
3488
|
+
shape_handles: List of handles for shape legend
|
|
3489
|
+
shape_title: Title for shape legend
|
|
3490
|
+
color_handles: List of handles for color legend
|
|
3491
|
+
color_title: Title for color legend
|
|
3492
|
+
location: Matplotlib location string for positioning
|
|
3493
|
+
"""
|
|
3494
|
+
is_edge = self._is_edge_location(location)
|
|
3495
|
+
|
|
3496
|
+
# Determine base anchor point from location string
|
|
3497
|
+
# Map location to (x, y) coordinates in figure space
|
|
3498
|
+
anchor_map = {
|
|
3499
|
+
'upper left': (0.02, 0.98),
|
|
3500
|
+
'upper center': (0.5, 0.98),
|
|
3501
|
+
'upper right': (0.98, 0.98),
|
|
3502
|
+
'center left': (0.02, 0.5),
|
|
3503
|
+
'center': (0.5, 0.5),
|
|
3504
|
+
'center right': (0.98, 0.5),
|
|
3505
|
+
'lower left': (0.02, 0.02),
|
|
3506
|
+
'lower center': (0.5, 0.02),
|
|
3507
|
+
'lower right': (0.98, 0.02),
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
base_x, base_y = anchor_map.get(location, (0.98, 0.98))
|
|
3511
|
+
|
|
3512
|
+
if is_edge:
|
|
3513
|
+
# Stack vertically on edges
|
|
3514
|
+
# Position shape legend at the top
|
|
3515
|
+
shape_legend = self.ax.legend(
|
|
3516
|
+
handles=shape_handles,
|
|
3517
|
+
title=shape_title,
|
|
3518
|
+
loc=location,
|
|
3519
|
+
frameon=True,
|
|
3520
|
+
framealpha=0.9,
|
|
3521
|
+
edgecolor='black',
|
|
3522
|
+
bbox_to_anchor=(base_x, base_y),
|
|
3523
|
+
bbox_transform=self.fig.transFigure
|
|
3524
|
+
)
|
|
3525
|
+
shape_legend.get_title().set_fontweight('bold')
|
|
3526
|
+
self.ax.add_artist(shape_legend)
|
|
3527
|
+
|
|
3528
|
+
# Calculate offset for color legend below shape legend
|
|
3529
|
+
# Estimate shape legend height and add spacing
|
|
3530
|
+
shape_height = len(shape_handles) * 0.025 + 0.05 # Rough estimate
|
|
3531
|
+
|
|
3532
|
+
# Adjust y position for color legend
|
|
3533
|
+
if 'upper' in location:
|
|
3534
|
+
color_y = base_y - shape_height - 0.02 # Stack below
|
|
3535
|
+
elif 'lower' in location:
|
|
3536
|
+
color_y = base_y + shape_height + 0.02 # Stack above
|
|
3537
|
+
else: # center
|
|
3538
|
+
color_y = base_y - shape_height / 2 - 0.01 # Stack below
|
|
3539
|
+
|
|
3540
|
+
color_legend = self.ax.legend(
|
|
3541
|
+
handles=color_handles,
|
|
3542
|
+
title=color_title,
|
|
3543
|
+
loc=location,
|
|
3544
|
+
frameon=True,
|
|
3545
|
+
framealpha=0.9,
|
|
3546
|
+
edgecolor='black',
|
|
3547
|
+
bbox_to_anchor=(base_x, color_y),
|
|
3548
|
+
bbox_transform=self.fig.transFigure
|
|
3549
|
+
)
|
|
3550
|
+
color_legend.get_title().set_fontweight('bold')
|
|
3551
|
+
else:
|
|
3552
|
+
# Place side by side for non-edge locations (top, bottom, center)
|
|
3553
|
+
# Estimate width of each legend
|
|
3554
|
+
legend_width = 0.15
|
|
3555
|
+
|
|
3556
|
+
if 'center' in location and location != 'center left' and location != 'center right':
|
|
3557
|
+
# For center positions, place them side by side
|
|
3558
|
+
shape_x = base_x - legend_width / 2 - 0.01
|
|
3559
|
+
color_x = base_x + legend_width / 2 + 0.01
|
|
3560
|
+
|
|
3561
|
+
shape_legend = self.ax.legend(
|
|
3562
|
+
handles=shape_handles,
|
|
3563
|
+
title=shape_title,
|
|
3564
|
+
loc='center',
|
|
3565
|
+
frameon=True,
|
|
3566
|
+
framealpha=0.9,
|
|
3567
|
+
edgecolor='black',
|
|
3568
|
+
bbox_to_anchor=(shape_x, base_y),
|
|
3569
|
+
bbox_transform=self.fig.transFigure
|
|
3570
|
+
)
|
|
3571
|
+
shape_legend.get_title().set_fontweight('bold')
|
|
3572
|
+
self.ax.add_artist(shape_legend)
|
|
3573
|
+
|
|
3574
|
+
color_legend = self.ax.legend(
|
|
3575
|
+
handles=color_handles,
|
|
3576
|
+
title=color_title,
|
|
3577
|
+
loc='center',
|
|
3578
|
+
frameon=True,
|
|
3579
|
+
framealpha=0.9,
|
|
3580
|
+
edgecolor='black',
|
|
3581
|
+
bbox_to_anchor=(color_x, base_y),
|
|
3582
|
+
bbox_transform=self.fig.transFigure
|
|
3583
|
+
)
|
|
3584
|
+
color_legend.get_title().set_fontweight('bold')
|
|
3585
|
+
else:
|
|
3586
|
+
# For other positions, fall back to stacking
|
|
3587
|
+
shape_legend = self.ax.legend(
|
|
3588
|
+
handles=shape_handles,
|
|
3589
|
+
title=shape_title,
|
|
3590
|
+
loc=location,
|
|
3591
|
+
frameon=True,
|
|
3592
|
+
framealpha=0.9,
|
|
3593
|
+
edgecolor='black'
|
|
3594
|
+
)
|
|
3595
|
+
shape_legend.get_title().set_fontweight('bold')
|
|
3596
|
+
self.ax.add_artist(shape_legend)
|
|
3597
|
+
|
|
3598
|
+
# Estimate offset
|
|
3599
|
+
shape_height = len(shape_handles) * 0.025 + 0.05
|
|
3600
|
+
if 'upper' in location:
|
|
3601
|
+
color_y = base_y - shape_height - 0.02
|
|
3602
|
+
else:
|
|
3603
|
+
color_y = base_y + shape_height + 0.02
|
|
3604
|
+
|
|
3605
|
+
color_legend = self.ax.legend(
|
|
3606
|
+
handles=color_handles,
|
|
3607
|
+
title=color_title,
|
|
3608
|
+
loc=location,
|
|
3609
|
+
frameon=True,
|
|
3610
|
+
framealpha=0.9,
|
|
3611
|
+
edgecolor='black',
|
|
3612
|
+
bbox_to_anchor=(base_x, color_y),
|
|
3613
|
+
bbox_transform=self.fig.transFigure
|
|
3614
|
+
)
|
|
3615
|
+
color_legend.get_title().set_fontweight('bold')
|
|
3616
|
+
|
|
3373
3617
|
def _format_regression_label(self, name: str, reg, include_equation: bool = None, include_r2: bool = None) -> str:
|
|
3374
3618
|
"""Format a modern, compact regression label.
|
|
3375
3619
|
|
|
@@ -3891,7 +4135,7 @@ class Crossplot:
|
|
|
3891
4135
|
# Create custom legend handles
|
|
3892
4136
|
legend_elements = [Patch(facecolor=category_colors[cat],
|
|
3893
4137
|
edgecolor=self.edge_color,
|
|
3894
|
-
label=
|
|
4138
|
+
label=self._get_display_label(cat, 'color'))
|
|
3895
4139
|
for cat in unique_categories]
|
|
3896
4140
|
|
|
3897
4141
|
colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
|
|
@@ -4001,20 +4245,53 @@ class Crossplot:
|
|
|
4001
4245
|
edgecolors=self.edge_color,
|
|
4002
4246
|
linewidths=self.edge_width,
|
|
4003
4247
|
marker=marker,
|
|
4004
|
-
label=
|
|
4248
|
+
label=self._get_display_label(group_name, 'shape')
|
|
4005
4249
|
)
|
|
4006
4250
|
|
|
4007
4251
|
if first_scatter is None and self.color and not is_categorical:
|
|
4008
4252
|
first_scatter = scatter
|
|
4009
4253
|
|
|
4010
|
-
#
|
|
4011
|
-
|
|
4254
|
+
# Check if we need both shape and color legends (grouped layout)
|
|
4255
|
+
need_shape_legend = self.show_legend
|
|
4256
|
+
need_color_legend = self.color and is_categorical and self.show_legend
|
|
4257
|
+
|
|
4258
|
+
if need_shape_legend and need_color_legend:
|
|
4259
|
+
# Create grouped legends in the same region
|
|
4012
4260
|
# Get best location based on data density
|
|
4013
4261
|
if self._data is not None:
|
|
4014
4262
|
primary_loc, _ = self._find_best_legend_locations(self._data)
|
|
4015
4263
|
else:
|
|
4016
4264
|
primary_loc = 'best'
|
|
4017
4265
|
|
|
4266
|
+
# Prepare shape legend handles (from scatter plots)
|
|
4267
|
+
shape_handles, _ = self.ax.get_legend_handles_labels()
|
|
4268
|
+
|
|
4269
|
+
# Prepare color legend handles
|
|
4270
|
+
c_vals_all = data['color_val'].values
|
|
4271
|
+
unique_categories = pd.Series(c_vals_all).dropna().unique()
|
|
4272
|
+
color_handles = [Patch(facecolor=category_colors[cat],
|
|
4273
|
+
edgecolor=self.edge_color,
|
|
4274
|
+
label=self._get_display_label(cat, 'color'))
|
|
4275
|
+
for cat in unique_categories]
|
|
4276
|
+
|
|
4277
|
+
colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
|
|
4278
|
+
|
|
4279
|
+
# Create grouped legends
|
|
4280
|
+
self._create_grouped_legends(
|
|
4281
|
+
shape_handles=shape_handles,
|
|
4282
|
+
shape_title=group_label,
|
|
4283
|
+
color_handles=color_handles,
|
|
4284
|
+
color_title=colorbar_label,
|
|
4285
|
+
location=primary_loc
|
|
4286
|
+
)
|
|
4287
|
+
|
|
4288
|
+
elif need_shape_legend:
|
|
4289
|
+
# Only shape legend needed
|
|
4290
|
+
if self._data is not None:
|
|
4291
|
+
primary_loc, _ = self._find_best_legend_locations(self._data)
|
|
4292
|
+
else:
|
|
4293
|
+
primary_loc = 'best'
|
|
4294
|
+
|
|
4018
4295
|
legend = self.ax.legend(
|
|
4019
4296
|
title=group_label,
|
|
4020
4297
|
loc=primary_loc,
|
|
@@ -4023,43 +4300,15 @@ class Crossplot:
|
|
|
4023
4300
|
edgecolor='black'
|
|
4024
4301
|
)
|
|
4025
4302
|
legend.get_title().set_fontweight('bold')
|
|
4026
|
-
|
|
4027
4303
|
# Store the primary legend so it persists when regression legend is added
|
|
4028
4304
|
self.ax.add_artist(legend)
|
|
4029
4305
|
|
|
4030
|
-
# Add colorbar
|
|
4031
|
-
if self.color:
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
# Create custom legend handles
|
|
4038
|
-
legend_elements = [Patch(facecolor=category_colors[cat],
|
|
4039
|
-
edgecolor=self.edge_color,
|
|
4040
|
-
label=str(cat))
|
|
4041
|
-
for cat in unique_categories]
|
|
4042
|
-
|
|
4043
|
-
colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
|
|
4044
|
-
|
|
4045
|
-
# Find a good location for the color legend (opposite corner from shape legend)
|
|
4046
|
-
if self._data is not None:
|
|
4047
|
-
_, secondary_loc = self._find_best_legend_locations(self._data)
|
|
4048
|
-
else:
|
|
4049
|
-
secondary_loc = 'upper right'
|
|
4050
|
-
|
|
4051
|
-
color_legend = self.ax.legend(handles=legend_elements,
|
|
4052
|
-
title=colorbar_label,
|
|
4053
|
-
loc=secondary_loc,
|
|
4054
|
-
frameon=True,
|
|
4055
|
-
framealpha=0.9,
|
|
4056
|
-
edgecolor='black')
|
|
4057
|
-
color_legend.get_title().set_fontweight('bold')
|
|
4058
|
-
elif not is_categorical and self.show_colorbar and first_scatter:
|
|
4059
|
-
# Add continuous colorbar
|
|
4060
|
-
self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
|
|
4061
|
-
colorbar_label = self.color if self.color != "depth" else "Depth"
|
|
4062
|
-
self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
|
|
4306
|
+
# Add colorbar for continuous color mapping
|
|
4307
|
+
if self.color and not is_categorical and self.show_colorbar and first_scatter:
|
|
4308
|
+
# Add continuous colorbar
|
|
4309
|
+
self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
|
|
4310
|
+
colorbar_label = self.color if self.color != "depth" else "Depth"
|
|
4311
|
+
self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
|
|
4063
4312
|
|
|
4064
4313
|
def add_regression(
|
|
4065
4314
|
self,
|
|
@@ -3,13 +3,13 @@ well_log_toolkit/exceptions.py,sha256=X_fzC7d4yaBFO9Vx74dEIB6xmI9Agi6_bTU3MPxn6k
|
|
|
3
3
|
well_log_toolkit/las_file.py,sha256=Tj0mRfX1aX2s6uug7BBlY1m_mu3G50EGxHGzD0eEedE,53876
|
|
4
4
|
well_log_toolkit/manager.py,sha256=PuHF8rqypirNIN77STHcvg8WneExikpq6ZvkcRbcQpg,109776
|
|
5
5
|
well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
|
|
6
|
-
well_log_toolkit/property.py,sha256=
|
|
6
|
+
well_log_toolkit/property.py,sha256=B-3mXNJmvIqjjMdsu1kgVSwMgEwbJ36wn_n_oppdJFw,76769
|
|
7
7
|
well_log_toolkit/regression.py,sha256=7D3oI-1XVlFb-mOoHTxTTtUHERFyvQSBAzJzAGVoZnk,25192
|
|
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=
|
|
10
|
+
well_log_toolkit/visualization.py,sha256=204l_LFgcSfnpHti1Xc1iGF-KUVEyxkJNS88gkUDeXA,179959
|
|
11
11
|
well_log_toolkit/well.py,sha256=Aav5Y-rui8YsJdvk7BFndNPUu1O9mcjwDApAGyqV9kw,104535
|
|
12
|
-
well_log_toolkit-0.1.
|
|
13
|
-
well_log_toolkit-0.1.
|
|
14
|
-
well_log_toolkit-0.1.
|
|
15
|
-
well_log_toolkit-0.1.
|
|
12
|
+
well_log_toolkit-0.1.132.dist-info/METADATA,sha256=5IVBXhf2t9MQW4Gswct9F1IHl5JJnKOWDZxK4g2vUq4,59810
|
|
13
|
+
well_log_toolkit-0.1.132.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
well_log_toolkit-0.1.132.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
|
|
15
|
+
well_log_toolkit-0.1.132.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|