well-log-toolkit 0.1.134__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,23 +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')
3670
3700
  shape_legend.set_clip_on(False) # Prevent clipping outside axes
3671
3701
  self.ax.add_artist(shape_legend)
3672
3702
 
3673
3703
  # Calculate offset for color legend below shape legend
3674
- # Estimate shape legend height and add spacing
3675
- 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
3676
3706
 
3677
3707
  # Adjust y position for color legend
3678
3708
  if 'upper' in location:
3679
- color_y = base_y - shape_height - 0.02 # Stack below
3709
+ color_y = base_y - shape_height # Stack below
3680
3710
  elif 'lower' in location:
3681
- color_y = base_y + shape_height + 0.02 # Stack above
3711
+ color_y = base_y + shape_height # Stack above
3682
3712
  else: # center
3683
- color_y = base_y - shape_height / 2 - 0.01 # Stack below
3713
+ color_y = base_y - shape_height / 2 # Stack below
3684
3714
 
3685
3715
  color_legend = self.ax.legend(
3686
3716
  handles=color_handles,
@@ -3690,19 +3720,19 @@ class Crossplot:
3690
3720
  framealpha=0.9,
3691
3721
  edgecolor='black',
3692
3722
  bbox_to_anchor=(base_x, color_y),
3693
- bbox_transform=self.fig.transFigure
3723
+ bbox_transform=self.ax.transAxes
3694
3724
  )
3695
3725
  color_legend.get_title().set_fontweight('bold')
3696
3726
  color_legend.set_clip_on(False) # Prevent clipping outside axes
3697
3727
  else:
3698
3728
  # Place side by side for non-edge locations (top, bottom, center)
3699
- # Estimate width of each legend
3700
- legend_width = 0.15
3729
+ # Estimate width of each legend in axes coordinates
3730
+ legend_width = 0.20
3701
3731
 
3702
3732
  if 'center' in location and location != 'center left' and location != 'center right':
3703
3733
  # For center positions, place them side by side
3704
- shape_x = base_x - legend_width / 2 - 0.01
3705
- 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
3706
3736
 
3707
3737
  shape_legend = self.ax.legend(
3708
3738
  handles=shape_handles,
@@ -3712,7 +3742,7 @@ class Crossplot:
3712
3742
  framealpha=0.9,
3713
3743
  edgecolor='black',
3714
3744
  bbox_to_anchor=(shape_x, base_y),
3715
- bbox_transform=self.fig.transFigure
3745
+ bbox_transform=self.ax.transAxes
3716
3746
  )
3717
3747
  shape_legend.get_title().set_fontweight('bold')
3718
3748
  shape_legend.set_clip_on(False) # Prevent clipping outside axes
@@ -3726,7 +3756,7 @@ class Crossplot:
3726
3756
  framealpha=0.9,
3727
3757
  edgecolor='black',
3728
3758
  bbox_to_anchor=(color_x, base_y),
3729
- bbox_transform=self.fig.transFigure
3759
+ bbox_transform=self.ax.transAxes
3730
3760
  )
3731
3761
  color_legend.get_title().set_fontweight('bold')
3732
3762
  color_legend.set_clip_on(False) # Prevent clipping outside axes
@@ -3738,18 +3768,20 @@ class Crossplot:
3738
3768
  loc=location,
3739
3769
  frameon=True,
3740
3770
  framealpha=0.9,
3741
- edgecolor='black'
3771
+ edgecolor='black',
3772
+ bbox_to_anchor=(base_x, base_y),
3773
+ bbox_transform=self.ax.transAxes
3742
3774
  )
3743
3775
  shape_legend.get_title().set_fontweight('bold')
3744
3776
  shape_legend.set_clip_on(False) # Prevent clipping outside axes
3745
3777
  self.ax.add_artist(shape_legend)
3746
3778
 
3747
- # Estimate offset
3748
- 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
3749
3781
  if 'upper' in location:
3750
- color_y = base_y - shape_height - 0.02
3782
+ color_y = base_y - shape_height
3751
3783
  else:
3752
- color_y = base_y + shape_height + 0.02
3784
+ color_y = base_y + shape_height
3753
3785
 
3754
3786
  color_legend = self.ax.legend(
3755
3787
  handles=color_handles,
@@ -3759,7 +3791,7 @@ class Crossplot:
3759
3791
  framealpha=0.9,
3760
3792
  edgecolor='black',
3761
3793
  bbox_to_anchor=(base_x, color_y),
3762
- bbox_transform=self.fig.transFigure
3794
+ bbox_transform=self.ax.transAxes
3763
3795
  )
3764
3796
  color_legend.get_title().set_fontweight('bold')
3765
3797
  color_legend.set_clip_on(False) # Prevent clipping outside axes
@@ -3837,14 +3869,32 @@ class Crossplot:
3837
3869
  secondary_loc = 'lower right'
3838
3870
 
3839
3871
  # Determine descriptive title based on regression type
3872
+ # Extract the regression type and add it to the title
3873
+ reg_type_str = None
3840
3874
  if self.regression_by_color_and_shape:
3841
- regression_title = 'Regressions 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)
3842
3878
  elif self.regression_by_color:
3843
- regression_title = 'Regressions 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)
3844
3882
  elif self.regression_by_group:
3845
- regression_title = 'Regressions 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)
3846
3886
  else:
3847
- regression_title = 'Regressions'
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
3848
3898
 
3849
3899
  # Import legend from matplotlib
3850
3900
  from matplotlib.legend import Legend
@@ -4284,11 +4334,15 @@ class Crossplot:
4284
4334
  if self.y_log:
4285
4335
  self.ax.set_yscale('log')
4286
4336
 
4287
- # Disable scientific notation on axes
4337
+ # Disable scientific notation on linear axes only
4338
+ # (log axes use matplotlib's default log formatter for proper log scale labels)
4288
4339
  from matplotlib.ticker import ScalarFormatter
4289
4340
  formatter = ScalarFormatter(useOffset=False)
4290
4341
  formatter.set_scientific(False)
4291
- self.ax.yaxis.set_major_formatter(formatter)
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)
4292
4346
  if not self.x_log:
4293
4347
  self.ax.xaxis.set_major_formatter(formatter)
4294
4348
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.134
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=xpui0fOv6JRaQTIK8o7QC8FNaJbjzdNCwnJ2KyOi4lk,196670
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.134.dist-info/METADATA,sha256=afUQvTYy5a4E2vvkdKjjrrWsvkC491apUHSUNQNw1jE,59810
13
- well_log_toolkit-0.1.134.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- well_log_toolkit-0.1.134.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.134.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,,