well-log-toolkit 0.1.129__py3-none-any.whl → 0.1.130__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,7 +3005,11 @@ class Crossplot:
3003
3005
  # Store parameters
3004
3006
  self.x = x
3005
3007
  self.y = y
3006
- self.shape = shape
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
3007
3013
  self.color = color
3008
3014
  self.size = size
3009
3015
  self.colortemplate = colortemplate
@@ -3679,6 +3685,36 @@ class Crossplot:
3679
3685
  if update_legend and self.ax is not None:
3680
3686
  self._update_regression_legend()
3681
3687
 
3688
+ def _is_categorical_color(self, color_values: np.ndarray) -> bool:
3689
+ """
3690
+ Determine if color values should be treated as categorical vs continuous.
3691
+
3692
+ Returns True if:
3693
+ - Less than 50 unique values
3694
+
3695
+ This helps distinguish between:
3696
+ - Categorical: well names, facies, zones, labels
3697
+ - Continuous: depth, porosity, saturation
3698
+ """
3699
+ # Remove NaN values for analysis
3700
+ valid_values = color_values[~pd.isna(color_values)]
3701
+
3702
+ if len(valid_values) == 0:
3703
+ return False
3704
+
3705
+ unique_values = np.unique(valid_values)
3706
+ n_unique = len(unique_values)
3707
+
3708
+ # Check if values are numeric - if not, it's categorical
3709
+ try:
3710
+ # Try to convert to float - if this fails, it's categorical (strings)
3711
+ _ = unique_values.astype(float)
3712
+ except (ValueError, TypeError):
3713
+ return True
3714
+
3715
+ # Apply the criteria
3716
+ return n_unique < 50
3717
+
3682
3718
  def plot(self) -> 'Crossplot':
3683
3719
  """Generate the crossplot figure."""
3684
3720
  # Prepare data
@@ -3778,13 +3814,41 @@ class Crossplot:
3778
3814
  y_vals = data['y'].values
3779
3815
 
3780
3816
  # Determine colors
3817
+ is_categorical = False
3781
3818
  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
3819
+ c_vals_raw = data['color_val'].values
3820
+
3821
+ # Check if color data is categorical
3822
+ is_categorical = self._is_categorical_color(c_vals_raw)
3823
+
3824
+ if is_categorical:
3825
+ # Handle categorical colors with discrete palette
3826
+ unique_categories = pd.Series(c_vals_raw).dropna().unique()
3827
+ n_categories = len(unique_categories)
3828
+
3829
+ # Create color map for categories
3830
+ if n_categories <= len(DEFAULT_COLORS):
3831
+ color_palette = DEFAULT_COLORS
3832
+ else:
3833
+ # Use colormap for many categories
3834
+ cmap_obj = cm.get_cmap(self.colortemplate, n_categories)
3835
+ color_palette = [cmap_obj(i) for i in range(n_categories)]
3836
+
3837
+ category_colors = {cat: color_palette[i % len(color_palette)]
3838
+ for i, cat in enumerate(unique_categories)}
3839
+
3840
+ # Map each value to its color
3841
+ c_vals = [category_colors.get(val, DEFAULT_COLORS[0]) for val in c_vals_raw]
3842
+ cmap = None
3843
+ vmin = vmax = None
3786
3844
  else:
3787
- vmin, vmax = np.nanmin(c_vals), np.nanmax(c_vals)
3845
+ # Handle continuous colors
3846
+ c_vals = c_vals_raw
3847
+ cmap = self.colortemplate
3848
+ if self.color_range:
3849
+ vmin, vmax = self.color_range
3850
+ else:
3851
+ vmin, vmax = np.nanmin(c_vals), np.nanmax(c_vals)
3788
3852
  else:
3789
3853
  c_vals = DEFAULT_COLORS[0]
3790
3854
  cmap = None
@@ -3817,11 +3881,33 @@ class Crossplot:
3817
3881
  marker=self.marker
3818
3882
  )
3819
3883
 
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')
3884
+ # Add colorbar or legend based on color type
3885
+ if self.color:
3886
+ if is_categorical and self.show_legend:
3887
+ # Create legend for categorical colors
3888
+ c_vals_raw = data['color_val'].values
3889
+ unique_categories = pd.Series(c_vals_raw).dropna().unique()
3890
+
3891
+ # Create custom legend handles
3892
+ legend_elements = [Patch(facecolor=category_colors[cat],
3893
+ edgecolor=self.edge_color,
3894
+ label=str(cat))
3895
+ for cat in unique_categories]
3896
+
3897
+ colorbar_label = self.color if self.color != "depth" and self.color != "label" else "Category"
3898
+ legend = self.ax.legend(handles=legend_elements,
3899
+ title=colorbar_label,
3900
+ loc='best',
3901
+ frameon=True,
3902
+ framealpha=0.9,
3903
+ edgecolor='black')
3904
+ legend.get_title().set_fontweight('bold')
3905
+ self.ax.add_artist(legend)
3906
+ elif not is_categorical and self.show_colorbar:
3907
+ # Add colorbar for continuous colors
3908
+ self.colorbar = self.fig.colorbar(self.scatter, ax=self.ax)
3909
+ colorbar_label = self.color if self.color != "depth" else "Depth"
3910
+ self.colorbar.set_label(colorbar_label, fontsize=11, fontweight='bold')
3825
3911
 
