well-log-toolkit 0.1.133__py3-none-any.whl → 0.1.135__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.
@@ -3475,22 +3475,48 @@ class Crossplot:
3475
3475
  Returns:
3476
3476
  Tuple of (segment_number, matplotlib_location_string)
3477
3477
  """
3478
- # Get x and y bounds
3478
+ # Get x and y data values
3479
3479
  x_vals = data['x'].values
3480
3480
  y_vals = data['y'].values
3481
3481
 
3482
- # Handle log scales for binning
3482
+ # Get axes limits to convert data coordinates to axes-normalized coordinates (0-1)
3483
+ # This ensures we're dividing the GRAPH AREA, not the data space
3484
+ if self.ax is not None:
3485
+ x_lim = self.ax.get_xlim()
3486
+ y_lim = self.ax.get_ylim()
3487
+ else:
3488
+ # Fallback if ax not available yet
3489
+ x_lim = (np.nanmin(x_vals), np.nanmax(x_vals))
3490
+ y_lim = (np.nanmin(y_vals), np.nanmax(y_vals))
3491
+
3492
+ # Handle logarithmic axes - transform to log space for proper visual segment calculation
3493
+ # On log axes, equal visual spacing corresponds to equal ratios, not equal differences
3483
3494
  if self.x_log:
3484
- x_vals = np.log10(x_vals[x_vals > 0])
3495
+ # Filter out non-positive values before log transform
3496
+ x_valid = x_vals > 0
3497
+ x_vals_transformed = np.where(x_valid, np.log10(x_vals), np.nan)
3498
+ x_lim_transformed = (np.log10(max(x_lim[0], 1e-10)), np.log10(max(x_lim[1], 1e-10)))
3499
+ else:
3500
+ x_vals_transformed = x_vals
3501
+ x_lim_transformed = x_lim
3502
+
3485
3503
  if self.y_log:
3486
- y_vals = np.log10(y_vals[y_vals > 0])
3504
+ # Filter out non-positive values before log transform
3505
+ y_valid = y_vals > 0
3506
+ y_vals_transformed = np.where(y_valid, np.log10(y_vals), np.nan)
3507
+ y_lim_transformed = (np.log10(max(y_lim[0], 1e-10)), np.log10(max(y_lim[1], 1e-10)))
3508
+ else:
3509
+ y_vals_transformed = y_vals
3510
+ y_lim_transformed = y_lim
3487
3511
 
3488
- x_min, x_max = np.nanmin(x_vals), np.nanmax(x_vals)
3489
- y_min, y_max = np.nanmin(y_vals), np.nanmax(y_vals)
3512
+ # Normalize transformed coordinates to axes coordinates (0-1)
3513
+ # This divides the visible graph area properly, accounting for log scales
3514
+ x_norm = (x_vals_transformed - x_lim_transformed[0]) / (x_lim_transformed[1] - x_lim_transformed[0])
3515
+ y_norm = (y_vals_transformed - y_lim_transformed[0]) / (y_lim_transformed[1] - y_lim_transformed[0])
3490
3516
 
3491
- # Create 3x3 grid and count points in each square
3492
- x_bins = np.linspace(x_min, x_max, 4)
3493
- y_bins = np.linspace(y_min, y_max, 4)
3517
+ # Create 3x3 grid in axes-normalized space (0-1)
3518
+ x_bins = np.linspace(0, 1, 4)
3519
+ y_bins = np.linspace(0, 1, 4)
3494
3520
 
3495
3521
  # Map segments 1-9 to grid positions (i, j)
3496
3522
  # Segment numbering:
@@ -3522,12 +3548,12 @@ class Crossplot:
3522
3548
  9: 'lower right',
3523
3549
  }
3524
3550
 
3525
- # Count points in each segment
3526
- total_points = len(x_vals)
3551
+ # Count points in each segment using normalized coordinates
3552
+ total_points = len(x_norm)
3527
3553
  segment_counts = {}
3528
3554
  for segment, (i, j) in segment_to_grid.items():
3529
- x_mask = (x_vals >= x_bins[i]) & (x_vals < x_bins[i+1])
3530
- y_mask = (y_vals >= y_bins[j]) & (y_vals < y_bins[j+1])
3555
+ x_mask = (x_norm >= x_bins[i]) & (x_norm < x_bins[i+1])
3556
+ y_mask = (y_norm >= y_bins[j]) & (y_norm < y_bins[j+1])
3531
3557
  count = np.sum(x_mask & y_mask)
3532
3558
  segment_counts[segment] = count
3533
3559
 
@@ -3638,20 +3664,24 @@ class Crossplot:
3638
3664
  is_edge = self._is_edge_location(location)
3639
3665
 
3640
3666
  # Determine base anchor point from location string
3641
- # Map location to (x, y) coordinates in figure space
3667
+ # Map location to (x, y) coordinates in AXES space (0-1 within the graph area)
3668
+ # These match the segment corners:
3669
+ # Segment 1=upper left (0,1), 2=upper center (0.5,1), 3=upper right (1,1)
3670
+ # Segment 4=center left (0,0.5), 5=center (0.5,0.5), 6=center right (1,0.5)
3671
+ # Segment 7=lower left (0,0), 8=lower center (0.5,0), 9=lower right (1,0)
3642
3672
  anchor_map = {
3643
- 'upper left': (0.02, 0.98),
3644
- 'upper center': (0.5, 0.98),
3645
- 'upper right': (0.98, 0.98),
3646
- 'center left': (0.02, 0.5),
3673
+ 'upper left': (0, 1),
3674
+ 'upper center': (0.5, 1),
3675
+ 'upper right': (1, 1),
3676
+ 'center left': (0, 0.5),
3647
3677
  'center': (0.5, 0.5),
3648
- 'center right': (0.98, 0.5),
3649
- 'lower left': (0.02, 0.02),
3650
- 'lower center': (0.5, 0.02),
3651
- 'lower right': (0.98, 0.02),
3678
+ 'center right': (1, 0.5),
3679
+ 'lower left': (0, 0),
3680
+ 'lower center': (0.5, 0),
3681
+ 'lower right': (1, 0),
3652
3682
  }
3653
3683
 
3654
- base_x, base_y = anchor_map.get(location, (0.98, 0.98))
3684
+ base_x, base_y = anchor_map.get(location, (1, 1))
3655
3685
 
3656
3686
  if is_edge:
3657
3687
  # Stack vertically on edges
@@ -3664,22 +3694,23 @@ class Crossplot:
3664
3694
  framealpha=0.9,
3665
3695
  edgecolor='black',
3666
3696
  bbox_to_anchor=(base_x, base_y),
3667
- bbox_transform=self.fig.transFigure
3697
+ bbox_transform=self.ax.transAxes
3668
3698
  )
3669
3699
  shape_legend.get_title().set_fontweight('bold')
3700
+ shape_legend.set_clip_on(False) # Prevent clipping outside axes
3670
3701
  self.ax.add_artist(shape_legend)
3671
3702
 
3672
3703
  # Calculate offset for color legend below shape legend
3673
- # Estimate shape legend height and add spacing
3674
- shape_height = len(shape_handles) * 0.025 + 0.05 # Rough estimate
3704
+ # Estimate shape legend height in axes coordinates
3705
+ shape_height = len(shape_handles) * 0.05 + 0.08 # Adjusted for axes space
3675
3706
 
3676
3707
  # Adjust y position for color legend
3677
3708
  if 'upper' in location:
3678
- color_y = base_y - shape_height - 0.02 # Stack below
3709
+ color_y = base_y - shape_height # Stack below
3679
3710
  elif 'lower' in location:
3680
- color_y = base_y + shape_height + 0.02 # Stack above
3711
+ color_y = base_y + shape_height # Stack above
3681
3712
  else: # center
3682
- color_y = base_y - shape_height / 2 - 0.01 # Stack below
3713
+ color_y = base_y - shape_height / 2 # Stack below
3683
3714
 
3684
3715
  color_legend = self.ax.legend(
3685
3716
  handles=color_handles,
@@ -3689,18 +3720,19 @@ class Crossplot:
3689
3720
  framealpha=0.9,
3690
3721
  edgecolor='black',
3691
3722
  bbox_to_anchor=(base_x, color_y),
3692
- bbox_transform=self.fig.transFigure
3723
+ bbox_transform=self.ax.transAxes
3693
3724
  )
3694
3725
  color_legend.get_title().set_fontweight('bold')
3726
+ color_legend.set_clip_on(False) # Prevent clipping outside axes
3695
3727
  else:
3696
3728
  # Place side by side for non-edge locations (top, bottom, center)
3697
- # Estimate width of each legend
3698
- legend_width = 0.15
3729
+ # Estimate width of each legend in axes coordinates
3730
+ legend_width = 0.20
3699
3731
 
3700
3732
  if 'center' in location and location != 'center left' and location != 'center right':
3701
3733
  # For center positions, place them side by side
3702
- shape_x = base_x - legend_width / 2 - 0.01
3703
- color_x = base_x + legend_width / 2 + 0.01
3734
+ shape_x = base_x - legend_width / 2
3735
+ color_x = base_x + legend_width / 2
3704
3736
 
3705
3737
  shape_legend = self.ax.legend(
3706
3738
  handles=shape_handles,
@@ -3710,9 +3742,10 @@ class Crossplot:
3710
3742
  framealpha=0.9,
3711
3743
  edgecolor='black',
3712
3744
  bbox_to_anchor=(shape_x, base_y),
3713
- bbox_transform=self.fig.transFigure
3745
+ bbox_transform=self.ax.transAxes
3714
3746
  )
3715
3747
  shape_legend.get_title().set_fontweight('bold')
3748
+ shape_legend.set_clip_on(False) # Prevent clipping outside axes
3716
3749
  self.ax.add_artist(shape_legend)
3717
3750
 
3718
3751
  color_legend = self.ax.legend(
@@ -3723,9 +3756,10 @@ class Crossplot:
3723
3756
  framealpha=0.9,
3724
3757
  edgecolor='black',
3725
3758
  bbox_to_anchor=(color_x, base_y),
3726
- bbox_transform=self.fig.transFigure
3759
+ bbox_transform=self.ax.transAxes
3727
3760
  )
3728
3761
  color_legend.get_title().set_fontweight('bold')
3762
+ color_legend.set_clip_on(False) # Prevent clipping outside axes
3729
3763
  else:
3730
3764
  # For other positions, fall back to stacking
3731
3765
  shape_legend = self.ax.legend(
@@ -3734,17 +3768,20 @@ class Crossplot:
3734
3768
  loc=location,
3735
3769
  frameon=True,
3736
3770
  framealpha=0.9,
3737
- edgecolor='black'
3771
+ edgecolor='black',
3772
+ bbox_to_anchor=(base_x, base_y),
3773
+ bbox_transform=self.ax.transAxes
3738
3774
  )
3739
3775
  shape_legend.get_title().set_fontweight('bold')
3776
+ shape_legend.set_clip_on(False) # Prevent clipping outside axes
3740
3777
  self.ax.add_artist(shape_legend)
3741
3778
 
3742
- # Estimate offset
3743
- shape_height = len(shape_handles) * 0.025 + 0.05
3779
+ # Estimate offset in axes coordinates
3780
+ shape_height = len(shape_handles) * 0.05 + 0.08
3744
3781
  if 'upper' in location:
3745
- color_y = base_y - shape_height - 0.02
3782
+ color_y = base_y - shape_height
3746
3783
  else:
3747
- color_y = base_y + shape_height + 0.02
3784
+ color_y = base_y + shape_height
3748
3785
 
3749
3786
  color_legend = self.ax.legend(
3750
3787
  handles=color_handles,
@@ -3754,9 +3791,10 @@ class Crossplot:
3754
3791
  framealpha=0.9,
3755
3792
  edgecolor='black',
3756
3793
  bbox_to_anchor=(base_x, color_y),
3757
- bbox_transform=self.fig.transFigure
3794
+ bbox_transform=self.ax.transAxes
3758
3795
  )
3759
3796
  color_legend.get_title().set_fontweight('bold')
3797
+ color_legend.set_clip_on(False) # Prevent clipping outside axes
3760
3798
 
3761
3799
  def _format_regression_label(self, name: str, reg, include_equation: bool = None, include_r2: bool = None) -> str:
3762
3800
  """Format a modern, compact regression label.
