well-log-toolkit 0.1.129__py3-none-any.whl → 0.1.131__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.
@@ -2598,7 +2598,7 @@ class WellDataManager:
2598
2598
  y: Optional[str] = None,
2599
2599
  wells: Optional[list[str]] = None,
2600
2600
  layers: Optional[dict[str, list[str]]] = None,
2601
- shape: str = "well",
2601
+ shape: Optional[str] = None,
2602
2602
  color: Optional[str] = None,
2603
2603
  size: Optional[str] = None,
2604
2604
  colortemplate: str = "viridis",
@@ -2761,6 +2761,14 @@ class WellDataManager:
2761
2761
  if not well_objects:
2762
2762
  raise ValueError("No wells available for crossplot")
2763
2763
 
2764
+ # Set default shape: "well" when no layers, "label" when layers provided
2765
+ if shape is None and layers is None:
2766
+ shape = "well"
2767
+
2768
+ # Set default color: "well" when shape defaults to "label" (i.e., when layers provided)
2769
+ if color is None and layers is not None and shape is None:
2770
+ color = "well"
2771
+
2764
2772
  return CrossplotClass(
2765
2773
  wells=well_objects,
2766
2774
  x=x,
@@ -12,9 +12,10 @@ import warnings
12
12
  import numpy as np
13
13
  import pandas as pd
14
14
  import matplotlib.pyplot as plt
15
+ import matplotlib.cm as cm
15
16
  from matplotlib.collections import PolyCollection
16
17
  from matplotlib.colors import Normalize
17
- from matplotlib.patches import Rectangle
18
+ from matplotlib.patches import Rectangle, Patch
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from .well import Well
@@ -2776,16 +2777,16 @@ class Crossplot:
2776
2777
  Dictionary mapping layer labels to [x_property, y_property] lists.
2777
2778
  Use this to combine multiple property pairs in a single plot.
2778
2779
  Example: {"Core": ["CorePor", "CorePerm"], "Sidewall": ["SWPor", "SWPerm"]}
2779
- When using layers, you can use "label" for color/shape/size to differentiate
2780
- between layer types. Default: None
2780
+ When using layers, shape defaults to "label" and color defaults to "well" for
2781
+ easy visualization of both layer types and wells. Default: None
2781
2782
  shape : str, optional
2782
2783
  Property name for shape mapping. Use "well" to map shapes by well name,
2783
2784
  or "label" (when using layers) to map shapes by layer type.
2784
- Default: None (single shape)
2785
+ Default: "well" for multi-well plots, "label" when layers provided, None otherwise
2785
2786
  color : str, optional
2786
2787
  Property name for color mapping. Use "depth" to color by depth,
2787
- or "label" (when using layers) to color by layer type.
2788
- Default: None (single color)
2788
+ "well" to color by well, or "label" (when using layers) to color by layer type.
2789
+ Default: "well" when layers provided, None otherwise
2789
2790
  size : str, optional
2790
2791
  Property name for size mapping, or "label" (when using layers) to
2791
2792
  size by layer type.
@@ -2901,8 +2902,9 @@ class Crossplot:
2901
2902
  ... "Core": ["CorePor_obds", "CorePerm_obds"],
2902
2903
  ... "Sidewall": ["SidewallPor_ob", "SidewallPerm_ob"]
2903
2904
  ... },
2904
- ... shape="label", # Different shapes for Core vs Sidewall
2905
2905
  ... y_log=True
2906
+ ... # shape defaults to "label" - different shapes for Core vs Sidewall
2907
+ ... # color defaults to "well" - different colors for each well
2906
2908
  ... )
2907
2909
  >>> plot.show()
2908
2910
 
@@ -2912,6 +2914,7 @@ class Crossplot:
2912
2914
  ... .add_layer("CorePor_obds", "CorePerm_obds", label="Core") \\
2913
2915
  ... .add_layer("SidewallPor_ob", "SidewallPerm_ob", label="Sidewall") \\
2914
2916
  ... .show()
2917
+ ... # Automatically uses shape="label" and color="well"
2915
2918
 
2916
2919
  Layers with regression by color (single trend per well):
2917
2920
 
@@ -2920,9 +2923,8 @@ class Crossplot:
2920
2923
  ... "Core": ["CorePor_obds", "CorePerm_obds"],
2921
2924
  ... "Sidewall": ["SidewallPor_ob", "SidewallPerm_ob"]
2922
2925
  ... },
2923
- ... shape="label", # Different shapes for data types
2924
- ... color="well", # Color by well
2925
2926
  ... regression_by_color="linear" # One trend per well (combining both data types)
