geovizpy 0.1.3__py3-none-any.whl → 0.1.5__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.
- geovizpy/__init__.py +5 -251
- geovizpy/controls.py +31 -0
- geovizpy/effects.py +48 -0
- geovizpy/geoviz.py +43 -0
- geovizpy/legends.py +60 -0
- geovizpy/marks.py +184 -0
- geovizpy/plots.py +86 -0
- geovizpy/renderer.py +363 -0
- geovizpy-0.1.5.dist-info/METADATA +117 -0
- geovizpy-0.1.5.dist-info/RECORD +12 -0
- geovizpy-0.1.3.dist-info/METADATA +0 -46
- geovizpy-0.1.3.dist-info/RECORD +0 -5
- {geovizpy-0.1.3.dist-info → geovizpy-0.1.5.dist-info}/WHEEL +0 -0
- {geovizpy-0.1.3.dist-info → geovizpy-0.1.5.dist-info}/top_level.txt +0 -0
geovizpy/__init__.py
CHANGED
|
@@ -1,253 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
A Python wrapper for the geoviz JavaScript library.
|
|
6
|
-
Allows creating maps by chaining commands and rendering them to an HTML file.
|
|
7
|
-
"""
|
|
8
|
-
def __init__(self, **kwargs):
|
|
9
|
-
self.commands = []
|
|
10
|
-
self.commands.append({"name": "create", "args": kwargs})
|
|
11
|
-
|
|
12
|
-
def _add_command(self, name, args):
|
|
13
|
-
self.commands.append({"name": name, "args": args})
|
|
14
|
-
return self
|
|
15
|
-
|
|
16
|
-
# Marks
|
|
17
|
-
def outline(self, **kwargs):
|
|
18
|
-
return self._add_command("outline", kwargs)
|
|
19
|
-
|
|
20
|
-
def graticule(self, **kwargs):
|
|
21
|
-
return self._add_command("graticule", kwargs)
|
|
22
|
-
|
|
23
|
-
def path(self, **kwargs):
|
|
24
|
-
return self._add_command("path", kwargs)
|
|
25
|
-
|
|
26
|
-
def header(self, **kwargs):
|
|
27
|
-
return self._add_command("header", kwargs)
|
|
28
|
-
|
|
29
|
-
def footer(self, **kwargs):
|
|
30
|
-
return self._add_command("footer", kwargs)
|
|
31
|
-
|
|
32
|
-
def circle(self, **kwargs):
|
|
33
|
-
return self._add_command("circle", kwargs)
|
|
34
|
-
|
|
35
|
-
def square(self, **kwargs):
|
|
36
|
-
return self._add_command("square", kwargs)
|
|
37
|
-
|
|
38
|
-
def spike(self, **kwargs):
|
|
39
|
-
return self._add_command("spike", kwargs)
|
|
40
|
-
|
|
41
|
-
def text(self, **kwargs):
|
|
42
|
-
return self._add_command("text", kwargs)
|
|
43
|
-
|
|
44
|
-
def tile(self, **kwargs):
|
|
45
|
-
return self._add_command("tile", kwargs)
|
|
46
|
-
|
|
47
|
-
def scalebar(self, **kwargs):
|
|
48
|
-
return self._add_command("scalebar", kwargs)
|
|
49
|
-
|
|
50
|
-
def north(self, **kwargs):
|
|
51
|
-
return self._add_command("north", kwargs)
|
|
52
|
-
|
|
53
|
-
def plot(self, **kwargs):
|
|
54
|
-
return self._add_command("plot", kwargs)
|
|
55
|
-
|
|
56
|
-
def tissot(self, **kwargs):
|
|
57
|
-
return self._add_command("tissot", kwargs)
|
|
58
|
-
|
|
59
|
-
def rhumbs(self, **kwargs):
|
|
60
|
-
return self._add_command("rhumbs", kwargs)
|
|
61
|
-
|
|
62
|
-
def earth(self, **kwargs):
|
|
63
|
-
return self._add_command("earth", kwargs)
|
|
64
|
-
|
|
65
|
-
def empty(self, **kwargs):
|
|
66
|
-
return self._add_command("empty", kwargs)
|
|
67
|
-
|
|
68
|
-
def halfcircle(self, **kwargs):
|
|
69
|
-
return self._add_command("halfcircle", kwargs)
|
|
70
|
-
|
|
71
|
-
def symbol(self, **kwargs):
|
|
72
|
-
return self._add_command("symbol", kwargs)
|
|
73
|
-
|
|
74
|
-
def grid(self, **kwargs):
|
|
75
|
-
return self._add_command("grid", kwargs)
|
|
76
|
-
|
|
77
|
-
# Plot shortcuts (sugar syntax for plot({type: ...}))
|
|
78
|
-
def choro(self, **kwargs):
|
|
79
|
-
return self._add_command("plot", {"type": "choro", **kwargs})
|
|
80
|
-
|
|
81
|
-
def typo(self, **kwargs):
|
|
82
|
-
return self._add_command("plot", {"type": "typo", **kwargs})
|
|
83
|
-
|
|
84
|
-
def prop(self, **kwargs):
|
|
85
|
-
return self._add_command("plot", {"type": "prop", **kwargs})
|
|
86
|
-
|
|
87
|
-
def propchoro(self, **kwargs):
|
|
88
|
-
return self._add_command("plot", {"type": "propchoro", **kwargs})
|
|
89
|
-
|
|
90
|
-
def proptypo(self, **kwargs):
|
|
91
|
-
return self._add_command("plot", {"type": "proptypo", **kwargs})
|
|
92
|
-
|
|
93
|
-
def picto(self, **kwargs):
|
|
94
|
-
return self._add_command("plot", {"type": "picto", **kwargs})
|
|
95
|
-
|
|
96
|
-
def bertin(self, **kwargs):
|
|
97
|
-
return self._add_command("plot", {"type": "bertin", **kwargs})
|
|
98
|
-
|
|
99
|
-
# Legends
|
|
100
|
-
def legend_circles_nested(self, **kwargs):
|
|
101
|
-
return self._add_command("legend.circles_nested", kwargs)
|
|
102
|
-
|
|
103
|
-
def legend_circles(self, **kwargs):
|
|
104
|
-
return self._add_command("legend.circles", kwargs)
|
|
105
|
-
|
|
106
|
-
def legend_squares(self, **kwargs):
|
|
107
|
-
return self._add_command("legend.squares", kwargs)
|
|
108
|
-
|
|
109
|
-
def legend_squares_nested(self, **kwargs):
|
|
110
|
-
return self._add_command("legend.squares_nested", kwargs)
|
|
111
|
-
|
|
112
|
-
def legend_circles_half(self, **kwargs):
|
|
113
|
-
return self._add_command("legend.circles_half", kwargs)
|
|
114
|
-
|
|
115
|
-
def legend_spikes(self, **kwargs):
|
|
116
|
-
return self._add_command("legend.spikes", kwargs)
|
|
117
|
-
|
|
118
|
-
def legend_mushrooms(self, **kwargs):
|
|
119
|
-
return self._add_command("legend.mushrooms", kwargs)
|
|
120
|
-
|
|
121
|
-
def legend_choro_vertical(self, **kwargs):
|
|
122
|
-
return self._add_command("legend.choro_vertical", kwargs)
|
|
123
|
-
|
|
124
|
-
def legend_choro_horizontal(self, **kwargs):
|
|
125
|
-
return self._add_command("legend.choro_horizontal", kwargs)
|
|
126
|
-
|
|
127
|
-
def legend_typo_vertical(self, **kwargs):
|
|
128
|
-
return self._add_command("legend.typo_vertical", kwargs)
|
|
129
|
-
|
|
130
|
-
def legend_typo_horizontal(self, **kwargs):
|
|
131
|
-
return self._add_command("legend.typo_horizontal", kwargs)
|
|
132
|
-
|
|
133
|
-
def legend_symbol_vertical(self, **kwargs):
|
|
134
|
-
return self._add_command("legend.symbol_vertical", kwargs)
|
|
135
|
-
|
|
136
|
-
def legend_symbol_horizontal(self, **kwargs):
|
|
137
|
-
return self._add_command("legend.symbol_horizontal", kwargs)
|
|
138
|
-
|
|
139
|
-
def legend_box(self, **kwargs):
|
|
140
|
-
return self._add_command("legend.box", kwargs)
|
|
141
|
-
|
|
142
|
-
# Effects
|
|
143
|
-
def effect_blur(self, **kwargs):
|
|
144
|
-
return self._add_command("effect.blur", kwargs)
|
|
145
|
-
|
|
146
|
-
def effect_shadow(self, **kwargs):
|
|
147
|
-
return self._add_command("effect.shadow", kwargs)
|
|
148
|
-
|
|
149
|
-
def effect_radialGradient(self, **kwargs):
|
|
150
|
-
return self._add_command("effect.radialGradient", kwargs)
|
|
151
|
-
|
|
152
|
-
def effect_clipPath(self, **kwargs):
|
|
153
|
-
return self._add_command("effect.clipPath", kwargs)
|
|
154
|
-
|
|
155
|
-
def get_config(self):
|
|
156
|
-
"""
|
|
157
|
-
Returns the configuration as a JSON-compatible list of commands.
|
|
158
|
-
"""
|
|
159
|
-
def process_args(args):
|
|
160
|
-
new_args = {}
|
|
161
|
-
for k, v in args.items():
|
|
162
|
-
if isinstance(v, str) and (v.strip().startswith("(") or v.strip().startswith("function") or "=>" in v):
|
|
163
|
-
new_args[k] = {"__js_func__": v}
|
|
164
|
-
elif isinstance(v, dict):
|
|
165
|
-
new_args[k] = process_args(v)
|
|
166
|
-
else:
|
|
167
|
-
new_args[k] = v
|
|
168
|
-
return new_args
|
|
169
|
-
|
|
170
|
-
processed_commands = []
|
|
171
|
-
for cmd in self.commands:
|
|
172
|
-
processed_commands.append({"name": cmd["name"], "args": process_args(cmd["args"])})
|
|
173
|
-
|
|
174
|
-
return processed_commands
|
|
175
|
-
|
|
176
|
-
def to_json(self):
|
|
177
|
-
"""
|
|
178
|
-
Returns the configuration as a JSON string.
|
|
179
|
-
"""
|
|
180
|
-
return json.dumps(self.get_config())
|
|
181
|
-
|
|
182
|
-
def render_html(self, filename="map.html"):
|
|
183
|
-
"""
|
|
184
|
-
Renders the map to an HTML file.
|
|
185
|
-
"""
|
|
186
|
-
json_commands = self.to_json()
|
|
187
|
-
|
|
188
|
-
html_content = f"""
|
|
189
|
-
<!DOCTYPE html>
|
|
190
|
-
<html>
|
|
191
|
-
<head>
|
|
192
|
-
<meta charset="UTF-8" />
|
|
193
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Tangerine"/>
|
|
194
|
-
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
195
|
-
<script src="https://cdn.jsdelivr.net/npm/geoviz@0.9.8"></script>
|
|
196
|
-
</head>
|
|
197
|
-
<body>
|
|
198
|
-
<script>
|
|
199
|
-
const commands = {json_commands};
|
|
200
|
-
let svg;
|
|
201
|
-
|
|
202
|
-
// Helper to revive functions
|
|
203
|
-
function revive(obj) {{
|
|
204
|
-
if (typeof obj === 'object' && obj !== null) {{
|
|
205
|
-
if (obj.hasOwnProperty('__js_func__')) {{
|
|
206
|
-
try {{
|
|
207
|
-
return eval(obj['__js_func__']);
|
|
208
|
-
}} catch (e) {{
|
|
209
|
-
console.error("Failed to eval function:", obj['__js_func__'], e);
|
|
210
|
-
return null;
|
|
211
|
-
}}
|
|
212
|
-
}} else {{
|
|
213
|
-
for (let key in obj) {{
|
|
214
|
-
obj[key] = revive(obj[key]);
|
|
215
|
-
}}
|
|
216
|
-
}}
|
|
217
|
-
}}
|
|
218
|
-
return obj;
|
|
219
|
-
}}
|
|
220
|
-
|
|
221
|
-
const revivedCommands = revive(commands);
|
|
1
|
+
"""
|
|
2
|
+
Geovizpy: A Python wrapper for the geoviz JavaScript library.
|
|
3
|
+
"""
|
|
222
4
|
|
|
223
|
-
|
|
224
|
-
if (cmd.name === "create") {{
|
|
225
|
-
svg = geoviz.create(cmd.args);
|
|
226
|
-
}} else {{
|
|
227
|
-
const parts = cmd.name.split(".");
|
|
228
|
-
if (parts.length === 1) {{
|
|
229
|
-
if (svg[parts[0]]) {{
|
|
230
|
-
svg[parts[0]](cmd.args);
|
|
231
|
-
}} else {{
|
|
232
|
-
console.warn("Method " + parts[0] + " not found");
|
|
233
|
-
}}
|
|
234
|
-
}} else if (parts.length === 2) {{
|
|
235
|
-
if (svg[parts[0]] && svg[parts[0]][parts[1]]) {{
|
|
236
|
-
svg[parts[0]][parts[1]](cmd.args);
|
|
237
|
-
}} else {{
|
|
238
|
-
console.warn("Method " + cmd.name + " not found");
|
|
239
|
-
}}
|
|
240
|
-
}}
|
|
241
|
-
}}
|
|
242
|
-
}});
|
|
5
|
+
from .geoviz import Geoviz
|
|
243
6
|
|
|
244
|
-
|
|
245
|
-
document.body.appendChild(svg.render());
|
|
246
|
-
}}
|
|
247
|
-
</script>
|
|
248
|
-
</body>
|
|
249
|
-
</html>
|
|
250
|
-
"""
|
|
251
|
-
with open(filename, "w") as f:
|
|
252
|
-
f.write(html_content)
|
|
253
|
-
print(f"Map saved to {filename}")
|
|
7
|
+
__all__ = ["Geoviz"]
|
geovizpy/controls.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Module for interactive map controls."""
|
|
2
|
+
|
|
3
|
+
class ControlsMixin:
|
|
4
|
+
"""Mixin class for interactive map controls."""
|
|
5
|
+
|
|
6
|
+
def add_layer_control(self, layers=None, pos=None, x=10, y=10, title="Layers"):
|
|
7
|
+
"""
|
|
8
|
+
Add a collapsible layer control widget (expands on hover).
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
layers (list): List of layer IDs to control. If None, finds all layers with IDs.
|
|
12
|
+
pos (string): Predefined position ("top-right", "top-left", etc.). Overrides x, y.
|
|
13
|
+
x (int): X position of the control.
|
|
14
|
+
y (int): Y position of the control.
|
|
15
|
+
title (string): Title of the control panel.
|
|
16
|
+
"""
|
|
17
|
+
self.layer_control_config = {"layers": layers, "pos": pos, "x": x, "y": y, "title": title}
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
def add_export_control(self, pos=None, x=10, y=50, title="Export"):
|
|
21
|
+
"""
|
|
22
|
+
Add a download button to export the map as SVG or PNG (expands on hover).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
pos (string): Predefined position ("top-right", "top-left", etc.). Overrides x, y.
|
|
26
|
+
x (int): X position of the control.
|
|
27
|
+
y (int): Y position of the control.
|
|
28
|
+
title (string): Title of the button.
|
|
29
|
+
"""
|
|
30
|
+
self.export_control_config = {"pos": pos, "x": x, "y": y, "title": title}
|
|
31
|
+
return self
|
geovizpy/effects.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Module for visual effects."""
|
|
2
|
+
|
|
3
|
+
class EffectsMixin:
|
|
4
|
+
"""Mixin class for visual effects."""
|
|
5
|
+
|
|
6
|
+
def effect_blur(self, **kwargs):
|
|
7
|
+
"""
|
|
8
|
+
Apply a blur effect.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
id (string): ID of the effect.
|
|
12
|
+
stdDeviation (number): Standard deviation of the blur.
|
|
13
|
+
"""
|
|
14
|
+
return self._add_command("effect.blur", kwargs)
|
|
15
|
+
|
|
16
|
+
def effect_shadow(self, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Apply a shadow effect.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
id (string): ID of the effect.
|
|
22
|
+
dx (number): X offset.
|
|
23
|
+
dy (number): Y offset.
|
|
24
|
+
stdDeviation (number): Blur amount.
|
|
25
|
+
opacity (number): Opacity of the shadow.
|
|
26
|
+
color (string): Color of the shadow.
|
|
27
|
+
"""
|
|
28
|
+
return self._add_command("effect.shadow", kwargs)
|
|
29
|
+
|
|
30
|
+
def effect_radialGradient(self, **kwargs):
|
|
31
|
+
"""
|
|
32
|
+
Apply a radial gradient effect.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
id (string): ID of the effect.
|
|
36
|
+
stops (list): List of stops (e.g., [{"offset": "0%", "color": "white"}, ...]).
|
|
37
|
+
"""
|
|
38
|
+
return self._add_command("effect.radialGradient", kwargs)
|
|
39
|
+
|
|
40
|
+
def effect_clipPath(self, **kwargs):
|
|
41
|
+
"""
|
|
42
|
+
Apply a clip path effect.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
id (string): ID of the effect.
|
|
46
|
+
datum (object): GeoJSON to use as clip path.
|
|
47
|
+
"""
|
|
48
|
+
return self._add_command("effect.clipPath", kwargs)
|
geovizpy/geoviz.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Main Geoviz class that assembles all modules."""
|
|
2
|
+
|
|
3
|
+
from .marks import MarksMixin
|
|
4
|
+
from .plots import PlotsMixin
|
|
5
|
+
from .legends import LegendsMixin
|
|
6
|
+
from .effects import EffectsMixin
|
|
7
|
+
from .controls import ControlsMixin
|
|
8
|
+
from .renderer import RendererMixin
|
|
9
|
+
|
|
10
|
+
class Geoviz(
|
|
11
|
+
MarksMixin,
|
|
12
|
+
PlotsMixin,
|
|
13
|
+
LegendsMixin,
|
|
14
|
+
EffectsMixin,
|
|
15
|
+
ControlsMixin,
|
|
16
|
+
RendererMixin
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
A Python wrapper for the geoviz JavaScript library.
|
|
20
|
+
Allows creating maps by chaining commands and rendering them to an HTML file.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, **kwargs):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the Geoviz object.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
width (int): Width of the SVG.
|
|
28
|
+
height (int): Height of the SVG.
|
|
29
|
+
margin (list): Margins [top, right, bottom, left].
|
|
30
|
+
domain (object): GeoJSON to define the domain.
|
|
31
|
+
projection (string): Projection name (e.g., "mercator", "EqualEarth").
|
|
32
|
+
zoomable (bool): If True, the map is zoomable.
|
|
33
|
+
background (string): Background color.
|
|
34
|
+
"""
|
|
35
|
+
self.commands = []
|
|
36
|
+
self.commands.append({"name": "create", "args": kwargs})
|
|
37
|
+
self.layer_control_config = None
|
|
38
|
+
self.export_control_config = None
|
|
39
|
+
|
|
40
|
+
def _add_command(self, name, args):
|
|
41
|
+
"""Add a command to the list of commands to be executed."""
|
|
42
|
+
self.commands.append({"name": name, "args": args})
|
|
43
|
+
return self
|
geovizpy/legends.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Module for map legends."""
|
|
2
|
+
|
|
3
|
+
class LegendsMixin:
|
|
4
|
+
"""Mixin class for map legends."""
|
|
5
|
+
|
|
6
|
+
def legend_circles_nested(self, **kwargs):
|
|
7
|
+
"""Draw a nested circles legend."""
|
|
8
|
+
return self._add_command("legend.circles_nested", kwargs)
|
|
9
|
+
|
|
10
|
+
def legend_circles(self, **kwargs):
|
|
11
|
+
"""Draw a circles legend."""
|
|
12
|
+
return self._add_command("legend.circles", kwargs)
|
|
13
|
+
|
|
14
|
+
def legend_squares(self, **kwargs):
|
|
15
|
+
"""Draw a squares legend."""
|
|
16
|
+
return self._add_command("legend.squares", kwargs)
|
|
17
|
+
|
|
18
|
+
def legend_squares_nested(self, **kwargs):
|
|
19
|
+
"""Draw a nested squares legend."""
|
|
20
|
+
return self._add_command("legend.squares_nested", kwargs)
|
|
21
|
+
|
|
22
|
+
def legend_circles_half(self, **kwargs):
|
|
23
|
+
"""Draw a half-circles legend."""
|
|
24
|
+
return self._add_command("legend.circles_half", kwargs)
|
|
25
|
+
|
|
26
|
+
def legend_spikes(self, **kwargs):
|
|
27
|
+
"""Draw a spikes legend."""
|
|
28
|
+
return self._add_command("legend.spikes", kwargs)
|
|
29
|
+
|
|
30
|
+
def legend_mushrooms(self, **kwargs):
|
|
31
|
+
"""Draw a mushrooms legend."""
|
|
32
|
+
return self._add_command("legend.mushrooms", kwargs)
|
|
33
|
+
|
|
34
|
+
def legend_choro_vertical(self, **kwargs):
|
|
35
|
+
"""Draw a vertical choropleth legend."""
|
|
36
|
+
return self._add_command("legend.choro_vertical", kwargs)
|
|
37
|
+
|
|
38
|
+
def legend_choro_horizontal(self, **kwargs):
|
|
39
|
+
"""Draw a horizontal choropleth legend."""
|
|
40
|
+
return self._add_command("legend.choro_horizontal", kwargs)
|
|
41
|
+
|
|
42
|
+
def legend_typo_vertical(self, **kwargs):
|
|
43
|
+
"""Draw a vertical typology legend."""
|
|
44
|
+
return self._add_command("legend.typo_vertical", kwargs)
|
|
45
|
+
|
|
46
|
+
def legend_typo_horizontal(self, **kwargs):
|
|
47
|
+
"""Draw a horizontal typology legend."""
|
|
48
|
+
return self._add_command("legend.typo_horizontal", kwargs)
|
|
49
|
+
|
|
50
|
+
def legend_symbol_vertical(self, **kwargs):
|
|
51
|
+
"""Draw a vertical symbol legend."""
|
|
52
|
+
return self._add_command("legend.symbol_vertical", kwargs)
|
|
53
|
+
|
|
54
|
+
def legend_symbol_horizontal(self, **kwargs):
|
|
55
|
+
"""Draw a horizontal symbol legend."""
|
|
56
|
+
return self._add_command("legend.symbol_horizontal", kwargs)
|
|
57
|
+
|
|
58
|
+
def legend_box(self, **kwargs):
|
|
59
|
+
"""Draw a box legend."""
|
|
60
|
+
return self._add_command("legend.box", kwargs)
|
geovizpy/marks.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Module containing low-level drawing marks."""
|
|
2
|
+
|
|
3
|
+
class MarksMixin:
|
|
4
|
+
"""Mixin class for drawing marks on the map."""
|
|
5
|
+
|
|
6
|
+
def outline(self, **kwargs):
|
|
7
|
+
"""
|
|
8
|
+
Add an outline to the map (graticule sphere).
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
fill (string): Fill color.
|
|
12
|
+
stroke (string): Stroke color.
|
|
13
|
+
strokeWidth (number): Stroke width.
|
|
14
|
+
"""
|
|
15
|
+
return self._add_command("outline", kwargs)
|
|
16
|
+
|
|
17
|
+
def graticule(self, **kwargs):
|
|
18
|
+
"""
|
|
19
|
+
Add a graticule to the map.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
stroke (string): Stroke color.
|
|
23
|
+
strokeWidth (number): Stroke width.
|
|
24
|
+
step (list): Step [x, y] in degrees.
|
|
25
|
+
"""
|
|
26
|
+
return self._add_command("graticule", kwargs)
|
|
27
|
+
|
|
28
|
+
def path(self, **kwargs):
|
|
29
|
+
"""
|
|
30
|
+
Draw a path (geometry) on the map.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
datum (object): GeoJSON Feature or FeatureCollection.
|
|
34
|
+
fill (string): Fill color.
|
|
35
|
+
stroke (string): Stroke color.
|
|
36
|
+
strokeWidth (number): Stroke width.
|
|
37
|
+
"""
|
|
38
|
+
return self._add_command("path", kwargs)
|
|
39
|
+
|
|
40
|
+
def header(self, **kwargs):
|
|
41
|
+
"""
|
|
42
|
+
Add a header (title) to the map.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
text (string): Title text.
|
|
46
|
+
fontSize (number): Font size.
|
|
47
|
+
fontFamily (string): Font family.
|
|
48
|
+
fill (string): Text color.
|
|
49
|
+
anchor (string): Text anchor ("start", "middle", "end").
|
|
50
|
+
"""
|
|
51
|
+
return self._add_command("header", kwargs)
|
|
52
|
+
|
|
53
|
+
def footer(self, **kwargs):
|
|
54
|
+
"""
|
|
55
|
+
Add a footer (source, author) to the map.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
text (string): Footer text.
|
|
59
|
+
fontSize (number): Font size.
|
|
60
|
+
fill (string): Text color.
|
|
61
|
+
anchor (string): Text anchor.
|
|
62
|
+
"""
|
|
63
|
+
return self._add_command("footer", kwargs)
|
|
64
|
+
|
|
65
|
+
def circle(self, **kwargs):
|
|
66
|
+
"""
|
|
67
|
+
Draw circles on the map (low-level mark).
|
|
68
|
+
For proportional circles with legend, use prop().
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
data (object): GeoJSON FeatureCollection.
|
|
72
|
+
r (string|number): Radius value or property name.
|
|
73
|
+
fill (string): Fill color.
|
|
74
|
+
stroke (string): Stroke color.
|
|
75
|
+
tip (string|bool): Tooltip content.
|
|
76
|
+
"""
|
|
77
|
+
return self._add_command("circle", kwargs)
|
|
78
|
+
|
|
79
|
+
def square(self, **kwargs):
|
|
80
|
+
"""
|
|
81
|
+
Draw squares on the map (low-level mark).
|
|
82
|
+
For proportional squares with legend, use prop(symbol="square").
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
data (object): GeoJSON FeatureCollection.
|
|
86
|
+
side (string|number): Side length or property name.
|
|
87
|
+
fill (string): Fill color.
|
|
88
|
+
"""
|
|
89
|
+
return self._add_command("square", kwargs)
|
|
90
|
+
|
|
91
|
+
def spike(self, **kwargs):
|
|
92
|
+
"""
|
|
93
|
+
Draw spikes on the map (low-level mark).
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
data (object): GeoJSON FeatureCollection.
|
|
97
|
+
height (string|number): Height value or property name.
|
|
98
|
+
width (number): Width of the spike.
|
|
99
|
+
fill (string): Fill color.
|
|
100
|
+
"""
|
|
101
|
+
return self._add_command("spike", kwargs)
|
|
102
|
+
|
|
103
|
+
def text(self, **kwargs):
|
|
104
|
+
"""
|
|
105
|
+
Add text labels to the map.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
data (object): GeoJSON FeatureCollection.
|
|
109
|
+
text (string): Property name for the text.
|
|
110
|
+
fontSize (number): Font size.
|
|
111
|
+
fill (string): Text color.
|
|
112
|
+
"""
|
|
113
|
+
return self._add_command("text", kwargs)
|
|
114
|
+
|
|
115
|
+
def tile(self, **kwargs):
|
|
116
|
+
"""
|
|
117
|
+
Add a tile layer (basemap).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
url (string): URL template or keyword (e.g., "worldStreet", "openstreetmap").
|
|
121
|
+
opacity (number): Opacity (0 to 1).
|
|
122
|
+
"""
|
|
123
|
+
return self._add_command("tile", kwargs)
|
|
124
|
+
|
|
125
|
+
def scalebar(self, **kwargs):
|
|
126
|
+
"""
|
|
127
|
+
Add a scale bar.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
x (number): X position.
|
|
131
|
+
y (number): Y position.
|
|
132
|
+
units (string): "km" or "mi".
|
|
133
|
+
"""
|
|
134
|
+
return self._add_command("scalebar", kwargs)
|
|
135
|
+
|
|
136
|
+
def north(self, **kwargs):
|
|
137
|
+
"""
|
|
138
|
+
Add a north arrow.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
x (number): X position.
|
|
142
|
+
y (number): Y position.
|
|
143
|
+
width (number): Width of the arrow.
|
|
144
|
+
"""
|
|
145
|
+
return self._add_command("north", kwargs)
|
|
146
|
+
|
|
147
|
+
def plot(self, **kwargs):
|
|
148
|
+
"""
|
|
149
|
+
Generic plot function.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
type (string): Type of plot ("choro", "prop", "typo", etc.).
|
|
153
|
+
data (object): GeoJSON data.
|
|
154
|
+
var (string): Variable to map.
|
|
155
|
+
"""
|
|
156
|
+
return self._add_command("plot", kwargs)
|
|
157
|
+
|
|
158
|
+
def tissot(self, **kwargs):
|
|
159
|
+
"""Draw Tissot's indicatrix to visualize distortion."""
|
|
160
|
+
return self._add_command("tissot", kwargs)
|
|
161
|
+
|
|
162
|
+
def rhumbs(self, **kwargs):
|
|
163
|
+
"""Draw rhumb lines."""
|
|
164
|
+
return self._add_command("rhumbs", kwargs)
|
|
165
|
+
|
|
166
|
+
def earth(self, **kwargs):
|
|
167
|
+
"""Draw the earth (background)."""
|
|
168
|
+
return self._add_command("earth", kwargs)
|
|
169
|
+
|
|
170
|
+
def empty(self, **kwargs):
|
|
171
|
+
"""Create an empty layer."""
|
|
172
|
+
return self._add_command("empty", kwargs)
|
|
173
|
+
|
|
174
|
+
def halfcircle(self, **kwargs):
|
|
175
|
+
"""Draw half-circles."""
|
|
176
|
+
return self._add_command("halfcircle", kwargs)
|
|
177
|
+
|
|
178
|
+
def symbol(self, **kwargs):
|
|
179
|
+
"""Draw symbols."""
|
|
180
|
+
return self._add_command("symbol", kwargs)
|
|
181
|
+
|
|
182
|
+
def grid(self, **kwargs):
|
|
183
|
+
"""Draw a grid."""
|
|
184
|
+
return self._add_command("grid", kwargs)
|
geovizpy/plots.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Module for thematic map plots."""
|
|
2
|
+
|
|
3
|
+
class PlotsMixin:
|
|
4
|
+
"""Mixin class for thematic map plots."""
|
|
5
|
+
|
|
6
|
+
def choro(self, **kwargs):
|
|
7
|
+
"""
|
|
8
|
+
Draw a choropleth map.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
data (object): GeoJSON FeatureCollection.
|
|
12
|
+
var (string): Variable name containing numeric values.
|
|
13
|
+
method (string): Classification method ('quantile', 'jenks', 'equal', etc.).
|
|
14
|
+
nb (int): Number of classes.
|
|
15
|
+
colors (string|list): Color palette name or list of colors.
|
|
16
|
+
legend (bool): Whether to show the legend (default: True).
|
|
17
|
+
leg_pos (list): Legend position [x, y].
|
|
18
|
+
leg_title (string): Legend title.
|
|
19
|
+
"""
|
|
20
|
+
return self._add_command("plot", {"type": "choro", **kwargs})
|
|
21
|
+
|
|
22
|
+
def typo(self, **kwargs):
|
|
23
|
+
"""
|
|
24
|
+
Draw a typology map (categorical data).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
data (object): GeoJSON FeatureCollection.
|
|
28
|
+
var (string): Variable name containing categories.
|
|
29
|
+
colors (string|list): Color palette or list.
|
|
30
|
+
legend (bool): Show legend.
|
|
31
|
+
"""
|
|
32
|
+
return self._add_command("plot", {"type": "typo", **kwargs})
|
|
33
|
+
|
|
34
|
+
def prop(self, **kwargs):
|
|
35
|
+
"""
|
|
36
|
+
Draw a proportional symbol map.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
data (object): GeoJSON FeatureCollection.
|
|
40
|
+
var (string): Variable name containing numeric values.
|
|
41
|
+
symbol (string): Symbol type ("circle", "square", "spike").
|
|
42
|
+
k (number): Size of the largest symbol.
|
|
43
|
+
fill (string): Fill color.
|
|
44
|
+
legend (bool): Show legend.
|
|
45
|
+
leg_type (string): Legend style ("nested", "separate").
|
|
46
|
+
"""
|
|
47
|
+
return self._add_command("plot", {"type": "prop", **kwargs})
|
|
48
|
+
|
|
49
|
+
def propchoro(self, **kwargs):
|
|
50
|
+
"""
|
|
51
|
+
Draw proportional symbols colored by a choropleth variable.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data (object): GeoJSON FeatureCollection.
|
|
55
|
+
var (string): Variable for symbol size.
|
|
56
|
+
var2 (string): Variable for color.
|
|
57
|
+
method (string): Classification method for color.
|
|
58
|
+
colors (string|list): Color palette.
|
|
59
|
+
"""
|
|
60
|
+
return self._add_command("plot", {"type": "propchoro", **kwargs})
|
|
61
|
+
|
|
62
|
+
def proptypo(self, **kwargs):
|
|
63
|
+
"""
|
|
64
|
+
Draw proportional symbols colored by categories.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
data (object): GeoJSON FeatureCollection.
|
|
68
|
+
var (string): Variable for symbol size.
|
|
69
|
+
var2 (string): Variable for category color.
|
|
70
|
+
"""
|
|
71
|
+
return self._add_command("plot", {"type": "proptypo", **kwargs})
|
|
72
|
+
|
|
73
|
+
def picto(self, **kwargs):
|
|
74
|
+
"""Draw a pictogram map."""
|
|
75
|
+
return self._add_command("plot", {"type": "picto", **kwargs})
|
|
76
|
+
|
|
77
|
+
def bertin(self, **kwargs):
|
|
78
|
+
"""
|
|
79
|
+
Draw a Bertin map (dots).
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
data (object): GeoJSON FeatureCollection.
|
|
83
|
+
var (string): Variable name.
|
|
84
|
+
n (int): Number of dots per unit.
|
|
85
|
+
"""
|
|
86
|
+
return self._add_command("plot", {"type": "bertin", **kwargs})
|
geovizpy/renderer.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Module for rendering the map to HTML and JSON."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
class RendererMixin:
|
|
9
|
+
"""Mixin class for rendering the map."""
|
|
10
|
+
|
|
11
|
+
def get_config(self):
|
|
12
|
+
"""Return the configuration as a JSON-compatible list of commands."""
|
|
13
|
+
def process_args(args):
|
|
14
|
+
new_args = {}
|
|
15
|
+
for k, v in args.items():
|
|
16
|
+
if v is None:
|
|
17
|
+
continue
|
|
18
|
+
if isinstance(v, str) and (v.strip().startswith("(") or v.strip().startswith("function") or "=>" in v):
|
|
19
|
+
new_args[k] = {"__js_func__": v}
|
|
20
|
+
elif isinstance(v, dict):
|
|
21
|
+
new_args[k] = process_args(v)
|
|
22
|
+
else:
|
|
23
|
+
new_args[k] = v
|
|
24
|
+
return new_args
|
|
25
|
+
|
|
26
|
+
processed_commands = []
|
|
27
|
+
for cmd in self.commands:
|
|
28
|
+
processed_commands.append({"name": cmd["name"], "args": process_args(cmd["args"])})
|
|
29
|
+
|
|
30
|
+
return processed_commands
|
|
31
|
+
|
|
32
|
+
def to_json(self):
|
|
33
|
+
"""Return the configuration as a JSON string."""
|
|
34
|
+
return json.dumps(self.get_config())
|
|
35
|
+
|
|
36
|
+
def render_html(self, filename="map.html"):
|
|
37
|
+
"""Render the map to an HTML file."""
|
|
38
|
+
json_commands = self.to_json()
|
|
39
|
+
|
|
40
|
+
layer_control_js = self._get_layer_control_js()
|
|
41
|
+
export_control_js = self._get_export_control_js()
|
|
42
|
+
|
|
43
|
+
html_content = f"""
|
|
44
|
+
<!DOCTYPE html>
|
|
45
|
+
<html>
|
|
46
|
+
<head>
|
|
47
|
+
<meta charset="UTF-8" />
|
|
48
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Tangerine"/>
|
|
49
|
+
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
50
|
+
<script src="https://cdn.jsdelivr.net/npm/geoviz@0.9.8"></script>
|
|
51
|
+
<style>
|
|
52
|
+
body {{ margin: 0; padding: 0; }}
|
|
53
|
+
button {{ background: #f8f9fa; border: 1px solid #ddd; border-radius: 3px; }}
|
|
54
|
+
button:hover {{ background: #e2e6ea; }}
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<script>
|
|
59
|
+
const commands = {json_commands};
|
|
60
|
+
let svg;
|
|
61
|
+
|
|
62
|
+
// Helper to revive functions
|
|
63
|
+
function revive(obj) {{
|
|
64
|
+
if (typeof obj === 'object' && obj !== null) {{
|
|
65
|
+
if (obj.hasOwnProperty('__js_func__')) {{
|
|
66
|
+
try {{
|
|
67
|
+
return eval(obj['__js_func__']);
|
|
68
|
+
}} catch (e) {{
|
|
69
|
+
console.error("Failed to eval function:", obj['__js_func__'], e);
|
|
70
|
+
return null;
|
|
71
|
+
}}
|
|
72
|
+
}} else {{
|
|
73
|
+
for (let key in obj) {{
|
|
74
|
+
obj[key] = revive(obj[key]);
|
|
75
|
+
}}
|
|
76
|
+
}}
|
|
77
|
+
}}
|
|
78
|
+
return obj;
|
|
79
|
+
}}
|
|
80
|
+
|
|
81
|
+
const revivedCommands = revive(commands);
|
|
82
|
+
|
|
83
|
+
revivedCommands.forEach(cmd => {{
|
|
84
|
+
if (cmd.name === "create") {{
|
|
85
|
+
svg = geoviz.create(cmd.args);
|
|
86
|
+
}} else {{
|
|
87
|
+
const parts = cmd.name.split(".");
|
|
88
|
+
if (parts.length === 1) {{
|
|
89
|
+
if (svg[parts[0]]) {{
|
|
90
|
+
svg[parts[0]](cmd.args);
|
|
91
|
+
}} else {{
|
|
92
|
+
console.warn("Method " + parts[0] + " not found");
|
|
93
|
+
}}
|
|
94
|
+
}} else if (parts.length === 2) {{
|
|
95
|
+
if (svg[parts[0]] && svg[parts[0]][parts[1]]) {{
|
|
96
|
+
svg[parts[0]][parts[1]](cmd.args);
|
|
97
|
+
}} else {{
|
|
98
|
+
console.warn("Method " + cmd.name + " not found");
|
|
99
|
+
}}
|
|
100
|
+
}}
|
|
101
|
+
}}
|
|
102
|
+
}});
|
|
103
|
+
|
|
104
|
+
if (svg) {{
|
|
105
|
+
document.body.appendChild(svg.render());
|
|
106
|
+
}}
|
|
107
|
+
|
|
108
|
+
{layer_control_js}
|
|
109
|
+
{export_control_js}
|
|
110
|
+
</script>
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|
|
113
|
+
"""
|
|
114
|
+
with open(filename, "w") as f:
|
|
115
|
+
f.write(html_content)
|
|
116
|
+
print(f"Map saved to {filename}")
|
|
117
|
+
|
|
118
|
+
def save(self, filename="map.html"):
|
|
119
|
+
"""
|
|
120
|
+
Save the map to a file.
|
|
121
|
+
|
|
122
|
+
If filename ends with .html, saves the interactive map.
|
|
123
|
+
If filename ends with .png or .svg, saves a static image.
|
|
124
|
+
|
|
125
|
+
For image export, 'playwright' is required. Install it with:
|
|
126
|
+
pip install geovizpy[export]
|
|
127
|
+
playwright install
|
|
128
|
+
"""
|
|
129
|
+
if filename.endswith(".html"):
|
|
130
|
+
self.render_html(filename)
|
|
131
|
+
elif filename.endswith(".png") or filename.endswith(".svg"):
|
|
132
|
+
self._save_image(filename)
|
|
133
|
+
else:
|
|
134
|
+
print("Error: filename must end with .html, .png, or .svg")
|
|
135
|
+
|
|
136
|
+
def _save_image(self, filename):
|
|
137
|
+
"""Internal method to save as PNG or SVG using Playwright."""
|
|
138
|
+
try:
|
|
139
|
+
from playwright.sync_api import sync_playwright
|
|
140
|
+
except ImportError:
|
|
141
|
+
print("Error: Playwright is required for image export.")
|
|
142
|
+
print("Please install it with: pip install geovizpy[export] && playwright install")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html") as tmp_file:
|
|
146
|
+
self.render_html(tmp_file.name)
|
|
147
|
+
tmp_path = tmp_file.name
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
with sync_playwright() as p:
|
|
151
|
+
browser = p.chromium.launch()
|
|
152
|
+
page = browser.new_page(viewport={"width": 1000, "height": 800})
|
|
153
|
+
page.goto(f"file://{os.path.abspath(tmp_path)}")
|
|
154
|
+
page.wait_for_timeout(2000)
|
|
155
|
+
|
|
156
|
+
if filename.endswith(".svg"):
|
|
157
|
+
svg_outer = page.locator("svg").first.evaluate("el => el.outerHTML")
|
|
158
|
+
with open(filename, "w") as f:
|
|
159
|
+
f.write(svg_outer)
|
|
160
|
+
else: # .png
|
|
161
|
+
page.locator("svg").first.screenshot(path=filename)
|
|
162
|
+
|
|
163
|
+
browser.close()
|
|
164
|
+
print(f"Image saved to {filename}")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print(f"Error saving image: {e}")
|
|
167
|
+
finally:
|
|
168
|
+
if os.path.exists(tmp_path):
|
|
169
|
+
os.remove(tmp_path)
|
|
170
|
+
|
|
171
|
+
def _get_layer_control_js(self):
|
|
172
|
+
if not self.layer_control_config:
|
|
173
|
+
return ""
|
|
174
|
+
config = self.layer_control_config
|
|
175
|
+
layers_json = json.dumps(config["layers"]) if config["layers"] else "null"
|
|
176
|
+
return f"""
|
|
177
|
+
const layerConfig = {{
|
|
178
|
+
layers: {layers_json},
|
|
179
|
+
pos: "{config.get('pos')}",
|
|
180
|
+
x: {config.get('x', 10)},
|
|
181
|
+
y: {config.get('y', 10)},
|
|
182
|
+
title: "{config.get('title', 'Layers')}"
|
|
183
|
+
}};
|
|
184
|
+
|
|
185
|
+
function createLayerControl() {{
|
|
186
|
+
const wrapper = document.createElement("div");
|
|
187
|
+
wrapper.style.position = "absolute";
|
|
188
|
+
wrapper.style.zIndex = "1000";
|
|
189
|
+
|
|
190
|
+
const button = document.createElement("div");
|
|
191
|
+
button.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="#333"><path d="M11.99 18.54l-7.37-5.73L3 14.07l9 7 9-7-1.63-1.27-7.38 5.74zM12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27L12 16z"/></svg>`;
|
|
192
|
+
button.style.width = "32px";
|
|
193
|
+
button.style.height = "32px";
|
|
194
|
+
button.style.cursor = "pointer";
|
|
195
|
+
button.style.border = "1px solid #ccc";
|
|
196
|
+
button.style.borderRadius = "4px";
|
|
197
|
+
button.style.backgroundColor = "white";
|
|
198
|
+
button.style.display = "flex";
|
|
199
|
+
button.style.alignItems = "center";
|
|
200
|
+
button.style.justifyContent = "center";
|
|
201
|
+
button.style.boxShadow = "0 1px 3px rgba(0,0,0,0.2)";
|
|
202
|
+
|
|
203
|
+
const panel = document.createElement("div");
|
|
204
|
+
panel.style.display = "none";
|
|
205
|
+
panel.style.backgroundColor = "white";
|
|
206
|
+
panel.style.padding = "10px";
|
|
207
|
+
panel.style.border = "1px solid #ccc";
|
|
208
|
+
panel.style.borderRadius = "5px";
|
|
209
|
+
panel.style.fontFamily = "sans-serif";
|
|
210
|
+
panel.style.fontSize = "12px";
|
|
211
|
+
panel.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
|
|
212
|
+
panel.style.marginTop = "5px";
|
|
213
|
+
panel.style.minWidth = "100px";
|
|
214
|
+
|
|
215
|
+
wrapper.addEventListener("mouseenter", () => panel.style.display = "block");
|
|
216
|
+
wrapper.addEventListener("mouseleave", () => panel.style.display = "none");
|
|
217
|
+
|
|
218
|
+
if (layerConfig.pos === "top-right") {{
|
|
219
|
+
wrapper.style.top = "10px"; wrapper.style.right = "10px";
|
|
220
|
+
}} else if (layerConfig.pos === "bottom-right") {{
|
|
221
|
+
wrapper.style.bottom = "10px"; wrapper.style.right = "10px";
|
|
222
|
+
}} else if (layerConfig.pos === "bottom-left") {{
|
|
223
|
+
wrapper.style.bottom = "10px"; wrapper.style.left = "10px";
|
|
224
|
+
}} else {{
|
|
225
|
+
wrapper.style.top = `${{layerConfig.y}}px`;
|
|
226
|
+
wrapper.style.left = `${{layerConfig.x}}px`;
|
|
227
|
+
}}
|
|
228
|
+
|
|
229
|
+
const title = document.createElement("div");
|
|
230
|
+
title.innerText = layerConfig.title;
|
|
231
|
+
title.style.fontWeight = "bold";
|
|
232
|
+
title.style.marginBottom = "8px";
|
|
233
|
+
title.style.borderBottom = "1px solid #eee";
|
|
234
|
+
title.style.paddingBottom = "5px";
|
|
235
|
+
panel.appendChild(title);
|
|
236
|
+
|
|
237
|
+
let layers = layerConfig.layers;
|
|
238
|
+
if (!layers) {{
|
|
239
|
+
layers = Array.from(svg.selectAll("g[id]").nodes()).map(n => n.id);
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
let count = 0;
|
|
243
|
+
layers.forEach(layerId => {{
|
|
244
|
+
const layer = svg.select("#" + layerId);
|
|
245
|
+
if (!layer.empty()) {{
|
|
246
|
+
count++;
|
|
247
|
+
const row = document.createElement("div");
|
|
248
|
+
row.style.marginBottom = "5px";
|
|
249
|
+
row.style.display = "flex";
|
|
250
|
+
row.style.alignItems = "center";
|
|
251
|
+
|
|
252
|
+
const checkbox = document.createElement("input");
|
|
253
|
+
checkbox.type = "checkbox";
|
|
254
|
+
checkbox.id = "chk_" + layerId;
|
|
255
|
+
checkbox.checked = true;
|
|
256
|
+
checkbox.style.marginRight = "8px";
|
|
257
|
+
checkbox.style.cursor = "pointer";
|
|
258
|
+
|
|
259
|
+
checkbox.addEventListener("change", (e) => {{
|
|
260
|
+
const display = e.target.checked ? "inline" : "none";
|
|
261
|
+
layer.style("display", display);
|
|
262
|
+
const legLayer = svg.select("#leg_" + layerId);
|
|
263
|
+
if (!legLayer.empty()) legLayer.style("display", display);
|
|
264
|
+
}});
|
|
265
|
+
|
|
266
|
+
const label = document.createElement("label");
|
|
267
|
+
label.htmlFor = "chk_" + layerId;
|
|
268
|
+
label.innerText = layerId;
|
|
269
|
+
label.style.cursor = "pointer";
|
|
270
|
+
|
|
271
|
+
row.appendChild(checkbox);
|
|
272
|
+
row.appendChild(label);
|
|
273
|
+
panel.appendChild(row);
|
|
274
|
+
}}
|
|
275
|
+
}});
|
|
276
|
+
|
|
277
|
+
if (count > 0) {{
|
|
278
|
+
wrapper.appendChild(button);
|
|
279
|
+
wrapper.appendChild(panel);
|
|
280
|
+
document.body.appendChild(wrapper);
|
|
281
|
+
}}
|
|
282
|
+
}}
|
|
283
|
+
setTimeout(createLayerControl, 100);
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def _get_export_control_js(self):
|
|
287
|
+
if not self.export_control_config:
|
|
288
|
+
return ""
|
|
289
|
+
ex_config = self.export_control_config
|
|
290
|
+
return f"""
|
|
291
|
+
const exportConfig = {{
|
|
292
|
+
pos: "{ex_config.get('pos')}",
|
|
293
|
+
x: {ex_config.get('x', 10)},
|
|
294
|
+
y: {ex_config.get('y', 50)},
|
|
295
|
+
title: "{ex_config.get('title', 'Export')}"
|
|
296
|
+
}};
|
|
297
|
+
|
|
298
|
+
function createExportControl() {{
|
|
299
|
+
const wrapper = document.createElement("div");
|
|
300
|
+
wrapper.style.position = "absolute";
|
|
301
|
+
wrapper.style.zIndex = "1000";
|
|
302
|
+
|
|
303
|
+
const button = document.createElement("div");
|
|
304
|
+
button.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="#333"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>`;
|
|
305
|
+
button.style.width = "32px";
|
|
306
|
+
button.style.height = "32px";
|
|
307
|
+
button.style.cursor = "pointer";
|
|
308
|
+
button.style.border = "1px solid #ccc";
|
|
309
|
+
button.style.borderRadius = "4px";
|
|
310
|
+
button.style.backgroundColor = "white";
|
|
311
|
+
button.style.display = "flex";
|
|
312
|
+
button.style.alignItems = "center";
|
|
313
|
+
button.style.justifyContent = "center";
|
|
314
|
+
button.style.boxShadow = "0 1px 3px rgba(0,0,0,0.2)";
|
|
315
|
+
|
|
316
|
+
const panel = document.createElement("div");
|
|
317
|
+
panel.style.display = "none";
|
|
318
|
+
panel.style.backgroundColor = "white";
|
|
319
|
+
panel.style.padding = "5px";
|
|
320
|
+
panel.style.border = "1px solid #ccc";
|
|
321
|
+
panel.style.borderRadius = "5px";
|
|
322
|
+
panel.style.marginTop = "5px";
|
|
323
|
+
panel.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
|
|
324
|
+
panel.style.display = "none";
|
|
325
|
+
panel.style.flexDirection = "column";
|
|
326
|
+
panel.style.gap = "5px";
|
|
327
|
+
|
|
328
|
+
wrapper.addEventListener("mouseenter", () => panel.style.display = "flex");
|
|
329
|
+
wrapper.addEventListener("mouseleave", () => panel.style.display = "none");
|
|
330
|
+
|
|
331
|
+
if (exportConfig.pos === "top-right") {{
|
|
332
|
+
wrapper.style.top = "50px"; wrapper.style.right = "10px";
|
|
333
|
+
}} else if (exportConfig.pos === "bottom-right") {{
|
|
334
|
+
wrapper.style.bottom = "50px"; wrapper.style.right = "10px";
|
|
335
|
+
}} else {{
|
|
336
|
+
wrapper.style.top = `${{exportConfig.y}}px`;
|
|
337
|
+
wrapper.style.left = `${{exportConfig.x}}px`;
|
|
338
|
+
}}
|
|
339
|
+
|
|
340
|
+
const btnSVG = document.createElement("button");
|
|
341
|
+
btnSVG.innerText = "SVG";
|
|
342
|
+
btnSVG.style.cursor = "pointer";
|
|
343
|
+
btnSVG.style.padding = "5px 10px";
|
|
344
|
+
btnSVG.onclick = () => {{
|
|
345
|
+
geoviz.exportSVG(svg, {{filename: "map.svg"}});
|
|
346
|
+
}};
|
|
347
|
+
|
|
348
|
+
const btnPNG = document.createElement("button");
|
|
349
|
+
btnPNG.innerText = "PNG";
|
|
350
|
+
btnPNG.style.cursor = "pointer";
|
|
351
|
+
btnPNG.style.padding = "5px 10px";
|
|
352
|
+
btnPNG.onclick = () => {{
|
|
353
|
+
geoviz.exportPNG(svg, {{filename: "map.png"}});
|
|
354
|
+
}};
|
|
355
|
+
|
|
356
|
+
panel.appendChild(btnSVG);
|
|
357
|
+
panel.appendChild(btnPNG);
|
|
358
|
+
wrapper.appendChild(button);
|
|
359
|
+
wrapper.appendChild(panel);
|
|
360
|
+
document.body.appendChild(wrapper);
|
|
361
|
+
}}
|
|
362
|
+
setTimeout(createExportControl, 100);
|
|
363
|
+
"""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geovizpy
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: A Python wrapper for the geoviz JavaScript library
|
|
5
|
+
Author: fbxyz
|
|
6
|
+
Project-URL: Source, https://codeberg.org/fbxyz/geovizpy
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Provides-Extra: export
|
|
13
|
+
Requires-Dist: playwright; extra == "export"
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: project-url
|
|
19
|
+
Dynamic: provides-extra
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
22
|
+
|
|
23
|
+
# geovizpy
|
|
24
|
+
|
|
25
|
+
**geovizpy** is a Python wrapper for the `geoviz` JavaScript library, designed to bring the power of D3.js-based thematic mapping to Python. It allows you to create high-quality, interactive maps directly from Python scripts or Jupyter notebooks.
|
|
26
|
+
|
|
27
|
+
This library is a wrapper around the excellent `geoviz` library. For detailed information on the underlying mapping logic, please refer to the [original geoviz documentation](https://github.com/neocarto/geoviz).
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Simple, chainable API**: Build complex maps by chaining intuitive methods.
|
|
32
|
+
- **Variety of Map Types**: Create choropleth, proportional symbol, typology, and other thematic maps.
|
|
33
|
+
- **Interactive Controls**: Add hover-to-expand controls for toggling layer visibility and exporting the map as SVG or PNG.
|
|
34
|
+
- **Customizable**: Extensive options to customize colors, legends, strokes, and more.
|
|
35
|
+
- **Standalone HTML**: Renders self-contained HTML files with no server required.
|
|
36
|
+
- **Image Export**: Save maps directly to PNG or SVG from Python (requires optional dependencies).
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### Standard Installation
|
|
41
|
+
|
|
42
|
+
You can install the core library using pip:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install geovizpy
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or install directly from the source repository:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install git+https://codeberg.org/fbxyz/geovizpy.git
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### For Image Export
|
|
55
|
+
|
|
56
|
+
To save maps as PNG or SVG files directly from Python, you need to install the optional `export` dependencies, which include `playwright`.
|
|
57
|
+
|
|
58
|
+
1. **Install the extra dependencies:**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install "geovizpy[export]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
2. **Install Playwright's browser binaries:**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
playwright install
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
On Linux, you may also need to install host dependencies:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
sudo playwright install-deps
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quick Start
|
|
77
|
+
|
|
78
|
+
Here is a simple example of how to create a choropleth map:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from geovizpy import Geoviz
|
|
82
|
+
import json
|
|
83
|
+
|
|
84
|
+
# Load your GeoJSON data
|
|
85
|
+
# (Assuming 'world.json' is in a 'data' subdirectory)
|
|
86
|
+
with open("data/world.json") as f:
|
|
87
|
+
world_data = json.load(f)
|
|
88
|
+
|
|
89
|
+
# Initialize the map
|
|
90
|
+
viz = Geoviz(projection="EqualEarth", width=800)
|
|
91
|
+
|
|
92
|
+
# Add layers
|
|
93
|
+
viz.outline()
|
|
94
|
+
viz.graticule()
|
|
95
|
+
|
|
96
|
+
# Add a choropleth layer
|
|
97
|
+
viz.choro(
|
|
98
|
+
data=world_data,
|
|
99
|
+
var="gdppc",
|
|
100
|
+
colors="Blues",
|
|
101
|
+
legend=True,
|
|
102
|
+
leg_title="GDP per Capita"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Add interactive controls
|
|
106
|
+
viz.add_layer_control(layers=["choropleth_gdp"])
|
|
107
|
+
viz.add_export_control()
|
|
108
|
+
|
|
109
|
+
# Save the map
|
|
110
|
+
viz.save("my_map.html") # Renders an interactive HTML file
|
|
111
|
+
# viz.save("my_map.png") # Renders a static PNG image (requires export dependencies)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
For more detailed information on all available methods and parameters, please see the [full documentation](https://your-username.github.io/geovizpy/).
|
|
117
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
geovizpy/__init__.py,sha256=gvgVlK5oB6meBmYgXkXFVohPKkO3ev9UupPA8BQNzYg,120
|
|
2
|
+
geovizpy/controls.py,sha256=pvSrbrkgyB8VURVXED3Etou3Wk2wfWB5p4nnvHLFrgM,1319
|
|
3
|
+
geovizpy/effects.py,sha256=6F78g0IJEbEG8EsKurmBapE8UX0sho7zKHDnzqJ0ci0,1433
|
|
4
|
+
geovizpy/geoviz.py,sha256=bG1A5AQIhsfIJ9RQ4RJteG9HRuu70k2xI9l_eydQTH0,1415
|
|
5
|
+
geovizpy/legends.py,sha256=IPn_1drQ9MkmJlQ54XwBqAlTRSyq98sm8gzgE-ATRog,2218
|
|
6
|
+
geovizpy/marks.py,sha256=xqwf8ELY05lnrrXGQljMe-mm315vUt_-qOmdFAsYhwA,5520
|
|
7
|
+
geovizpy/plots.py,sha256=EVjPPBFyUFQpXH7sAuxCz_sctonJVUxoi8edbehKrpI,3089
|
|
8
|
+
geovizpy/renderer.py,sha256=lrPZnAbvOdvtjA5DrcHkcID_CMwFcpreQwzDspf0ES4,14603
|
|
9
|
+
geovizpy-0.1.5.dist-info/METADATA,sha256=SNuEBOpCeC8TT1o-bV5WkT3jB-E5wos0fGx9rd3EOcg,3386
|
|
10
|
+
geovizpy-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
geovizpy-0.1.5.dist-info/top_level.txt,sha256=3D5AFdMd9bWEvGQUWDy6jccJamUwdKmchgCmxLHaAYs,9
|
|
12
|
+
geovizpy-0.1.5.dist-info/RECORD,,
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: geovizpy
|
|
3
|
-
Version: 0.1.3
|
|
4
|
-
Summary: A Python wrapper for the geoviz JavaScript library
|
|
5
|
-
Author: Florian
|
|
6
|
-
Project-URL: Source, https://codeberg.org/florian/geovizpy
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
-
Classifier: Operating System :: OS Independent
|
|
10
|
-
Requires-Python: >=3.6
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
Dynamic: author
|
|
13
|
-
Dynamic: classifier
|
|
14
|
-
Dynamic: description
|
|
15
|
-
Dynamic: description-content-type
|
|
16
|
-
Dynamic: project-url
|
|
17
|
-
Dynamic: requires-python
|
|
18
|
-
Dynamic: summary
|
|
19
|
-
|
|
20
|
-
# geovizpy
|
|
21
|
-
|
|
22
|
-
A Python wrapper for the [geoviz](https://github.com/neocarto/geoviz) JavaScript library.
|
|
23
|
-
It allows you to create thematic maps using a simple Python API and render them to HTML.
|
|
24
|
-
|
|
25
|
-
## Installation
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
pip install git+https://github.com/fbxyz/geovizpy.git
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Usage
|
|
32
|
-
|
|
33
|
-
```python
|
|
34
|
-
from geovizpy import Geoviz
|
|
35
|
-
import json
|
|
36
|
-
|
|
37
|
-
# Load your GeoJSON data
|
|
38
|
-
with open("examples/world.json") as f:
|
|
39
|
-
world_data = json.load(f)
|
|
40
|
-
|
|
41
|
-
# Create a map
|
|
42
|
-
viz = Geoviz(projection="EqualEarth")
|
|
43
|
-
viz.outline()
|
|
44
|
-
viz.choro(data=world_data, var="gdppc")
|
|
45
|
-
viz.render_html("map.html")
|
|
46
|
-
```
|
geovizpy-0.1.3.dist-info/RECORD
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
geovizpy/__init__.py,sha256=pM9RjTVtCgm9v19mW52wMMhwCSMjtJjNTUqY6O5fJeU,7691
|
|
2
|
-
geovizpy-0.1.3.dist-info/METADATA,sha256=ykOlJ5BnrMYlR8b0rWrNYLNqPP2tbgWjo_PQtVkIfu4,1122
|
|
3
|
-
geovizpy-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
-
geovizpy-0.1.3.dist-info/top_level.txt,sha256=3D5AFdMd9bWEvGQUWDy6jccJamUwdKmchgCmxLHaAYs,9
|
|
5
|
-
geovizpy-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|