@@ -3786,11 +3824,8 @@ class Crossplot:
3786
3824
 
3787
3825
  # Add R² on second line if requested (will be styled grey later)
3788
3826
  if include_r2:
3789
- # Format R² with appropriate note
3790
- if self.y_log:
3791
- r2_label = f"R² = {reg.r_squared:.3f} (log)"
3792
- else:
3793
- r2_label = f"R² = {reg.r_squared:.3f}"
3827
+ # Format R² (no suffix needed)
3828
+ r2_label = f"R² = {reg.r_squared:.3f}"
3794
3829
  return f"{first_line}\n{r2_label}"
3795
3830
  else:
3796
3831
  return first_line
@@ -3833,6 +3868,34 @@ class Crossplot:
3833
3868
  # Fallback if data not available
3834
3869
  secondary_loc = 'lower right'
3835
3870
 
3871
+ # Determine descriptive title based on regression type
3872
+ # Extract the regression type and add it to the title
3873
+ reg_type_str = None
3874
+ if self.regression_by_color_and_shape:
3875
+ base_title = 'Regressions by color and shape'
3876
+ config = self._parse_regression_config(self.regression_by_color_and_shape)
3877
+ reg_type_str = config.get('type', None)
3878
+ elif self.regression_by_color:
3879
+ base_title = 'Regressions by color'
3880
+ config = self._parse_regression_config(self.regression_by_color)
3881
+ reg_type_str = config.get('type', None)
3882
+ elif self.regression_by_group:
3883
+ base_title = 'Regressions by group'
3884
+ config = self._parse_regression_config(self.regression_by_group)
3885
+ reg_type_str = config.get('type', None)
3886
+ else:
3887
+ base_title = 'Regressions'
3888
+ if self.regression:
3889
+ config = self._parse_regression_config(self.regression)
3890
+ reg_type_str = config.get('type', None)
3891
+
3892
+ # Add regression type to title (e.g., "Regressions by color - Power")
3893
+ if reg_type_str:
3894
+ reg_type_display = reg_type_str.capitalize()
3895
+ regression_title = f"{base_title} - {reg_type_display}"
3896
+ else:
3897
+ regression_title = base_title
3898
+
3836
3899
  # Import legend from matplotlib
