ipyd3geo 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.
- ipyd3geo-0.1.0/.gitignore +7 -0
- ipyd3geo-0.1.0/LICENSE +29 -0
- ipyd3geo-0.1.0/PKG-INFO +61 -0
- ipyd3geo-0.1.0/README.md +41 -0
- ipyd3geo-0.1.0/pyproject.toml +54 -0
- ipyd3geo-0.1.0/src/ipyd3geo/__init__.py +4 -0
- ipyd3geo-0.1.0/src/ipyd3geo/py.typed +0 -0
- ipyd3geo-0.1.0/src/ipyd3geo/static/widget.js +156 -0
- ipyd3geo-0.1.0/src/ipyd3geo/widget.py +192 -0
ipyd3geo-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, ipyd3geo contributors
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
ipyd3geo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ipyd3geo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An interactive map widget for Jupyter that uses D3 Geo as its renderer.
|
|
5
|
+
Project-URL: Repository, https://github.com/geojupyter/prototype-ipyd3geo
|
|
6
|
+
Project-URL: Issues, https://github.com/geojupyter/prototype-ipyd3geo/issues
|
|
7
|
+
Author-email: Benjamin Szeghy <benjamin.szeghy@berkeley.edu>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Framework :: Jupyter
|
|
12
|
+
Classifier: Framework :: Jupyter :: JupyterLab
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: anywidget>=0.11.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# ipyd3geo
|
|
22
|
+
|
|
23
|
+
A Jupyter widget for D3-based geospatial visualization.
|
|
24
|
+
|
|
25
|
+
It is designed to work similarly to other map renderer widgets, but uses D3-Geo as its renderer.
|
|
26
|
+
|
|
27
|
+
Using D3-Geo over other map renderers has a number of key benefits:
|
|
28
|
+
- Renders directly to an SVG. This package acts as a bridge between Jupyter-based geospatial tools, and cartography in design software.
|
|
29
|
+
- Maps can be arbitrarily styled with CSS or D3
|
|
30
|
+
- Native vector output means no pixelation, so rendered maps are infinitely scalable.
|
|
31
|
+
- Native best in class projection support. There are few if any conventionally defined projections that D3-Geo does not support (including certain very obscure ones)
|
|
32
|
+
|
|
33
|
+
However, there are a few tradeoffs:
|
|
34
|
+
- Due to its lack of virtualization, it is far less performant with large datasets than alternatives.
|
|
35
|
+
- It is intentionally client-side only, so it does not have any tiling server functionality
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
</br></br>
|
|
39
|
+
> [!WARNING]
|
|
40
|
+
> For now this project is experimental. We cannot guarantee further development or ongoing maintenance.
|
|
41
|
+
>
|
|
42
|
+
> If you or your organization are interested in supporting this project's development, either financially or in kind, please email Benny@BennySzeghy.com
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pip install ipyd3geo
|
|
48
|
+
```
|
|
49
|
+
or
|
|
50
|
+
```sh
|
|
51
|
+
uv add ipyd3geo
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
uv sync
|
|
58
|
+
pnpm install ## No build step is actually needed yet. This will be added in the future
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [examples/example.ipynb](examples/example.ipynb) for usage.
|
ipyd3geo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ipyd3geo
|
|
2
|
+
|
|
3
|
+
A Jupyter widget for D3-based geospatial visualization.
|
|
4
|
+
|
|
5
|
+
It is designed to work similarly to other map renderer widgets, but uses D3-Geo as its renderer.
|
|
6
|
+
|
|
7
|
+
Using D3-Geo over other map renderers has a number of key benefits:
|
|
8
|
+
- Renders directly to an SVG. This package acts as a bridge between Jupyter-based geospatial tools, and cartography in design software.
|
|
9
|
+
- Maps can be arbitrarily styled with CSS or D3
|
|
10
|
+
- Native vector output means no pixelation, so rendered maps are infinitely scalable.
|
|
11
|
+
- Native best in class projection support. There are few if any conventionally defined projections that D3-Geo does not support (including certain very obscure ones)
|
|
12
|
+
|
|
13
|
+
However, there are a few tradeoffs:
|
|
14
|
+
- Due to its lack of virtualization, it is far less performant with large datasets than alternatives.
|
|
15
|
+
- It is intentionally client-side only, so it does not have any tiling server functionality
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
</br></br>
|
|
19
|
+
> [!WARNING]
|
|
20
|
+
> For now this project is experimental. We cannot guarantee further development or ongoing maintenance.
|
|
21
|
+
>
|
|
22
|
+
> If you or your organization are interested in supporting this project's development, either financially or in kind, please email Benny@BennySzeghy.com
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install ipyd3geo
|
|
28
|
+
```
|
|
29
|
+
or
|
|
30
|
+
```sh
|
|
31
|
+
uv add ipyd3geo
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Development
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
uv sync
|
|
38
|
+
pnpm install ## No build step is actually needed yet. This will be added in the future
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
See [examples/example.ipynb](examples/example.ipynb) for usage.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ipyd3geo"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "An interactive map widget for Jupyter that uses D3 Geo as its renderer."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "BSD-3-Clause"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Benjamin Szeghy", email = "benjamin.szeghy@berkeley.edu" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Framework :: Jupyter",
|
|
14
|
+
"Framework :: Jupyter :: JupyterLab",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"anywidget>=0.11.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/geojupyter/prototype-ipyd3geo"
|
|
26
|
+
Issues = "https://github.com/geojupyter/prototype-ipyd3geo/issues"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.sdist]
|
|
33
|
+
include = [
|
|
34
|
+
"src/ipyd3geo",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md",
|
|
37
|
+
"pyproject.toml",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"ipykernel>=7.3.0",
|
|
43
|
+
"jupyterlab>=4.6.4",
|
|
44
|
+
"pytest>=9.1.1",
|
|
45
|
+
]
|
|
46
|
+
docs = [
|
|
47
|
+
"myst-parser>=5.1.0",
|
|
48
|
+
"pydata-sphinx-theme>=0.21.0",
|
|
49
|
+
"sphinx>=9.1.0",
|
|
50
|
+
"sphinx-autodoc-typehints>=3.13.7",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.uv.dependency-groups]
|
|
54
|
+
docs = { requires-python = ">=3.12" }
|
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as d3 from "https://esm.sh/d3-geo@3.1.1";
|
|
2
|
+
import { zoom } from "https://esm.sh/d3-zoom@3.0.0";
|
|
3
|
+
import { select } from "https://esm.sh/d3-selection@3.0.0";
|
|
4
|
+
|
|
5
|
+
async function render({ model, el }) {
|
|
6
|
+
|
|
7
|
+
if (model.get("hamburger")) {
|
|
8
|
+
const menuButton = document.createElement("button");
|
|
9
|
+
menuButton.textContent = "☰";
|
|
10
|
+
menuButton.style.display = "block";
|
|
11
|
+
el.appendChild(menuButton);
|
|
12
|
+
const menuContent = document.createElement("div");
|
|
13
|
+
menuContent.style.display = "none";
|
|
14
|
+
menuContent.style.position = "absolute";
|
|
15
|
+
menuContent.style.color = "black";
|
|
16
|
+
menuContent.style.cursor = "pointer";
|
|
17
|
+
menuContent.style.backgroundColor = "lightgray";
|
|
18
|
+
menuContent.style.padding = "5px";
|
|
19
|
+
menuContent.style.border = "1px solid black";
|
|
20
|
+
|
|
21
|
+
const exportSvgButton = document.createElement("div");
|
|
22
|
+
exportSvgButton.textContent = "Export SVG";
|
|
23
|
+
exportSvgButton.addEventListener('click', () => {
|
|
24
|
+
const svgString = new XMLSerializer().serializeToString(svg);
|
|
25
|
+
const blob = new Blob([svgString], { type: "image/svg+xml" });
|
|
26
|
+
const url = URL.createObjectURL(blob);
|
|
27
|
+
const a = document.createElement("a");
|
|
28
|
+
a.href = url;
|
|
29
|
+
a.download = "map.svg";
|
|
30
|
+
a.click();
|
|
31
|
+
URL.revokeObjectURL(url);
|
|
32
|
+
});
|
|
33
|
+
menuContent.appendChild(exportSvgButton);
|
|
34
|
+
|
|
35
|
+
//TODO this is the wrong approach to toggling graticule visibility, bc it doesn't toggle on if it didn't start visible.
|
|
36
|
+
const graticuleButton = document.createElement("div");
|
|
37
|
+
graticuleButton.textContent = `Toggle Graticule${model.get("graticule") ? " ✓" : ""}`;
|
|
38
|
+
graticuleButton.addEventListener('click', () => {
|
|
39
|
+
if (graticule) {
|
|
40
|
+
const isShown = graticule.style.display === 'none';
|
|
41
|
+
graticule.style.display = isShown ? 'block' : 'none';
|
|
42
|
+
model.set("graticule", isShown);
|
|
43
|
+
model.save_changes();
|
|
44
|
+
graticuleButton.textContent = `Toggle Graticule${isShown ? ' ✓' : ''}`;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
menuContent.appendChild(graticuleButton);
|
|
48
|
+
|
|
49
|
+
el.appendChild(menuContent);
|
|
50
|
+
|
|
51
|
+
menuButton.addEventListener('click', () => {
|
|
52
|
+
menuContent.style.display = menuContent.style.display === 'none' ? 'block' : 'none';
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const countries = await fetch(
|
|
57
|
+
"https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector/geojson/ne_110m_admin_0_countries.geojson"
|
|
58
|
+
).then((res) => res.json());
|
|
59
|
+
|
|
60
|
+
const projection = d3[model.get("projection")]();
|
|
61
|
+
projection.rotate(model.get("projection_rotate"));
|
|
62
|
+
const center = model.get("projection_center");
|
|
63
|
+
if (center) projection.center(center);
|
|
64
|
+
const precision = model.get("projection_precision");
|
|
65
|
+
if (precision != null) projection.precision(precision);
|
|
66
|
+
const parallels = model.get("projection_parallels");
|
|
67
|
+
if (parallels) projection.parallels(parallels);
|
|
68
|
+
const path = d3.geoPath(projection);
|
|
69
|
+
const baseScale = projection.scale();
|
|
70
|
+
const baseTranslate = projection.translate();
|
|
71
|
+
const basemap = model.get("basemap");
|
|
72
|
+
|
|
73
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
74
|
+
svg.setAttribute("viewBox", "0 0 960 500");
|
|
75
|
+
svg.setAttribute("width", "960");
|
|
76
|
+
svg.setAttribute("height", "500");
|
|
77
|
+
const sphere = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
78
|
+
sphere.setAttribute("fill", "none");
|
|
79
|
+
sphere.setAttribute("stroke", model.get("border") ? "black" : "none");
|
|
80
|
+
svg.appendChild(sphere);
|
|
81
|
+
|
|
82
|
+
let naturalEarth = null;
|
|
83
|
+
if (basemap === "naturalearth") {
|
|
84
|
+
naturalEarth = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
85
|
+
naturalEarth.setAttribute("fill", "none");
|
|
86
|
+
naturalEarth.setAttribute("stroke", "black");
|
|
87
|
+
svg.appendChild(naturalEarth);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const layerGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
91
|
+
svg.appendChild(layerGroup);
|
|
92
|
+
function drawLayers() {
|
|
93
|
+
layerGroup.replaceChildren();
|
|
94
|
+
for (const layerData of model.get("layers")) {
|
|
95
|
+
for (const feature of layerData.features) {
|
|
96
|
+
const style = feature.properties.__ipyd3geo_style__ || {};
|
|
97
|
+
const layer = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
98
|
+
layer.setAttribute("d", path(feature));
|
|
99
|
+
layer.setAttribute("fill", style.fill_color || "none");
|
|
100
|
+
layer.setAttribute("stroke", style.stroke_color || "#000000");
|
|
101
|
+
layer.setAttribute("stroke-width", style.weight || 3);
|
|
102
|
+
layer.setAttribute("stroke-opacity", style.stroke_opacity || 1.0);
|
|
103
|
+
layer.setAttribute("fill-opacity", style.fill_opacity || 0.2);
|
|
104
|
+
layerGroup.appendChild(layer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
model.on("change:layers", drawLayers);
|
|
109
|
+
|
|
110
|
+
let graticule = null;
|
|
111
|
+
if (model.get("graticule")) {
|
|
112
|
+
graticule = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
113
|
+
graticule.setAttribute("fill", "none");
|
|
114
|
+
graticule.setAttribute("stroke", "lightgray");
|
|
115
|
+
graticule.setAttribute("opacity", "0.5");
|
|
116
|
+
svg.appendChild(graticule);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function redraw() {
|
|
120
|
+
sphere.setAttribute("d", path({ type: "Sphere" }));
|
|
121
|
+
if (naturalEarth) naturalEarth.setAttribute("d", path(countries));
|
|
122
|
+
if (graticule) graticule.setAttribute("d", path(d3.geoGraticule()()));
|
|
123
|
+
drawLayers();
|
|
124
|
+
}
|
|
125
|
+
redraw();
|
|
126
|
+
|
|
127
|
+
const dynamic = model.get("dynamic");
|
|
128
|
+
if (dynamic !== "none") {
|
|
129
|
+
let prev = { x: 0, y: 0 };
|
|
130
|
+
const zoomBehavior = zoom().on("zoom", (event) => {
|
|
131
|
+
const t = event.transform;
|
|
132
|
+
projection.scale(baseScale * t.k);
|
|
133
|
+
const isDrag = /^(mouse|touch)move$/.test(event.sourceEvent?.type);
|
|
134
|
+
if (dynamic === "rotate") {
|
|
135
|
+
if (isDrag) {
|
|
136
|
+
const [lambda, phi, gamma] = projection.rotate();
|
|
137
|
+
const degPerPixel = 180 / (Math.PI * projection.scale());
|
|
138
|
+
projection.rotate([
|
|
139
|
+
lambda + (t.x - prev.x) * degPerPixel,
|
|
140
|
+
phi - (t.y - prev.y) * degPerPixel,
|
|
141
|
+
gamma,
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
projection.translate([t.x + baseTranslate[0] * t.k, t.y + baseTranslate[1] * t.k]);
|
|
146
|
+
}
|
|
147
|
+
prev = { x: t.x, y: t.y };
|
|
148
|
+
redraw();
|
|
149
|
+
});
|
|
150
|
+
select(svg).call(zoomBehavior);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
el.appendChild(svg);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default { render };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
import anywidget
|
|
5
|
+
import traitlets
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
class Map(anywidget.AnyWidget):
|
|
10
|
+
"""Defines a D3-Geo backed map widget for Jupyter notebooks.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
center (tuple): center of the map (longitude, latitude). **Not implemented yet.**
|
|
14
|
+
zoom (float): The zoom level of the map. **Not implemented yet.**
|
|
15
|
+
projection (Projection or str): The projection to use for the map. If a string is provided, it will be used to create a Projection object. Defaults to "geoEqualEarth".
|
|
16
|
+
graticule (bool): Whether to show the graticule.
|
|
17
|
+
border (bool): Whether to draw a border frame around the map.
|
|
18
|
+
basemap (str): The basemap to use. **Not implemented yet. Currently defaults to Natural Earth small scale borders.**
|
|
19
|
+
hamburger (bool): Whether to show the hamburger menu. **Not implemented yet.**
|
|
20
|
+
dynamic (str or bool): Whether the map is dynamically draggable. Value can be True, False, "pan", or "rotate". If True, the map will be draggable, and choose a default mode. If False, the map will not be draggable. If "pan", the map will pan post-projection. If "rotate", the map will be rotatable pre-projection.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_esm = pathlib.Path(__file__).parent / "static" / "widget.js"
|
|
24
|
+
|
|
25
|
+
projection = traitlets.Unicode("geoEqualEarth").tag(sync=True)
|
|
26
|
+
border = traitlets.Bool(True).tag(sync=True)
|
|
27
|
+
graticule = traitlets.Bool(False).tag(sync=True)
|
|
28
|
+
projection_rotate = traitlets.Tuple(traitlets.Float(), traitlets.Float(), traitlets.Float(), default_value=(0, 0, 0)).tag(sync=True)
|
|
29
|
+
projection_center = traitlets.Tuple(traitlets.Float(), traitlets.Float(), allow_none=True, default_value=None).tag(sync=True)
|
|
30
|
+
projection_precision = traitlets.Float(allow_none=True, default_value=None).tag(sync=True)
|
|
31
|
+
projection_parallels = traitlets.Tuple(traitlets.Float(), traitlets.Float(), allow_none=True, default_value=None).tag(sync=True)
|
|
32
|
+
layers = traitlets.List().tag(sync=True)
|
|
33
|
+
dynamic = traitlets.Unicode("pan").tag(sync=True)
|
|
34
|
+
basemap = traitlets.Unicode("").tag(sync=True)
|
|
35
|
+
hamburger = traitlets.Bool(True).tag(sync=True)
|
|
36
|
+
|
|
37
|
+
class Export:
|
|
38
|
+
def __init__(self, map):
|
|
39
|
+
self._map = map
|
|
40
|
+
|
|
41
|
+
def svg(self, path):
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
def __init__(self, center=(0.0, 0.0), zoom=1.0, projection=None, graticule=False, border=True, basemap=False, hamburger=True, dynamic=True):
|
|
45
|
+
if center != (0.0, 0.0):
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
if zoom != 1.0:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
if projection is None:
|
|
50
|
+
projection = Projection()
|
|
51
|
+
elif isinstance(projection, str):
|
|
52
|
+
projection = Projection(projection)
|
|
53
|
+
super().__init__()
|
|
54
|
+
self.projection = projection.name
|
|
55
|
+
self.projection_rotate = projection.rotate
|
|
56
|
+
self.projection_center = projection.center
|
|
57
|
+
self.projection_precision = projection.precision
|
|
58
|
+
self.projection_parallels = projection.parallels
|
|
59
|
+
self.border = border
|
|
60
|
+
self.graticule = graticule
|
|
61
|
+
if basemap == "naturalearth" or basemap == True:
|
|
62
|
+
self.basemap = "naturalearth"
|
|
63
|
+
elif basemap == False:
|
|
64
|
+
self.basemap = ""
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError(f"basemap must be 'naturalearth', True, or False")
|
|
67
|
+
self.hamburger = hamburger
|
|
68
|
+
if dynamic is True or dynamic is False:
|
|
69
|
+
self.dynamic = "pan" if dynamic else "none" #TODO: figure out how to make it decide mode based on whether or not the projection is globe-like
|
|
70
|
+
elif dynamic == "pan":
|
|
71
|
+
self.dynamic = "pan"
|
|
72
|
+
elif dynamic == "rotate":
|
|
73
|
+
self.dynamic = "rotate"
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(f"dynamic must be True, False, 'pan', or 'rotate'")
|
|
76
|
+
self.export = Map.Export(self)
|
|
77
|
+
|
|
78
|
+
def add(self, layer):
|
|
79
|
+
"""Add a layer to the map. Only GeoJSON layers are supported so far."""
|
|
80
|
+
self.layers = [*self.layers, layer.data]
|
|
81
|
+
|
|
82
|
+
def remove(self, layer):
|
|
83
|
+
"""Remove a layer from the map. **Not implemented yet.**"""
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
Map.__signature__ = inspect.Signature(list(inspect.signature(Map.__init__).parameters.values())[1:])
|
|
87
|
+
|
|
88
|
+
class Projection:
|
|
89
|
+
"""Defines a D3-Geo projection for the map widget.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
name (str): The name of the projection. Defaults to "geoEqualEarth". See `d3-geo geoProjection docs <https://d3js.org/d3-geo/projection#geoProjection>`_ for options.
|
|
93
|
+
rotate (tuple): The rotation of the projection.
|
|
94
|
+
center (tuple): The center of the projection.
|
|
95
|
+
precision (float): The precision of the projection.
|
|
96
|
+
parallels (tuple): The parallels of the projection. Only applicable for conic projections.
|
|
97
|
+
"""
|
|
98
|
+
def __init__(self, name="geoEqualEarth", rotate=(0, 0, 0), center=None, precision=None, parallels=None):
|
|
99
|
+
self.name = name
|
|
100
|
+
self.rotate = tuple(rotate)
|
|
101
|
+
self.center = center
|
|
102
|
+
self.precision = precision
|
|
103
|
+
self.parallels = parallels
|
|
104
|
+
|
|
105
|
+
class VectorStyle:
|
|
106
|
+
|
|
107
|
+
def __init__(self, stroke_color="#000000", fill_color="#ffffff", weight=3, stroke_opacity=1.0, fill_opacity=1.0):
|
|
108
|
+
self.stroke_color = stroke_color
|
|
109
|
+
self.fill_color = fill_color
|
|
110
|
+
self.weight = weight
|
|
111
|
+
self.stroke_opacity = stroke_opacity
|
|
112
|
+
self.fill_opacity = fill_opacity
|
|
113
|
+
|
|
114
|
+
def resolve(self, properties):
|
|
115
|
+
"""Return the concrete style for one feature. Callables receive the feature's properties."""
|
|
116
|
+
return {
|
|
117
|
+
attribute: value(properties) if callable(value) else value
|
|
118
|
+
for attribute, value in vars(self).items()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class Layer:
|
|
122
|
+
"""Base class for all layers in the map widget.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
name (str): The name of the layer.
|
|
126
|
+
"""
|
|
127
|
+
def __init__(self, name=""):
|
|
128
|
+
self.name = name
|
|
129
|
+
|
|
130
|
+
class Point(Layer):
|
|
131
|
+
"""A point layer on the map. **Not implemented yet.**"""
|
|
132
|
+
def __init__(self, location, color="#3388ff", name=""):
|
|
133
|
+
raise NotImplementedError
|
|
134
|
+
|
|
135
|
+
class Polygon(Layer):
|
|
136
|
+
"""A polygon layer on the map. **Not implemented yet.**"""
|
|
137
|
+
def __init__(self, locations, color="#3388ff", fill_color="#3388ff", name=""):
|
|
138
|
+
raise NotImplementedError
|
|
139
|
+
|
|
140
|
+
class Line(Layer):
|
|
141
|
+
"""A line layer on the map. **Not implemented yet.**"""
|
|
142
|
+
def __init__(self, locations, color="#3388ff", name=""):
|
|
143
|
+
raise NotImplementedError
|
|
144
|
+
|
|
145
|
+
class GeoJSON(Layer):
|
|
146
|
+
"""A GeoJSON layer on the map.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
data (dict, str, or pathlib.Path): GeoJSON as a dict, a JSON string, or a path to a GeoJSON file.
|
|
150
|
+
name (str): The name of the layer.
|
|
151
|
+
style (VectorStyle): The style of the layer.
|
|
152
|
+
"""
|
|
153
|
+
def __init__(self, data, name="",style=None):
|
|
154
|
+
super().__init__(name)
|
|
155
|
+
if isinstance(data, pathlib.Path):
|
|
156
|
+
with open(data, "r") as f:
|
|
157
|
+
data = json.load(f)
|
|
158
|
+
elif type(data) is str:
|
|
159
|
+
if not data.strip().startswith("{"):
|
|
160
|
+
if pathlib.Path(data).exists():
|
|
161
|
+
with open(data, "r") as f:
|
|
162
|
+
data = json.load(f)
|
|
163
|
+
else:
|
|
164
|
+
raise ValueError(f"data string is not a valid JSON or file path: {data}")
|
|
165
|
+
else:
|
|
166
|
+
data = json.loads(data)
|
|
167
|
+
elif not isinstance(data, dict):
|
|
168
|
+
raise TypeError("data must be a dict, str, or pathlib.Path")
|
|
169
|
+
style = style or VectorStyle()
|
|
170
|
+
features = []
|
|
171
|
+
for f in data.get("features", []):
|
|
172
|
+
props = f.get("properties") or {}
|
|
173
|
+
features.append({**f, "properties": {**props, "__ipyd3geo_style__": style.resolve(props)}})
|
|
174
|
+
self.data = {**data, "features": features}
|
|
175
|
+
|
|
176
|
+
class GeoData(Layer):
|
|
177
|
+
"""A GeoDataFrame (such as from geopandas) layer on the map. **Not implemented yet.**"""
|
|
178
|
+
def __init__(self, data, name=""):
|
|
179
|
+
raise NotImplementedError
|
|
180
|
+
|
|
181
|
+
class Raster(Layer):
|
|
182
|
+
"""A local raster layer on the map. **Not implemented yet.**"""
|
|
183
|
+
def __init__(self, data, name=""):
|
|
184
|
+
raise NotImplementedError
|
|
185
|
+
|
|
186
|
+
class TileService(Layer):
|
|
187
|
+
"""Raster tiles from a service. **Not implemented yet.**"""
|
|
188
|
+
def __init__(self, url, name=""):
|
|
189
|
+
raise NotImplementedError
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
pass
|