3826
3912
  def _plot_by_groups(self, data: pd.DataFrame) -> None:
3827
3913
  """Plot data grouped by shape/well."""
@@ -3836,6 +3922,27 @@ class Crossplot:
3836
3922
  # Define markers for different groups
3837
3923
  markers = ['o', 's', '^', 'D', 'v', '<', '>', 'p', '*', 'h']
3838
3924
 
3925
+ # Check if colors are categorical (check once for all data)
3926
+ is_categorical = False
3927
+ category_colors = {}
3928
+ if self.color:
3929
+ c_vals_all = data['color_val'].values
3930
+ is_categorical = self._is_categorical_color(c_vals_all)
3931
+
3932
+ if is_categorical:
3933
+ # Prepare color mapping for categorical values
3934
+ unique_categories = pd.Series(c_vals_all).dropna().unique()
3935
+ n_categories = len(unique_categories)
3936
+
3937
+ if n_categories <= len(DEFAULT_COLORS):
3938
+ color_palette = DEFAULT_COLORS
3939
+ else:
3940
+ cmap_obj = cm.get_cmap(self.colortemplate, n_categories)
3941
+ color_palette = [cmap_obj(i) for i in range(n_categories)]
3942
+
3943
+ category_colors = {cat: color_palette[i % len(color_palette)]
3944
+ for i, cat in enumerate(unique_categories)}
3945
+
3839
3946
  # Track for colorbar (use first scatter)
3840
3947
  first_scatter = None
3841
3948
 
@@ -3848,13 +3955,22 @@ class Crossplot:
3848
3955
 
3849
3956
  # Determine colors
3850
3957
  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
3958
+ c_vals_raw = group_data['color_val'].values
3959
+
3960
+ if is_categorical:
3961
+ # Map categorical values to colors
3962
+ c_vals = [category_colors.get(val, DEFAULT_COLORS[0]) for val in c_vals_raw]
3963
+ cmap = None
3964
+ vmin = vmax = None
3855
3965
  else:
3856
- # Use global range from all data
3857
- vmin, vmax = np.nanmin(data['color_val']), np.nanmax(data['color_val'])
3966
+ # Use continuous color mapping
3967
+ c_vals = c_vals_raw
3968
+ cmap = self.colortemplate
3969
+ if self.color_range:
3970
+ vmin, vmax = self.color_range
3971
+ else:
3972
+ # Use global range from all data
3973
+ vmin, vmax = np.nanmin(data['color_val']), np.nanmax(data['color_val'])
3858
3974
  else:
3859
3975
  c_vals = DEFAULT_COLORS[idx % len(DEFAULT_COLORS)]
3860
3976
  cmap = None
@@ -3888,7 +4004,7 @@ class Crossplot:
3888
4004
  label=str(group_name)
3889
4005
  )
3890
4006
 
3891
- if first_scatter is None and self.color:
4007
+ if first_scatter is None and self.color and not is_categorical:
3892
4008
  first_scatter = scatter
3893
4009
 
3894
4010
  # Add legend with smart placement
@@ -3911,11 +4027,39 @@ class Crossplot:
3911
4027
  # Store the primary legend so it persists when regression legend is added
3912
4028
  self.ax.add_artist(legend)
3913
4029
 
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')
4030
+ # Add colorbar or color legend based on color type
4031
+ if self.color:
4032
+ if is_categorical and self.show_legend:
4033
+ # Create separate legend for categorical colors
4034
+ c_vals_all = data['color_val'].values
4035
+ unique_categories = pd.Series(c_vals_all).dropna().unique()
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')
3919
4063
 
3920
4064
  def add_regression(
3921
4065
  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.130
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=2Wmydurnll31chjhFlx6xr1UpySvi0OBu4ClLUte1BA,169609
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.130.dist-info/METADATA,sha256=GT1zxMtQKy_wI9XVrgNGerOGzmzLS-K7XiAeVS4uUH8,59810
13
+ well_log_toolkit-0.1.130.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ well_log_toolkit-0.1.130.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.130.dist-info/RECORD,,