3837
3900
  from matplotlib.legend import Legend
3838
3901
 
@@ -3848,7 +3911,7 @@ class Crossplot:
3848
3911
  fancybox=False,
3849
3912
  shadow=False,
3850
3913
  fontsize=9,
3851
- title='Regressions',
3914
+ title=regression_title,
3852
3915
  title_fontsize=10
3853
3916
  )
3854
3917
 
@@ -3902,20 +3965,16 @@ class Crossplot:
3902
3965
 
3903
3966
  # Determine grouping column based on what's being used for colors in the plot
3904
3967
  group_column = None
3905
- group_label = None
3906
3968
 
3907
3969
  if self.color and 'color_val' in data.columns:
3908
3970
  # User specified explicit color mapping
3909
3971
  group_column = 'color_val'
3910
- group_label = self.color
3911
3972
  elif self.shape == "well" and 'well' in data.columns:
3912
3973
  # When shape="well", each well gets a different color in the plot
3913
3974
  group_column = 'well'
3914
- group_label = 'well'
3915
3975
  elif self.shape and self.shape != "well" and 'shape_val' in data.columns:
3916
3976
  # When shape is a property, each shape group gets a different color
3917
3977
  group_column = 'shape_val'
3918
- group_label = self.shape
3919
3978
 
3920
3979
  if group_column is None:
3921
3980
  warnings.warn(
@@ -3961,12 +4020,15 @@ class Crossplot:
3961
4020
  # Use the same color as the data points for this group
3962
4021
  group_config['line_color'] = group_colors_map.get(group_name, regression_colors[color_idx % len(regression_colors)])
3963
4022
 
4023
+ # Get display label for group name (converts codes to formation names)
4024
+ group_display = self._get_display_label(group_name, 'color')
4025
+
3964
4026
  # Skip legend update for all but last regression
3965
4027
  is_last = (idx == n_groups - 1)
3966
4028
  self._add_group_regression(
3967
4029
  x_vals[mask], y_vals[mask],
3968
4030
  reg_type,
3969
- name=f"{group_label}={group_name}",
4031
+ name=group_display,
3970
4032
  config=group_config,
3971
4033
  update_legend=is_last
3972
4034
  )
@@ -3999,12 +4061,15 @@ class Crossplot:
3999
4061
  # Use the same color as the data points for this group
4000
4062
  group_config['line_color'] = group_colors_map.get(group_name, regression_colors[color_idx % len(regression_colors)])
4001
4063
 
4064
+ # Get display label for group name (converts codes to formation names)
4065
+ group_display = self._get_display_label(group_name, 'color')
4066
+
4002
4067
  # Skip legend update for all but last regression
4003
4068
  is_last = (idx == n_groups - 1)
4004
4069
  self._add_group_regression(
4005
4070
  x_vals[mask], y_vals[mask],
4006
4071
  reg_type,
4007
- name=f"{group_label}={group_name}",
4072
+ name=group_display,
4008
4073
  config=group_config,
4009
4074
  update_legend=is_last
4010
4075
  )
@@ -4269,6 +4334,18 @@ class Crossplot:
4269
4334
  if self.y_log:
4270
4335
  self.ax.set_yscale('log')
4271
4336
 
4337
+ # Disable scientific notation on linear axes only
4338
+ # (log axes use matplotlib's default log formatter for proper log scale labels)
4339
+ from matplotlib.ticker import ScalarFormatter
4340
+ formatter = ScalarFormatter(useOffset=False)
4341
+ formatter.set_scientific(False)
4342
+
4343
+ # Only apply to linear axes - log axes need their default formatter
4344
+ if not self.y_log:
4345
+ self.ax.yaxis.set_major_formatter(formatter)
4346
+ if not self.x_log:
4347
+ self.ax.xaxis.set_major_formatter(formatter)
4348
+
4272
4349
  # Labels and title
4273
4350
  self.ax.set_xlabel(self.xlabel, fontsize=12, fontweight='bold')
4274
4351
  self.ax.set_ylabel(self.ylabel, fontsize=12, fontweight='bold')
@@ -4449,6 +4526,7 @@ class Crossplot:
4449
4526
  framealpha=0.9,
4450
4527
  edgecolor='black')
