soiltextureplot 0.1.0__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.
@@ -0,0 +1,32 @@
1
+ """
2
+ Soil Texture Plotting Package
3
+ =============================
4
+
5
+ A Python package for soil texture classification and plotting on ternary diagrams.
6
+
7
+ Modules
8
+ -------
9
+ classifier
10
+ Logic for classifying soil samples into texture classes.
11
+ datasets
12
+ Loading sample datasets.
13
+ plotting
14
+ Functions for creating ternary plots.
15
+ systems
16
+ Definitions of standard soil texture classification systems (USDA, HYPRES, etc.).
17
+ triangle
18
+ Core triangle geometry utilities.
19
+ utils
20
+ General utility functions.
21
+ """
22
+
23
+ from .classifier import PolygonClassifier
24
+ from .systems import TextureSystem, get_texture_system
25
+ from .plotting import plot_triangle_with_points
26
+
27
+ __all__ = [
28
+ "PolygonClassifier",
29
+ "TextureSystem",
30
+ "get_texture_system",
31
+ "plot_triangle_with_points",
32
+ ]
@@ -0,0 +1,95 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, List
3
+
4
+ import numpy as np
5
+ from matplotlib.path import Path
6
+
7
+ from .systems import TextureSystem
8
+ from .utils import ternary_to_cartesian
9
+
10
+
11
+ @dataclass
12
+ class PolygonClassifier:
13
+ """
14
+ Classifies points into soil texture classes using polygon inclusion.
15
+
16
+ Parameters
17
+ ----------
18
+ system : TextureSystem
19
+ The texture system definition containing polygon vertices.
20
+ _paths : Dict[str, Path]
21
+ Precomputed matplotlib Paths for point testing.
22
+ _class_order : List[str]
23
+ Ordered list of class names for consistency.
24
+ """
25
+ system: TextureSystem
26
+ _paths: Dict[str, Path]
27
+ _class_order: List[str]
28
+
29
+ @classmethod
30
+ def from_system(cls, system: TextureSystem) -> "PolygonClassifier":
31
+ """
32
+ Create a classifier from a TextureSystem.
33
+
34
+ Parameters
35
+ ----------
36
+ system : TextureSystem
37
+ The texture classification system to use.
38
+
39
+ Returns
40
+ -------
41
+ PolygonClassifier
42
+ Initialized classifier instance.
43
+ """
44
+ paths: Dict[str, Path] = {}
45
+
46
+ for name, vertices in system.polygons.items():
47
+ verts = np.array(vertices, dtype=float) # shape (N, 3) (clay, sand, silt)
48
+ clay, sand, silt = verts.T
49
+ xy = ternary_to_cartesian(clay, sand, silt)
50
+ # Ensure polygon is closed
51
+ if not np.allclose(xy[0], xy[-1]):
52
+ xy = np.vstack([xy, xy[0]])
53
+
54
+ paths[name] = Path(xy)
55
+
56
+ class_order = list(system.polygons.keys())
57
+ return cls(system=system, _paths=paths, _class_order=class_order)
58
+
59
+ def classify_points(
60
+ self,
61
+ clay: np.ndarray,
62
+ sand: np.ndarray,
63
+ silt: np.ndarray
64
+ ) -> np.ndarray:
65
+ """
66
+ Classify many points at once.
67
+
68
+ Parameters
69
+ ----------
70
+ clay : np.ndarray
71
+ Array of clay percentages.
72
+ sand : np.ndarray
73
+ Array of sand percentages.
74
+ silt : np.ndarray
75
+ Array of silt percentages.
76
+
77
+ Returns
78
+ -------
79
+ np.ndarray
80
+ Array of class names (dtype=object). Returns 'Unknown' if no
81
+ polygon contains the point.
82
+ """
83
+ xy = ternary_to_cartesian(clay, sand, silt)
84
+ n = xy.shape[0]
85
+ result = np.full(n, "Unknown", dtype=object)
86
+
87
+ # Vectorized point-in-polygon: test all points against each Path
88
+ for class_name in self._class_order:
89
+ path = self._paths[class_name]
90
+ inside = path.contains_points(xy)
91
+ # Only overwrite where still Unknown
92
+ mask = inside & (result == "Unknown")
93
+ result[mask] = class_name
94
+
95
+ return result
@@ -0,0 +1,106 @@
1
+ from typing import Dict, List, Annotated
2
+
3
+ # Type alias for texture definitions: Name -> List of Polygon Vertices (Clay, Sand, Silt)
4
+ TextureClasses = Dict[str, List[List[float]]]
5
+
6
+ USDA_TEXTURE_CLASSES: TextureClasses = {
7
+ "sand": [[10.0, 90.0, 0.0], [0.0, 100.0, 0.0], [0.0, 85.0, 15.0]],
8
+ "loamy sand": [
9
+ [15.0, 85.0, 0.0],
10
+ [10.0, 90.0, 0.0],
11
+ [0.0, 85.0, 15.0],
12
+ [0.0, 70.0, 30.0],
13
+ ],
14
+ "sandy loam": [
15
+ [20.0, 52.0, 28.0],
16
+ [20.0, 80.0, 0.0],
17
+ [15.0, 85.0, 0.0],
18
+ [0.0, 70.0, 30.0],
19
+ [0.0, 50.0, 50.0],
20
+ [7.0, 43.0, 50.0],
21
+ [7.0, 52.0, 41.0],
22
+ ],
23
+ "loam": [
24
+ [27.0, 23.0, 50.0],
25
+ [27.0, 45.0, 28.0],
26
+ [20.0, 52.0, 28.0],
27
+ [7.0, 52.0, 41.0],
28
+ [7.0, 43.0, 50.0],
29
+ ],
30
+ "silt loam": [
31
+ [27.0, 0.0, 73.0],
32
+ [27.0, 23.0, 50.0],
33
+ [0.0, 50.0, 50.0],
34
+ [0.0, 20.0, 80.0],
35
+ [12.0, 8.0, 80.0],
36
+ [12.0, 0.0, 88.0],
37
+ ],
38
+ "silt": [
39
+ [12.0, 0.0, 88.0],
40
+ [12.0, 8.0, 80.0],
41
+ [0.0, 20.0, 80.0],
42
+ [0.0, 0.0, 100.0],
43
+ ],
44
+ "sandy clay loam": [
45
+ [35.0, 45.0, 20.0],
46
+ [35.0, 65.0, 0.0],
47
+ [20.0, 80.0, 0.0],
48
+ [20.0, 52.0, 28.0],
49
+ [27.0, 45.0, 28.0],
50
+ ],
51
+ "clay loam": [
52
+ [40.0, 20.0, 40.0],
53
+ [40.0, 45.0, 15.0],
54
+ [27.0, 45.0, 28.0],
55
+ [27.0, 20.0, 53.0],
56
+ ],
57
+ "silty clay loam": [
58
+ [40.0, 0.0, 60.0],
59
+ [40.0, 20.0, 40.0],
60
+ [27.0, 20.0, 53.0],
61
+ [27.0, 0.0, 73.0],
62
+ ],
63
+ "sandy clay": [[55.0, 45.0, 0.0], [35.0, 65.0, 0.0], [35.0, 45.0, 20.0]],
64
+ "silty clay": [[60.0, 0.0, 40.0], [40.0, 20.0, 40.0], [40.0, 0.0, 60.0]],
65
+ "clay": [
66
+ [100.0, 0.0, 0.0],
67
+ [55.0, 45.0, 0.0],
68
+ [40.0, 45.0, 15.0],
69
+ [40.0, 20.0, 40.0],
70
+ [60.0, 0.0, 40.0],
71
+ ],
72
+ }
73
+
74
+ HYPRES_TEXTURE_CLASSES: TextureClasses = {
75
+ "coarse": [ # Coarse: 0 ≤ clay < 18, sand ≥ 65
76
+ [18.0, 82.0, 0.0],
77
+ [0.0, 100.0, 0.0],
78
+ [0.0, 65.0, 35.0],
79
+ [18.0, 65.0, 17.0],
80
+ ],
81
+ "medium": [ # Medium: (0–18 clay & 15–65 sand) OR (18–35 clay & sand ≥15)
82
+ [35.0, 65.0, 0.0],
83
+ [18.0, 82.0, 0.0],
84
+ [18.0, 65.0, 17.0],
85
+ [0.0, 65.0, 35.0],
86
+ [0.0, 15.0, 85.0],
87
+ [35.0, 15.0, 50.0],
88
+ ],
89
+ "medium fine": [ # Medium fine: (clay<35) and (sand<15)
90
+ [35.0, 15.0, 50.0],
91
+ [0.0, 15.0, 85.0],
92
+ [0.0, 0.0, 100.0],
93
+ [35.0, 0.0, 65.0],
94
+ ],
95
+ "fine": [ # Fine: 35 ≤ clay < 60
96
+ [60.0, 40.0, 0.0],
97
+ [35.0, 65.0, 0.0],
98
+ [35.0, 0.0, 65.0],
99
+ [60.0, 0.0, 40.0],
100
+ ],
101
+ "very fine": [ # Very fine: clay ≥ 60
102
+ [100.0, 0.0, 0.0],
103
+ [60.0, 40.0, 0.0],
104
+ [60.0, 0.0, 40.0],
105
+ ],
106
+ }
@@ -0,0 +1,247 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import pandas as pd
4
+
5
+ from typing import Optional, Union, List, Tuple
6
+ from matplotlib.figure import Figure
7
+ from matplotlib.axes import Axes
8
+ from matplotlib.colors import Colormap
9
+ from matplotlib.ticker import AutoMinorLocator, MultipleLocator
10
+ from matplotlib import colormaps
11
+
12
+ from .systems import TextureSystem
13
+ from .utils import calculate_centroid
14
+
15
+
16
+ def plot_triangle_with_points(
17
+ df: pd.DataFrame,
18
+ system: TextureSystem,
19
+ size_by: Optional[str] = None,
20
+ size_min: Optional[float] = None,
21
+ size_max: Optional[float] = None,
22
+ show_labels: Optional[bool] = None,
23
+ cmap: Optional[Union[str, List[str], Colormap]] = None,
24
+ color_points: Optional[Union[str, List[str]]] = None,
25
+ ) -> Tuple[Figure, Axes]:
26
+ """
27
+ Plot soil texture data on a ternary diagram.
28
+
29
+ Parameters
30
+ ----------
31
+ df : pd.DataFrame
32
+ DataFrame with 'clay', 'sand', 'silt' columns.
33
+ system : TextureSystem
34
+ The soil texture classification system to use.
35
+ size_by : str, optional
36
+ Column name to use for sizing points.
37
+ size_min : float, optional
38
+ Minimum point size.
39
+ size_max : float, optional
40
+ Maximum point size.
41
+ show_labels : bool, optional
42
+ If True, show sample labels on points.
43
+ cmap : str, list, or Colormap, optional
44
+ Colormap for filling texture classes. Can be a matplotlib colormap name
45
+ or a list of colors. Defaults to 'Set3_r'.
46
+ color_points : str or list, optional
47
+ Color for the scattered points.
48
+
49
+ Returns
50
+ -------
51
+ fig : Figure
52
+ The matplotlib Figure object.
53
+ ax : Axes
54
+ The matplotlib Axes object (ternary projection).
55
+ """
56
+ import mpltern # imported here so core doesn’t hard‑depend for non‑plot use
57
+
58
+ fig = plt.figure(figsize=(7, 6))
59
+ ax = fig.add_subplot(projection="ternary", ternary_sum=100.0)
60
+
61
+ _plot_background_classes(ax, system, cmap=cmap)
62
+
63
+ # coordinates in ternary order (clay, sand, silt)
64
+ t = df["clay"].to_numpy()
65
+ l = df["sand"].to_numpy()
66
+ r = df["silt"].to_numpy()
67
+
68
+ sizes = _compute_sizes(df, size_by, size_min, size_max)
69
+
70
+ ax.scatter(
71
+ t,
72
+ l,
73
+ r,
74
+ c=color_points,
75
+ alpha=0.7,
76
+ edgecolors="none",
77
+ s=sizes,
78
+ )
79
+
80
+ if show_labels and "sample_id" in df.columns:
81
+ for (_, row), tt, ll, rr in zip(df.iterrows(), t, l, r):
82
+ ax.text(
83
+ tt,
84
+ ll,
85
+ rr,
86
+ row["sample_id"],
87
+ fontsize=7,
88
+ ha="center",
89
+ va="center",
90
+ color="white",
91
+ )
92
+
93
+ ax.set_title(f"{system.name} Soil Texture Triangle", weight="bold", pad=20)
94
+ fig.tight_layout()
95
+ return fig, ax
96
+
97
+
98
+ def _plot_background_classes(
99
+ ax: Axes,
100
+ system: TextureSystem,
101
+ cmap: Optional[Union[str, List[str], Colormap]] = None
102
+ ) -> None:
103
+ """
104
+ Plots the texture class polygons in the background.
105
+
106
+ Parameters
107
+ ----------
108
+ ax : Axes
109
+ The ternary axes to plot on.
110
+ system : TextureSystem
111
+ The system containing polygons to plot.
112
+ cmap : str, list, or Colormap, optional
113
+ Colormap to use for filling polygons.
114
+ """
115
+ from itertools import cycle
116
+
117
+ num_polygons = len(system.polygons)
118
+
119
+ colors = None
120
+ if isinstance(cmap, list):
121
+ colors = cmap
122
+ else:
123
+ if cmap is None:
124
+ cmap = "Set3_r" # default
125
+ try:
126
+ colormap = colormaps.get_cmap(cmap)
127
+ # Resample the colormap to have exactly the number of colors we need.
128
+ # Then, get the list of colors by calling the resampled map.
129
+ # This is a robust way that works for both continuous and discrete colormaps.
130
+ resampled_cmap = colormap.resampled(num_polygons)
131
+ colors = [resampled_cmap(i) for i in range(resampled_cmap.N)]
132
+ except (ValueError, KeyError):
133
+ print(
134
+ f"Warning: Colormap '{cmap}' not found. Falling back to default 'Set3_r'."
135
+ )
136
+ colormap = colormaps.get_cmap("Set3_r")
137
+ resampled_cmap = colormap.resampled(num_polygons)
138
+ colors = [resampled_cmap(i) for i in range(resampled_cmap.N)]
139
+
140
+ # Cycle through colors if not enough are provided for the polygons.
141
+ # This is a safeguard, though resampled() should give the correct number.
142
+ if len(colors) < num_polygons:
143
+ color_cycle = cycle(colors)
144
+ else:
145
+ color_cycle = colors # type: ignore
146
+
147
+ for (name, vertices), color in zip(system.polygons.items(), color_cycle):
148
+ tn0, tn1, tn2 = np.array(vertices).T # clay, sand, silt
149
+ patch = ax.fill(
150
+ tn0,
151
+ tn1,
152
+ tn2,
153
+ ec="k",
154
+ fc=color,
155
+ alpha=0.5,
156
+ )
157
+ centroid = calculate_centroid(patch[0].get_xy())
158
+
159
+ label = name.capitalize()
160
+
161
+ ax.text(
162
+ centroid[0],
163
+ centroid[1],
164
+ label,
165
+ ha="center",
166
+ va="center",
167
+ transform=ax.transData,
168
+ )
169
+
170
+ # ticks, grid, etc. (same as you already have)
171
+ ax.taxis.set_major_locator(MultipleLocator(10.0))
172
+ ax.laxis.set_major_locator(MultipleLocator(10.0))
173
+ ax.raxis.set_major_locator(MultipleLocator(10.0))
174
+ ax.taxis.set_minor_locator(AutoMinorLocator(2))
175
+ ax.laxis.set_minor_locator(AutoMinorLocator(2))
176
+ ax.raxis.set_minor_locator(AutoMinorLocator(2))
177
+ ax.grid(which="both", linewidth=0.4, color="gray", alpha=0.6)
178
+
179
+ # Add custom axis labels along edges
180
+
181
+ # Sand (%) – bottom edge, centered, below triangle
182
+ ax.text(
183
+ -5,
184
+ 33,
185
+ 33, # roughly mid bottom edge in (t,l,r) = (clay,sand,silt)
186
+ "Sand [%]",
187
+ ha="center",
188
+ va="top",
189
+ fontsize=12,
190
+ rotation=0,
191
+ transform=ax.transTernaryAxes, # use ternary coordinate classification_system
192
+ )
193
+
194
+ # Clay (%) – left edge, centered, rotated up
195
+ ax.text(
196
+ 40,
197
+ 40,
198
+ -8, # somewhere along left edge
199
+ "Clay [%]",
200
+ ha="center",
201
+ va="center",
202
+ fontsize=12,
203
+ rotation=60, # rotate along left edge
204
+ transform=ax.transTernaryAxes,
205
+ )
206
+
207
+ # Silt (%) – right edge, centered, rotated down
208
+ ax.text(
209
+ 40,
210
+ -8,
211
+ 40, # somewhere along right edge
212
+ "Silt [%]",
213
+ ha="center",
214
+ va="center",
215
+ fontsize=12,
216
+ rotation=-60, # rotate along right edge
217
+ transform=ax.transTernaryAxes,
218
+ )
219
+ ax.taxis.set_ticks_position("tick2")
220
+ ax.laxis.set_ticks_position("tick2")
221
+ ax.raxis.set_ticks_position("tick2")
222
+
223
+
224
+ def _compute_sizes(
225
+ df: pd.DataFrame,
226
+ size_by: Optional[str],
227
+ size_min: Optional[float],
228
+ size_max: Optional[float]
229
+ ) -> Union[float, np.ndarray]:
230
+ """Helper to compute point sizes based on a column."""
231
+ if size_min is None:
232
+ size_min = 20.0 # default fallback
233
+ if size_by is None or size_by not in df.columns:
234
+ return size_min
235
+
236
+ if size_max is None:
237
+ size_max = 100.0 # default fallback
238
+
239
+ vals = df[size_by].to_numpy().astype(float)
240
+ valid = np.isfinite(vals)
241
+ if not valid.any() or np.allclose(vals[valid], vals[valid][0]):
242
+ return np.full_like(vals, (size_min + size_max) / 2.0)
243
+
244
+ vmin = vals[valid].min()
245
+ vmax = vals[valid].max()
246
+ norm = (vals - vmin) / (vmax - vmin)
247
+ return size_min + norm * (size_max - size_min)
@@ -0,0 +1,80 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Dict, Mapping, Any, Optional
3
+ from . import datasets
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class TextureSystem:
8
+ """
9
+ Represents a soil texture classification system.
10
+
11
+ Parameters
12
+ ----------
13
+ name : str
14
+ The unique name of the system (e.g., 'USDA').
15
+ polygons : Mapping[str, Any]
16
+ A mapping of class names to polygon vertices.
17
+ meta : Mapping[str, Any]
18
+ Metadata about the system (description, citation, etc.).
19
+ """
20
+ name: str
21
+ polygons: Mapping[str, Any]
22
+ meta: Mapping[str, Any]
23
+
24
+
25
+ _SYSTEMS: Dict[str, TextureSystem] = {
26
+ "USDA": TextureSystem(
27
+ name="USDA",
28
+ polygons=datasets.USDA_TEXTURE_CLASSES,
29
+ meta={
30
+ "description": "United States Department of Agriculture (USDA) Soil Texture Classification"
31
+ },
32
+ ),
33
+ "HYPRES": TextureSystem(
34
+ name="HYPRES",
35
+ polygons=datasets.HYPRES_TEXTURE_CLASSES,
36
+ meta={
37
+ "description": "The HYdraulic PRoperties of European Soils (HYPRES) is a European framework for classifying soils based on their hydrologic properties"
38
+ },
39
+ ),
40
+ # additional systems can be added here
41
+ }
42
+
43
+
44
+ def get_texture_system(system_name: str) -> TextureSystem:
45
+ """
46
+ Retrieve a TextureSystem by name.
47
+
48
+ Parameters
49
+ ----------
50
+ system_name : str
51
+ The name of the system to retrieve.
52
+
53
+ Returns
54
+ -------
55
+ TextureSystem
56
+ The requested texture system.
57
+
58
+ Raises
59
+ ------
60
+ ValueError
61
+ If the system name is not found.
62
+ """
63
+ try:
64
+ return _SYSTEMS[system_name]
65
+ except KeyError:
66
+ raise ValueError(
67
+ f"Unknown texture system {system_name!r}. Available: {list(_SYSTEMS)}"
68
+ )
69
+
70
+
71
+ def list_texture_systems() -> Dict[str, str]:
72
+ """
73
+ List all available texture systems.
74
+
75
+ Returns
76
+ -------
77
+ Dict[str, str]
78
+ A dictionary mapping system names to their descriptions.
79
+ """
80
+ return {k: v.meta.get("description", "") for k, v in _SYSTEMS.items()}
@@ -0,0 +1,178 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path as PathLibPath
6
+ from typing import Optional, Union, TYPE_CHECKING
7
+ from matplotlib.figure import Figure
8
+ from matplotlib.axes import Axes
9
+
10
+ from .systems import get_texture_system, TextureSystem
11
+ from .classifier import PolygonClassifier
12
+ from . import plotting
13
+
14
+ if TYPE_CHECKING:
15
+ # Avoid circular import at runtime by only importing for type checking if needed
16
+ pass
17
+
18
+
19
+ @dataclass
20
+ class SoilTextureTriangle:
21
+ """
22
+ Main interface for loading soil data, classifying it, and plotting it.
23
+
24
+ Parameters
25
+ ----------
26
+ system_name : str, optional
27
+ Name of the texture classification system. Defaults to 'USDA'.
28
+ df : pd.DataFrame, optional
29
+ Initial DataFrame. Can be set later via load functions.
30
+ """
31
+ system_name: str = "USDA"
32
+ df: Optional[pd.DataFrame] = field(default=None, repr=False)
33
+
34
+ def __post_init__(self):
35
+ self.system: TextureSystem = get_texture_system(self.system_name)
36
+ self._classifier = PolygonClassifier.from_system(self.system)
37
+
38
+ # data loading
39
+ def load_csv(
40
+ self,
41
+ path: Union[str, PathLibPath],
42
+ sand_col: str = "sand",
43
+ silt_col: str = "silt",
44
+ clay_col: str = "clay",
45
+ ) -> "SoilTextureTriangle":
46
+ """
47
+ Load data from a CSV file.
48
+
49
+ Parameters
50
+ ----------
51
+ path : str or Path
52
+ Path to the CSV file.
53
+ sand_col : str
54
+ Name of the column containing sand percentages.
55
+ silt_col : str
56
+ Name of the column containing silt percentages.
57
+ clay_col : str
58
+ Name of the column containing clay percentages.
59
+
60
+ Returns
61
+ -------
62
+ SoilTextureTriangle
63
+ Self for chaining.
64
+ """
65
+ df = pd.read_csv(path)
66
+ return self.load_dataframe(df, sand_col, silt_col, clay_col)
67
+
68
+ def load_dataframe(
69
+ self,
70
+ df: pd.DataFrame,
71
+ sand_col: str = "sand",
72
+ silt_col: str = "silt",
73
+ clay_col: str = "clay",
74
+ ) -> "SoilTextureTriangle":
75
+ """
76
+ Load data from an existing DataFrame.
77
+
78
+ Parameters
79
+ ----------
80
+ df : pd.DataFrame
81
+ The DataFrame containing soil data.
82
+ sand_col : str
83
+ Name of the column containing sand percentages.
84
+ silt_col : str
85
+ Name of the column containing silt percentages.
86
+ clay_col : str
87
+ Name of the column containing clay percentages.
88
+
89
+ Returns
90
+ -------
91
+ SoilTextureTriangle
92
+ Self for chaining.
93
+ """
94
+ # normalize column names internally
95
+ self.df = df.rename(
96
+ columns={sand_col: "sand", silt_col: "silt", clay_col: "clay"}
97
+ )
98
+ return self
99
+
100
+ # classification
101
+ def classify(self) -> pd.DataFrame:
102
+ """
103
+ Classify loaded data into texture classes.
104
+
105
+ Adds a 'texture_class' column to the internal DataFrame.
106
+
107
+ Returns
108
+ -------
109
+ pd.DataFrame
110
+ The DataFrame with the added 'texture_class' column.
111
+
112
+ Raises
113
+ ------
114
+ ValueError
115
+ If no data has been loaded.
116
+ """
117
+ if self.df is None:
118
+ raise ValueError("No data loaded. Call load_csv or load_dataframe first.")
119
+
120
+ clay = self.df["clay"].to_numpy()
121
+ sand = self.df["sand"].to_numpy()
122
+ silt = self.df["silt"].to_numpy()
123
+
124
+ classes = self._classifier.classify_points(clay, sand, silt)
125
+ self.df["texture_class"] = classes
126
+ return self.df
127
+
128
+ # plotting
129
+ def plot(
130
+ self,
131
+ size_by: Optional[str] = None,
132
+ size_min: float = 40,
133
+ size_max: float = 160,
134
+ show_labels: bool = True,
135
+ cmap: Optional[str] = None,
136
+ color_points: Optional[str] = "black",
137
+ ) -> tuple[Figure, Axes]:
138
+ """
139
+ Plot current data on the soil texture triangle using mpltern.
140
+
141
+ Parameters
142
+ ----------
143
+ size_by : str, optional
144
+ Column name for sizing points.
145
+ size_min : float, optional
146
+ Min point size.
147
+ size_max : float, optional
148
+ Max point size.
149
+ show_labels : bool, optional
150
+ Show labels on points.
151
+ cmap : str, optional
152
+ Colormap name for background polygons.
153
+ color_points : str, optional
154
+ Color for sample points.
155
+
156
+ Returns
157
+ -------
158
+ fig, ax
159
+ Matplotlib Figure and Axes.
160
+
161
+ Raises
162
+ ------
163
+ ValueError
164
+ If no data is loaded.
165
+ """
166
+ if self.df is None:
167
+ raise ValueError("No data loaded. Call load_csv or load_dataframe first.")
168
+
169
+ return plotting.plot_triangle_with_points(
170
+ df=self.df,
171
+ system=self.system,
172
+ size_by=size_by,
173
+ size_min=size_min,
174
+ size_max=size_max,
175
+ show_labels=show_labels,
176
+ cmap=cmap,
177
+ color_points=color_points,
178
+ )
@@ -0,0 +1,85 @@
1
+ import numpy as np
2
+
3
+
4
+ def calculate_centroid(vertices: np.ndarray) -> np.ndarray:
5
+ """
6
+ Compute centroid of a 2D polygon given an (N, 2) array of vertices.
7
+ Uses the standard shoelace formula (no np.cross).
8
+
9
+ Parameters
10
+ ----------
11
+ vertices : np.ndarray
12
+ Array of shape (N, 2) containing polygon vertices.
13
+
14
+ Returns
15
+ -------
16
+ np.ndarray
17
+ Array of shape (2,) containing the (x, y) centroid coordinates.
18
+ """
19
+ x = vertices[:, 0]
20
+ y = vertices[:, 1]
21
+
22
+ # Close the polygon
23
+ x_next = np.roll(x, -1)
24
+ y_next = np.roll(y, -1)
25
+
26
+ cross = x * y_next - x_next * y
27
+ area = cross.sum() / 2.0
28
+
29
+ if np.isclose(area, 0.0):
30
+ # Fallback: simple mean if area is ~0 (degenerate polygon)
31
+ return np.array([x.mean(), y.mean()])
32
+
33
+ cx = ((x + x_next) * cross).sum() / (6.0 * area)
34
+ cy = ((y + y_next) * cross).sum() / (6.0 * area)
35
+
36
+ return np.array([cx, cy])
37
+
38
+
39
+ def ternary_to_cartesian(
40
+ clay: np.ndarray,
41
+ sand: np.ndarray,
42
+ silt: np.ndarray,
43
+ ternary_sum: float = 100.0
44
+ ) -> np.ndarray:
45
+ """
46
+ Convert ternary coordinates (clay, sand, silt) to 2D Cartesian (x, y).
47
+
48
+ Uses an equilateral triangle with side length = ternary_sum.
49
+
50
+ Parameters
51
+ ----------
52
+ clay : np.ndarray
53
+ Clay percentages.
54
+ sand : np.ndarray
55
+ Sand percentages.
56
+ silt : np.ndarray
57
+ Silt percentages.
58
+ ternary_sum : float, optional
59
+ The sum of the ternary components (default 100.0).
60
+
61
+ Returns
62
+ -------
63
+ np.ndarray
64
+ Array of shape (N, 2) containing Cartesian (x, y) coordinates.
65
+ """
66
+ clay = np.asarray(clay, dtype=float)
67
+ sand = np.asarray(sand, dtype=float)
68
+ silt = np.asarray(silt, dtype=float)
69
+
70
+ # Normalize to sum to ternary_sum to be safe
71
+ total = clay + sand + silt
72
+ total[total == 0] = ternary_sum
73
+ clay = clay * ternary_sum / total
74
+ sand = sand * ternary_sum / total
75
+ silt = silt * ternary_sum / total
76
+
77
+ # Place triangle with one vertex at (0, 0), base horizontal
78
+ # Many ternary implementations use:
79
+ # x = 0.5 * (2*sand + silt) / ternary_sum
80
+ # y = (np.sqrt(3)/2) * silt / ternary_sum
81
+ # but we’ll keep units ~percent and let plotting handle scaling.
82
+ x = sand + 0.5 * silt
83
+ y = (np.sqrt(3) / 2.0) * silt
84
+
85
+ return np.stack([x, y], axis=-1)
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: soiltextureplot
3
+ Version: 0.1.0
4
+ Summary: Soil texture triangle plotting and classification
5
+ Author-email: Amninder Singh <amnindersingh13@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Amninder Singh
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE OR ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/singhamninder/soiltextureplot
29
+ Project-URL: Bug Tracker, https://github.com/singhamninder/soiltextureplot/issues
30
+ Keywords: soil,texture,triangle,plot,classification,ternary
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Intended Audience :: Science/Research
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Topic :: Scientific/Engineering
40
+ Requires-Python: >=3.9
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Requires-Dist: pandas>=1.5.0
44
+ Requires-Dist: numpy>=1.23.0
45
+ Requires-Dist: matplotlib>=3.7.0
46
+ Requires-Dist: mpltern>=1.0.0
47
+ Dynamic: license-file
48
+
49
+ # Soil Texture Plot
50
+
51
+ [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://soiltextureplot.streamlit.app/)
52
+
53
+ This repository contains codebase for soil texture classification and visualization using ternary diagrams. It provides tools to plot soil texture data on texture triangles and classify soil samples according to different classification systems.
54
+
55
+ ## Features
56
+
57
+ - **Ternary Plotting**: Visualize soil texture data on interactive ternary diagrams
58
+ - **Multiple Classification Systems**: Support for USDA and HYPRES soil texture classification systems
59
+ - **Interactive Web App**: Streamlit-based application for easy data upload and visualization
60
+ - **Flexible Data Input**: Support for CSV files with customizable column mapping
61
+ - **Point Classification**: Automatic classification of soil samples into texture classes
62
+ - **Customizable Visualization**: Control point sizes, colors, and labels
63
+
64
+ ## Dependencies
65
+
66
+ - streamlit
67
+ - pandas
68
+ - numpy
69
+ - matplotlib
70
+ - mpltern
71
+
72
+
73
+ ## Web Application
74
+
75
+ The web app allows you to:
76
+ - Upload CSV files with soil texture data
77
+ - Map columns to sand, silt, and clay percentages
78
+ - Visualize data on interactive texture triangles
79
+ - Customize plot appearance
80
+
81
+ ## Supported Classification Systems
82
+
83
+ ### USDA (United States Department of Agriculture)
84
+ The standard USDA soil texture classification system with 12 texture classes.
85
+
86
+ ### HYPRES (HYdraulic PRoperties of European Soils)
87
+ A European framework for classifying soils based on their hydrologic properties.
88
+
89
+ ## Data Format
90
+
91
+ Your CSV file should contain soil texture data with percentages of sand, silt, and clay. The percentages should sum to 100% for each sample.
92
+
93
+ Example data format:
94
+
95
+ ```csv
96
+ sample_id,sand,silt,clay
97
+ S1,65,20,15
98
+ S2,70,24,6
99
+ S3,75,21,4
100
+ ```
101
+
102
+ ## Acknowledgments
103
+
104
+ - Built using [mpltern](https://github.com/yuzie007/mpltern) for ternary plotting
105
+ - Inspired by soil science classification standards
106
+ - Streamlit for the web interface
@@ -0,0 +1,12 @@
1
+ soiltextureplot/__init__.py,sha256=PXZ9KDE8bVO0seCW21ds3psztkh_Ab2mQdAyGf_YrbQ,774
2
+ soiltextureplot/classifier.py,sha256=sPf33xI5gJHKACTIWaqW9r1MPpX9R8VMRgsXJgzDt8M,2748
3
+ soiltextureplot/datasets.py,sha256=k42L-eMIlG_ehWwxW7Is_JUIWOAEgOgyIzYXRayp3RY,2827
4
+ soiltextureplot/plotting.py,sha256=sH9_2YKz9O0XVkJOhb5izf47yRk7_epaIioF9fOEsXU,7485
5
+ soiltextureplot/systems.py,sha256=sk4YFLjLlRbyAAPsitmBI3hG5mJq3bOhAIcOgUJm9K8,2046
6
+ soiltextureplot/triangle.py,sha256=lAySOgGZFipaTeqedoG4H2NU3d-x6C2VKkWdFeZYFQ8,5056
7
+ soiltextureplot/utils.py,sha256=Yhq3PXPUQt17xkaH1HEO9fvZHeR7E5UzH4VIcNUtaDc,2324
8
+ soiltextureplot-0.1.0.dist-info/licenses/LICENSE,sha256=heva5rpPPnoz6pdO3QinIy2P0LfIHXOoi_CALnus4f8,1073
9
+ soiltextureplot-0.1.0.dist-info/METADATA,sha256=OH2tw4o0u0hWAqI-1tBZIKK9ew6ea10ZnpDSxHKHU2A,4274
10
+ soiltextureplot-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ soiltextureplot-0.1.0.dist-info/top_level.txt,sha256=MyfpMMvqcQcnj28Ns9jfqGvl6zqo4vaycuPFu8oDXv0,16
12
+ soiltextureplot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Amninder Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE OR ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ soiltextureplot