spotmap 0.1.0__tar.gz

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 @@
1
+ .claude/
spotmap-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tharun Mallesan
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, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
spotmap-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: spotmap
3
+ Version: 0.1.0
4
+ Summary: Interactive epidemiological spot maps for India
5
+ Project-URL: Homepage, https://github.com/TharunMallesan/spotmap
6
+ Project-URL: Repository, https://github.com/TharunMallesan/spotmap
7
+ Project-URL: Bug Tracker, https://github.com/TharunMallesan/spotmap/issues
8
+ Author-email: Tharun Mallesan <tharunmallesan@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2025 Tharun Mallesan
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: case-control,epidemiology,folium,geospatial,india,spot map
32
+ Classifier: Intended Audience :: Science/Research
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Topic :: Scientific/Engineering :: GIS
37
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
38
+ Requires-Python: >=3.9
39
+ Requires-Dist: folium>=0.14
40
+ Requires-Dist: geopandas>=0.13
41
+ Requires-Dist: numpy>=1.24
42
+ Requires-Dist: pandas>=2.0
43
+ Requires-Dist: shapely>=2.0
44
+ Description-Content-Type: text/markdown
45
+
46
+ # SpotMap
47
+
48
+ **Interactive epidemiological spot maps for India**
49
+
50
+ SpotMap turns a CSV of case/control coordinates into a publication-ready interactive HTML map with:
51
+
52
+ - **Dot density** clustering (cases only)
53
+ - **Spot map** pins (cases and/or controls) with custom colours
54
+ - Automatic state/district boundary overlays (bundled data — no shapefile setup needed)
55
+ - Sidebar with mode toggle, colour pickers, pin size slider, and PNG/PDF export
56
+ - Smart auto-detection of latitude, longitude, and outcome columns
57
+
58
+ ---
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install spotmap
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Quick start
69
+
70
+ ### Python API
71
+
72
+ ```python
73
+ from spotmap import SpotMap
74
+
75
+ SpotMap("my_data.csv").build().save("map.html")
76
+ ```
77
+
78
+ ### Command line
79
+
80
+ ```bash
81
+ spotmap my_data.csv -o map.html
82
+ ```
83
+
84
+ ---
85
+
86
+ ## CSV format
87
+
88
+ SpotMap auto-detects columns — no strict naming required.
89
+
90
+ | Requirement | Details |
91
+ |---|---|
92
+ | **Coordinates** | Separate `lat` / `lon` columns **or** a combined `"lat,lon"` column |
93
+ | **Outcome** | A column named `outcome`, `status`, `case_control`, etc. with values like `case` / `control` |
94
+
95
+ Example:
96
+
97
+ ```csv
98
+ latitude,longitude,outcome
99
+ 28.6,77.2,case
100
+ 19.0,72.8,control
101
+ 13.0,80.2,case
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Python API reference
107
+
108
+ ```python
109
+ SpotMap(
110
+ csv_path, # required
111
+ state_shp=None, # custom state boundary (shapefile/GeoPackage)
112
+ district_shp=None, # custom district boundary
113
+ lat_col=None, # override auto-detection
114
+ lon_col=None,
115
+ outcome_col=None,
116
+ case_value=None, # value that means "case" in outcome_col
117
+ count_cutoff=2, # districts ≤ cutoff → district zoom
118
+ margin_deg=1.0, # boundary crop padding
119
+ cluster_color="#E85252", # dot-density bubble colour
120
+ case_color="#D55757", # case pin colour
121
+ control_color="#7676E7", # control pin colour
122
+ )
123
+ ```
124
+
125
+ Chain calls:
126
+
127
+ ```python
128
+ sm = SpotMap("data.csv", case_color="#FF0000").build()
129
+ sm.save("map.html")
130
+
131
+ # Access the raw Folium map for further customisation
132
+ folium_map = sm.map
133
+ ```
134
+
135
+ ---
136
+
137
+ ## CLI reference
138
+
139
+ ```
140
+ usage: spotmap [-h] [-o OUTPUT] [--state-shp STATE_SHP]
141
+ [--district-shp DISTRICT_SHP] [--lat-col LAT_COL]
142
+ [--lon-col LON_COL] [--outcome-col OUTCOME_COL]
143
+ [--case-value CASE_VALUE] [--count-cutoff COUNT_CUTOFF]
144
+ [--cluster-color CLUSTER_COLOR] [--case-color CASE_COLOR]
145
+ [--control-color CONTROL_COLOR]
146
+ csv
147
+ ```
148
+
149
+ ---
150
+
151
+ ## License
152
+
153
+ MIT © Tharun Mallesan
@@ -0,0 +1,108 @@
1
+ # SpotMap
2
+
3
+ **Interactive epidemiological spot maps for India**
4
+
5
+ SpotMap turns a CSV of case/control coordinates into a publication-ready interactive HTML map with:
6
+
7
+ - **Dot density** clustering (cases only)
8
+ - **Spot map** pins (cases and/or controls) with custom colours
9
+ - Automatic state/district boundary overlays (bundled data — no shapefile setup needed)
10
+ - Sidebar with mode toggle, colour pickers, pin size slider, and PNG/PDF export
11
+ - Smart auto-detection of latitude, longitude, and outcome columns
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install spotmap
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Quick start
24
+
25
+ ### Python API
26
+
27
+ ```python
28
+ from spotmap import SpotMap
29
+
30
+ SpotMap("my_data.csv").build().save("map.html")
31
+ ```
32
+
33
+ ### Command line
34
+
35
+ ```bash
36
+ spotmap my_data.csv -o map.html
37
+ ```
38
+
39
+ ---
40
+
41
+ ## CSV format
42
+
43
+ SpotMap auto-detects columns — no strict naming required.
44
+
45
+ | Requirement | Details |
46
+ |---|---|
47
+ | **Coordinates** | Separate `lat` / `lon` columns **or** a combined `"lat,lon"` column |
48
+ | **Outcome** | A column named `outcome`, `status`, `case_control`, etc. with values like `case` / `control` |
49
+
50
+ Example:
51
+
52
+ ```csv
53
+ latitude,longitude,outcome
54
+ 28.6,77.2,case
55
+ 19.0,72.8,control
56
+ 13.0,80.2,case
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Python API reference
62
+
63
+ ```python
64
+ SpotMap(
65
+ csv_path, # required
66
+ state_shp=None, # custom state boundary (shapefile/GeoPackage)
67
+ district_shp=None, # custom district boundary
68
+ lat_col=None, # override auto-detection
69
+ lon_col=None,
70
+ outcome_col=None,
71
+ case_value=None, # value that means "case" in outcome_col
72
+ count_cutoff=2, # districts ≤ cutoff → district zoom
73
+ margin_deg=1.0, # boundary crop padding
74
+ cluster_color="#E85252", # dot-density bubble colour
75
+ case_color="#D55757", # case pin colour
76
+ control_color="#7676E7", # control pin colour
77
+ )
78
+ ```
79
+
80
+ Chain calls:
81
+
82
+ ```python
83
+ sm = SpotMap("data.csv", case_color="#FF0000").build()
84
+ sm.save("map.html")
85
+
86
+ # Access the raw Folium map for further customisation
87
+ folium_map = sm.map
88
+ ```
89
+
90
+ ---
91
+
92
+ ## CLI reference
93
+
94
+ ```
95
+ usage: spotmap [-h] [-o OUTPUT] [--state-shp STATE_SHP]
96
+ [--district-shp DISTRICT_SHP] [--lat-col LAT_COL]
97
+ [--lon-col LON_COL] [--outcome-col OUTCOME_COL]
98
+ [--case-value CASE_VALUE] [--count-cutoff COUNT_CUTOFF]
99
+ [--cluster-color CLUSTER_COLOR] [--case-color CASE_COLOR]
100
+ [--control-color CONTROL_COLOR]
101
+ csv
102
+ ```
103
+
104
+ ---
105
+
106
+ ## License
107
+
108
+ MIT © Tharun Mallesan
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "spotmap"
7
+ version = "0.1.0"
8
+ description = "Interactive epidemiological spot maps for India"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Tharun Mallesan", email = "tharunmallesan@gmail.com" }
14
+ ]
15
+ keywords = ["epidemiology", "spot map", "india", "geospatial", "folium", "case-control"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Scientific/Engineering :: GIS",
21
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
22
+ "Intended Audience :: Science/Research",
23
+ ]
24
+ dependencies = [
25
+ "folium>=0.14",
26
+ "geopandas>=0.13",
27
+ "numpy>=1.24",
28
+ "pandas>=2.0",
29
+ "shapely>=2.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/TharunMallesan/spotmap"
34
+ Repository = "https://github.com/TharunMallesan/spotmap"
35
+ "Bug Tracker" = "https://github.com/TharunMallesan/spotmap/issues"
36
+
37
+ [project.scripts]
38
+ spotmap = "spotmap.cli:main"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["spotmap"]
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ include = ["spotmap/", "README.md", "LICENSE", "pyproject.toml"]
@@ -0,0 +1,7 @@
1
+ """spotmap — Interactive epidemiological spot maps for India."""
2
+
3
+ from .exceptions import ColumnNotFoundError, NoCasePointsError, SpotMapError
4
+ from .map_builder import SpotMap
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["SpotMap", "SpotMapError", "ColumnNotFoundError", "NoCasePointsError"]
@@ -0,0 +1,75 @@
1
+ """Command-line interface for spotmap."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+
7
+ def _build_parser() -> argparse.ArgumentParser:
8
+ p = argparse.ArgumentParser(
9
+ prog="spotmap",
10
+ description="Generate an interactive epidemiological spot map for India.",
11
+ )
12
+ p.add_argument("csv", help="Path to the input CSV file with point data.")
13
+ p.add_argument(
14
+ "-o", "--output",
15
+ default="spotmap_output.html",
16
+ help="Output HTML file path (default: spotmap_output.html).",
17
+ )
18
+ p.add_argument("--state-shp", default=None, help="Custom state boundary file.")
19
+ p.add_argument("--district-shp", default=None, help="Custom district boundary file.")
20
+ p.add_argument("--lat-col", default=None, help="Latitude column name.")
21
+ p.add_argument("--lon-col", default=None, help="Longitude column name.")
22
+ p.add_argument("--outcome-col", default=None, help="Outcome column name.")
23
+ p.add_argument("--case-value", default=None, help="Value that represents a case.")
24
+ p.add_argument(
25
+ "--count-cutoff",
26
+ type=int,
27
+ default=2,
28
+ help="District count threshold for mode selection (default: 2).",
29
+ )
30
+ p.add_argument(
31
+ "--cluster-color",
32
+ default="#E85252",
33
+ help="Hex colour for dot-density clusters (default: #E85252).",
34
+ )
35
+ p.add_argument(
36
+ "--case-color",
37
+ default="#D55757",
38
+ help="Hex colour for case pins (default: #D55757).",
39
+ )
40
+ p.add_argument(
41
+ "--control-color",
42
+ default="#7676E7",
43
+ help="Hex colour for control pins (default: #7676E7).",
44
+ )
45
+ return p
46
+
47
+
48
+ def main(argv=None) -> None:
49
+ from .map_builder import SpotMap
50
+
51
+ parser = _build_parser()
52
+ args = parser.parse_args(argv)
53
+
54
+ try:
55
+ SpotMap(
56
+ args.csv,
57
+ state_shp=args.state_shp,
58
+ district_shp=args.district_shp,
59
+ lat_col=args.lat_col,
60
+ lon_col=args.lon_col,
61
+ outcome_col=args.outcome_col,
62
+ case_value=args.case_value,
63
+ count_cutoff=args.count_cutoff,
64
+ cluster_color=args.cluster_color,
65
+ case_color=args.case_color,
66
+ control_color=args.control_color,
67
+ ).build().save(args.output)
68
+ print(f"Map saved to: {args.output}")
69
+ except Exception as exc:
70
+ print(f"Error: {exc}", file=sys.stderr)
71
+ sys.exit(1)
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ class SpotMapError(Exception):
2
+ pass
3
+
4
+
5
+ class ColumnNotFoundError(SpotMapError):
6
+ pass
7
+
8
+
9
+ class NoCasePointsError(SpotMapError):
10
+ pass
@@ -0,0 +1,153 @@
1
+ """Folium layer builders for dot-density clusters and spot-map pins."""
2
+
3
+ import folium
4
+ import geopandas as gpd
5
+ from folium.plugins import MarkerCluster
6
+
7
+
8
+ def _cluster_icon_fn(cluster_color: str, total_cases: int) -> str:
9
+ return f"""
10
+ function(cluster) {{
11
+ var count = cluster.getChildCount();
12
+ var total = {total_cases};
13
+
14
+ var frac = count / total;
15
+ if (!isFinite(frac) || frac < 0) frac = 0;
16
+ if (frac > 1) frac = 1;
17
+
18
+ var baseHex = window.clusterBaseColor || '{cluster_color}';
19
+
20
+ function hexToRgb(hex) {{
21
+ if (!hex) return {{r: 255, g: 0, b: 0}};
22
+ var c = hex.replace('#','');
23
+ if (c.length === 3) c = c[0]+c[0]+c[1]+c[1]+c[2]+c[2];
24
+ var num = parseInt(c, 16);
25
+ return {{r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255}};
26
+ }}
27
+
28
+ function mixWithWhite(rgb, t) {{
29
+ t = Math.max(0, Math.min(1, t));
30
+ return {{
31
+ r: Math.round(255*(1-t) + rgb.r*t),
32
+ g: Math.round(255*(1-t) + rgb.g*t),
33
+ b: Math.round(255*(1-t) + rgb.b*t)
34
+ }};
35
+ }}
36
+
37
+ var baseRgb = hexToRgb(baseHex);
38
+ var t = 0.4 + 0.6 * Math.sqrt(frac);
39
+ if (!isFinite(t) || t < 0.4) t = 0.4;
40
+ if (t > 1) t = 1;
41
+
42
+ var mixed = mixWithWhite(baseRgb, t);
43
+ var color = 'rgba('+mixed.r+','+mixed.g+','+mixed.b+',0.95)';
44
+ var size = 30 + Math.min(25, Math.sqrt(count) * 3);
45
+
46
+ return new L.DivIcon({{
47
+ html: '<div style="background:'+color+
48
+ '; width:'+size+'px; height:'+size+
49
+ 'px; border-radius:50%; display:flex; align-items:center; justify-content:center; '+
50
+ 'box-shadow:0 0 6px rgba(0,0,0,0.4); font-weight:bold;">'+count+'</div>',
51
+ className: 'cluster-icon',
52
+ iconSize: new L.Point(size, size)
53
+ }});
54
+ }}
55
+ """
56
+
57
+
58
+ def add_boundary_layers(
59
+ m: folium.Map,
60
+ india_outline: gpd.GeoDataFrame,
61
+ affected_states: gpd.GeoDataFrame,
62
+ affected_districts: gpd.GeoDataFrame,
63
+ ) -> None:
64
+ folium.GeoJson(
65
+ india_outline,
66
+ name="India Border",
67
+ control=False,
68
+ style_function=lambda x: {
69
+ "fillOpacity": 0.0,
70
+ "color": "#000000",
71
+ "weight": 1,
72
+ "opacity": 0.5,
73
+ },
74
+ ).add_to(m)
75
+
76
+ folium.GeoJson(
77
+ affected_states,
78
+ name="Affected States",
79
+ control=False,
80
+ style_function=lambda x: {
81
+ "fillOpacity": 0.05,
82
+ "color": "#4B0082",
83
+ "weight": 1.5,
84
+ "opacity": 0.7,
85
+ },
86
+ ).add_to(m)
87
+
88
+ folium.GeoJson(
89
+ affected_districts,
90
+ name="Affected Districts",
91
+ control=False,
92
+ style_function=lambda x: {
93
+ "color": "#000000",
94
+ "weight": 1,
95
+ "fillOpacity": 0.01,
96
+ "opacity": 1.0,
97
+ },
98
+ ).add_to(m)
99
+
100
+
101
+ def add_marker_layers(
102
+ m: folium.Map,
103
+ points_cases: gpd.GeoDataFrame,
104
+ points_controls: gpd.GeoDataFrame,
105
+ state_name_col: str,
106
+ district_name_col: str,
107
+ cluster_color: str,
108
+ case_color: str,
109
+ control_color: str,
110
+ ):
111
+ """Add dot-density cluster and spot-map pin layers.
112
+
113
+ Returns:
114
+ (cluster_layer, pins_cases_layer, pins_controls_layer)
115
+ """
116
+ total_cases = len(points_cases) or 1
117
+
118
+ cluster = MarkerCluster(
119
+ name="Dot Density Layer",
120
+ icon_create_function=_cluster_icon_fn(cluster_color, total_cases),
121
+ disableClusteringAtZoom=15,
122
+ spiderfyOnMaxZoom=True,
123
+ showCoverageOnHover=False,
124
+ maxClusterRadius=60,
125
+ singleMarkerMode=True,
126
+ )
127
+ m.add_child(cluster)
128
+
129
+ pins_cases_layer = folium.FeatureGroup(name="Spot Map - Cases")
130
+ pins_controls_layer = folium.FeatureGroup(name="Spot Map - Controls")
131
+ m.add_child(pins_cases_layer)
132
+ m.add_child(pins_controls_layer)
133
+
134
+ for _, row in points_cases.iterrows():
135
+ lat, lon = row.geometry.y, row.geometry.x
136
+ popup = (
137
+ f"<b>Type:</b> Case<br>"
138
+ f"<b>State:</b> {row.get(state_name_col, '')}<br>"
139
+ f"<b>District:</b> {row.get(district_name_col, '')}"
140
+ )
141
+ folium.Marker(location=[lat, lon], popup=popup).add_to(cluster)
142
+ folium.Marker(location=[lat, lon], popup=popup).add_to(pins_cases_layer)
143
+
144
+ for _, row in points_controls.iterrows():
145
+ lat, lon = row.geometry.y, row.geometry.x
146
+ popup = (
147
+ f"<b>Type:</b> Control<br>"
148
+ f"<b>State:</b> {row.get(state_name_col, '')}<br>"
149
+ f"<b>District:</b> {row.get(district_name_col, '')}"
150
+ )
151
+ folium.Marker(location=[lat, lon], popup=popup).add_to(pins_controls_layer)
152
+
153
+ return cluster, pins_cases_layer, pins_controls_layer