2927
+ ... # Defaults: shape="label" (different shapes), color="well" (different colors)
2926
2928
  ... )
2927
2929
  >>> plot.show()
2928
2930
 
@@ -3003,8 +3005,16 @@ class Crossplot:
3003
3005
  # Store parameters
3004
3006
  self.x = x
3005
3007
  self.y = y
3006
- self.shape = shape
3007
- self.color = color
3008
+ # Default shape to "label" when layers are provided
3009
+ if shape is None and layers is not None:
3010
+ self.shape = "label"
3011
+ else:
3012
+ self.shape = shape
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
3008
3018
  self.size = size
3009
3019
  self.colortemplate = colortemplate
3010
3020
  self.color_range = color_range
@@ -3160,6 +3170,9 @@ class Crossplot:
3160
3170
  if self.color == "label":
3161
3171
  # Use layer label for color
3162
3172
  df['color_val'] = layer_label
3173
+ elif self.color == "well":
3174
+ # Use well name for color (categorical)
3175
+ df['color_val'] = well.name
3163
3176
  elif self.color and self.color != "depth":
3164
3177
  try:
3165
3178
  color_prop = well.get_property(self.color)
@@ -3679,6 +3692,36 @@ class Crossplot:
3679
3692
  if update_legend and self.ax is not None:
3680
3693
  self._update_regression_legend()
3681
3694
 
3695
+ def _is_categorical_color(self, color_values: np.ndarray) -> bool:
3696
+ """
3697
+ Determine if color values should be treated as categorical vs continuous.
3698
+
3699
+ Returns True if:
3700
+ - Less than 50 unique values
3701
+
3702
+ This helps distinguish between:
3703
+ - Categorical: well names, facies, zones, labels
3704
+ - Continuous: depth, porosity, saturation
3705
+ """
3706
+ # Remove NaN values for analysis
3707
+ valid_values = color_values[~pd.isna(color_values)]
3708
+
3709
+ if len(valid_values) == 0:
3710
+ return False
3711
+
3712
+ unique_values = np.unique(valid_values)
3713
+ n_unique = len(unique_values)
3714
+
3715
+ # Check if values are numeric - if not, it's categorical
3716
+ try:
3717
+ # Try to convert to float - if this fails, it's categorical (strings)
3718
+ _ = unique_values.astype(float)
3719
+ except (ValueError, TypeError):
3720
+ return True
3721
+
3722
+ # Apply the criteria
3723
+ return n_unique < 50
3724
+
3682
3725
  def plot(self) -> 'Crossplot':
3683
3726
  """Generate the crossplot figure."""
3684
3727
  # Prepare data
@@ -3778,13 +3821,41 @@ class Crossplot:
3778
3821
  y_vals = data['y'].values
3779
3822
 
3780
3823
  # Determine colors
3824
+ is_categorical = False
3781
3825
  if self.color:
3782
- c_vals = data['color_val'].values
3783
- cmap = self.colortemplate
3784
- if self.color_range:
3785
- vmin, vmax = self.color_range
3826
+ c_vals_raw = data['color_val'].values
3827
+
3828
+ # Check if color data is categorical
3829
+ is_categorical = self._is_categorical_color(c_vals_raw)
3830
+
3831
+ if is_categorical:
3832
+ # Handle categorical colors with discrete palette
3833
+ unique_categories = pd.Series(c_vals_raw).dropna().unique()
3834
+ n_categories = len(unique_categories)
3835
+
3836
+ # Create color map for categories
3837
+ if n_categories <= len(DEFAULT_COLORS):
3838
+ color_palette = DEFAULT_COLORS
3839
+ else:
3840
+ # Use colormap for many categories
3841
+ cmap_obj = cm.get_cmap(self.colortemplate, n_categories)
3842
+ color_palette = [cmap_obj(i) for i in range(n_categories)]
3843
+
3844
+ category_colors = {cat: color_palette[i % len(color_palette)]
3845
+ for i, cat in enumerate(unique_categories)}
3846
+
3847
+ # Map each value to its color
3848
+ c_vals = [category_colors.get(val, DEFAULT_COLORS[0]) for val in c_vals_raw]
3849
+ cmap = None
3850
+ vmin = vmax = None
3786
3851
  else:
3787
- vmin, vmax = np.nanmin(c_vals), np.nanmax(c_vals)
3852
+ # Handle continuous colors
3853
+ c_vals = c_vals_raw
3854
+ cmap = self.colortemplate
3855
+ if self.color_range:
3856
+ vmin, vmax = self.color_range
3857
+ else:
3858
+ vmin, vmax = np.nanmin(c_vals), np.nanmax(c_vals)
3788
3859
  else:
3789
3860
  c_vals = DEFAULT_COLORS[0]
3790
3861
  cmap = None
@@ -3817,11 +3888,33 @@ class Crossplot:
3817
3888
  marker=self.marker
3818
3889
  )
3819
3890
 
3820
- # Add colorbar if using color mapping
3821
- if self.color and self.show_colorbar:
3822
- self.colorbar = self.fig.colorbar(self.scatter, ax=self.ax)
3823
- colorbar_label = self.color if self.color != "depth" else "Depth"
3824
- self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
3891
+ # Add colorbar or legend based on color type
3892
+ if self.color:
3893
+ if is_categorical and self.show_legend:
3894
+ # Create legend for categorical colors
3895
+ c_vals_raw = data['color_val'].values
3896
+ unique_categories = pd.Series(c_vals_raw).dropna().unique()
3897
+
3898
+ # Create custom legend handles
3899
+ legend_elements = [Patch(facecolor=category_colors[cat],
3900
+ edgecolor=self.edge_color,
3901
+ label=str(cat))
3902
+ for cat in unique_categories]
3903
+
3904
+ colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
3905
+ legend = self.ax.legend(handles=legend_elements,
3906
+ title=colorbar_label,
3907
+ loc='best',
3908
+ frameon=True,
3909
+ framealpha=0.9,
3910
+ edgecolor='black')
3911
+ legend.get_title().set_fontweight('bold')
3912
+ self.ax.add_artist(legend)
3913
+ elif not is_categorical and self.show_colorbar:
3914
+ # Add colorbar for continuous colors
3915
+ self.colorbar = self.fig.colorbar(self.scatter, ax=self.ax)
3916
+ colorbar_label = self.color if self.color != "depth" else "Depth"
3917
+ self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
3825
3918
 
3826
3919
  def _plot_by_groups(self, data: pd.DataFrame) -> None:
3827
3920
  """Plot data grouped by shape/well."""
@@ -3836,6 +3929,27 @@ class Crossplot:
3836
3929
  # Define markers for different groups
3837
3930
  markers = ['o', 's', '^', 'D', 'v', '<', '>', 'p', '*', 'h']
3838
3931
 
3932
+ # Check if colors are categorical (check once for all data)
3933
+ is_categorical = False
3934
+ category_colors = {}
3935
+ if self.color:
3936
+ c_vals_all = data['color_val'].values
3937
+ is_categorical = self._is_categorical_color(c_vals_all)
3938
+
3939
+ if is_categorical:
3940
+ # Prepare color mapping for categorical values
3941
+ unique_categories = pd.Series(c_vals_all).dropna().unique()
3942
+ n_categories = len(unique_categories)
3943
+
3944
+ if n_categories <= len(DEFAULT_COLORS):
3945
+ color_palette = DEFAULT_COLORS
3946
+ else:
3947
+ cmap_obj = cm.get_cmap(self.colortemplate, n_categories)
3948
+ color_palette = [cmap_obj(i) for i in range(n_categories)]
3949
+
3950
+ category_colors = {cat: color_palette[i % len(color_palette)]
3951
+ for i, cat in enumerate(unique_categories)}
3952
+
3839
3953
  # Track for colorbar (use first scatter)
3840
3954
  first_scatter = None
3841
3955
 
@@ -3848,13 +3962,22 @@ class Crossplot:
3848
3962
 
3849
3963
  # Determine colors
3850
3964
  if self.color:
3851
- c_vals = group_data['color_val'].values
3852
- cmap = self.colortemplate
3853
- if self.color_range:
3854
- vmin, vmax = self.color_range
3965
+ c_vals_raw = group_data['color_val'].values
3966
+
3967
+ if is_categorical:
3968
+ # Map categorical values to colors
3969
+ c_vals = [category_colors.get(val, DEFAULT_COLORS[0]) for val in c_vals_raw]
3970
+ cmap = None
3971
+ vmin = vmax = None
3855
3972
  else:
3856
- # Use global range from all data
3857
- vmin, vmax = np.nanmin(data['color_val']), np.nanmax(data['color_val'])
3973
+ # Use continuous color mapping
3974
+ c_vals = c_vals_raw
3975
+ cmap = self.colortemplate
3976
+ if self.color_range:
3977
+ vmin, vmax = self.color_range
3978
+ else:
3979
+ # Use global range from all data
3980
+ vmin, vmax = np.nanmin(data['color_val']), np.nanmax(data['color_val'])
3858
3981
  else:
3859
3982
  c_vals = DEFAULT_COLORS[idx % len(DEFAULT_COLORS)]