4451
4528
  legend.get_title().set_fontweight('bold')
4529
+ legend.set_clip_on(False) # Prevent clipping outside axes
4452
4530
  self.ax.add_artist(legend)
4453
4531
  elif not is_categorical and self.show_colorbar:
4454
4532
  # Add colorbar for continuous colors
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.133
3
+ Version: 0.1.135
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
@@ -7,9 +7,9 @@ well_log_toolkit/property.py,sha256=B-3mXNJmvIqjjMdsu1kgVSwMgEwbJ36wn_n_oppdJFw,
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=rzwSkqUOEeSnnuvPvNMQ56-F316JBk0XSqAdqyoRjEc,195237
10
+ well_log_toolkit/visualization.py,sha256=vnO8QjSNvvnQHGKXpe7BsLaQ0CMdLr0ruBt7poD-8Mc,199727
11
11
  well_log_toolkit/well.py,sha256=Aav5Y-rui8YsJdvk7BFndNPUu1O9mcjwDApAGyqV9kw,104535
12
- well_log_toolkit-0.1.133.dist-info/METADATA,sha256=F10zbt2b4FNQY76xM0eWAhG_y_ZABGkmY1nvem8doUY,59810
13
- well_log_toolkit-0.1.133.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- well_log_toolkit-0.1.133.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.133.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.135.dist-info/METADATA,sha256=q39tCQUCWVP2XaMzNgtega97YSMCukEnIgSnodrCxfI,59810
13
+ well_log_toolkit-0.1.135.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ well_log_toolkit-0.1.135.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.135.dist-info/RECORD,,