3860
3983
  cmap = None
@@ -3888,7 +4011,7 @@ class Crossplot:
3888
4011
  label=str(group_name)
3889
4012
  )
3890
4013
 
3891
- if first_scatter is None and self.color:
4014
+ if first_scatter is None and self.color and not is_categorical:
3892
4015
  first_scatter = scatter
3893
4016
 
3894
4017
  # Add legend with smart placement
@@ -3911,11 +4034,39 @@ class Crossplot:
3911
4034
  # Store the primary legend so it persists when regression legend is added
3912
4035
  self.ax.add_artist(legend)
3913
4036
 
3914
- # Add colorbar if using color mapping
3915
- if self.color and self.show_colorbar and first_scatter:
3916
- self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
3917
- colorbar_label = self.color if self.color != "depth" else "Depth"
3918
- self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
4037
+ # Add colorbar or color legend based on color type
4038
+ if self.color:
4039
+ if is_categorical and self.show_legend:
4040
+ # Create separate legend for categorical colors
4041
+ c_vals_all = data['color_val'].values
4042
+ unique_categories = pd.Series(c_vals_all).dropna().unique()
4043
+
4044
+ # Create custom legend handles
4045
+ legend_elements = [Patch(facecolor=category_colors[cat],
4046
+ edgecolor=self.edge_color,
4047
+ label=str(cat))
4048
+ for cat in unique_categories]
4049
+
4050
+ colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
4051
+
4052
+ # Find a good location for the color legend (opposite corner from shape legend)
4053
+ if self._data is not None:
4054
+ _, secondary_loc = self._find_best_legend_locations(self._data)
4055
+ else:
4056
+ secondary_loc = 'upper right'
4057
+
4058
+ color_legend = self.ax.legend(handles=legend_elements,
4059
+ title=colorbar_label,
4060
+ loc=secondary_loc,
4061
+ frameon=True,
4062
+ framealpha=0.9,
4063
+ edgecolor='black')
4064
+ color_legend.get_title().set_fontweight('bold')
4065
+ elif not is_categorical and self.show_colorbar and first_scatter:
4066
+ # Add continuous colorbar
4067
+ self.colorbar = self.fig.colorbar(first_scatter, ax=self.ax)
4068
+ colorbar_label = self.color if self.color != "depth" else "Depth"
4069
+ self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
3919
4070
 
3920
4071
  def add_regression(
3921
4072
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.129
3
+ Version: 0.1.131
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
@@ -1,15 +1,15 @@
1
1
  well_log_toolkit/__init__.py,sha256=ilJAIIhh68pYfD9I3V53juTEJpoMN8oHpcpEFNpuXAQ,3793
2
2
  well_log_toolkit/exceptions.py,sha256=X_fzC7d4yaBFO9Vx74dEIB6xmI9Agi6_bTU3MPxn6ko,985
3
3
  well_log_toolkit/las_file.py,sha256=Tj0mRfX1aX2s6uug7BBlY1m_mu3G50EGxHGzD0eEedE,53876
4
- well_log_toolkit/manager.py,sha256=gRgHJlNrB-JONc4gIxT_LvwDW2ZUO4tHRopzDd2x7WI,109423
4
+ well_log_toolkit/manager.py,sha256=PuHF8rqypirNIN77STHcvg8WneExikpq6ZvkcRbcQpg,109776
5
5
  well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
6
6
  well_log_toolkit/property.py,sha256=WOzoNQcmHCQ8moIKsnSyLgVC8s4LBu2x5IBXtFzmMe8,76236
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=oyjENj3milpJ7O0Wc0Kraucu8HmklHbw--s7Z4_r-Ig,162865
10
+ well_log_toolkit/visualization.py,sha256=MqMK5J3LGCXc2YDT1tlmLC6oMWA-F-YBuLBh5rbrPpI,169985
11
11
  well_log_toolkit/well.py,sha256=Aav5Y-rui8YsJdvk7BFndNPUu1O9mcjwDApAGyqV9kw,104535
12
- well_log_toolkit-0.1.129.dist-info/METADATA,sha256=SWxynEaltIqmkiwd-Ws5XKZnbVXMD_j_JEzhgUlsuFk,59810
13
- well_log_toolkit-0.1.129.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- well_log_toolkit-0.1.129.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.129.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.131.dist-info/METADATA,sha256=XC_qlZ-SFO5rFspWuxMXfvl1viLa-GpKZXKxWvA-RBc,59810
13
+ well_log_toolkit-0.1.131.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ well_log_toolkit-0.1.131.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.131.dist-info/RECORD,,