geovizpy 0.1.4__py3-none-any.whl → 0.1.6__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/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,421 @@
1
+ """Module for rendering the map to HTML and JSON."""
2
+
3
+ import json
4
+ import tempfile
5
+ import os
6
+ import time
7
+ import html
8
+ import sys
9
+ import subprocess
10
+
11
+ class RendererMixin:
12
+ """Mixin class for rendering the map."""
13
+
14
+ def get_config(self):
15
+ """Return the configuration as a JSON-compatible list of commands."""
16
+ def process_args(args):
17
+ new_args = {}
18
+ for k, v in args.items():
19
+ if v is None:
20
+ continue
21
+ if isinstance(v, str) and (v.strip().startswith("(") or v.strip().startswith("function") or "=>" in v):
22
+ new_args[k] = {"__js_func__": v}
23
+ elif isinstance(v, dict):
24
+ new_args[k] = process_args(v)
25
+ else:
26
+ new_args[k] = v
27
+ return new_args
28
+
29
+ processed_commands = []
30
+ for cmd in self.commands:
31
+ processed_commands.append({"name": cmd["name"], "args": process_args(cmd["args"])})
32
+
33
+ return processed_commands
34
+
35
+ def to_json(self):
36
+ """Return the configuration as a JSON string."""
37
+ return json.dumps(self.get_config())
38
+
39
+ def _get_html_content(self):
40
+ """Generate the full HTML content string."""
41
+ json_commands = self.to_json()
42
+
43
+ layer_control_js = self._get_layer_control_js()
44
+ export_control_js = self._get_export_control_js()
45
+
46
+ return f"""
47
+ <!DOCTYPE html>
48
+ <html>
49
+ <head>
50
+ <meta charset="UTF-8" />
51
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Tangerine"/>
52
+ <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
53
+ <script src="https://cdn.jsdelivr.net/npm/geoviz@0.9.8"></script>
54
+ <style>
55
+ body {{ margin: 0; padding: 0; overflow: hidden; }}
56
+ button {{ background: #f8f9fa; border: 1px solid #ddd; border-radius: 3px; }}
57
+ button:hover {{ background: #e2e6ea; }}
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <script>
62
+ const commands = {json_commands};
63
+ let svg;
64
+
65
+ // Helper to revive functions
66
+ function revive(obj) {{
67
+ if (typeof obj === 'object' && obj !== null) {{
68
+ if (obj.hasOwnProperty('__js_func__')) {{
69
+ try {{
70
+ return eval(obj['__js_func__']);
71
+ }} catch (e) {{
72
+ console.error("Failed to eval function:", obj['__js_func__'], e);
73
+ return null;
74
+ }}
75
+ }} else {{
76
+ for (let key in obj) {{
77
+ obj[key] = revive(obj[key]);
78
+ }}
79
+ }}
80
+ }}
81
+ return obj;
82
+ }}
83
+
84
+ const revivedCommands = revive(commands);
85
+
86
+ revivedCommands.forEach(cmd => {{
87
+ if (cmd.name === "create") {{
88
+ svg = geoviz.create(cmd.args);
89
+ }} else {{
90
+ const parts = cmd.name.split(".");
91
+ if (parts.length === 1) {{
92
+ if (svg[parts[0]]) {{
93
+ svg[parts[0]](cmd.args);
94
+ }} else {{
95
+ console.warn("Method " + parts[0] + " not found");
96
+ }}
97
+ }} else if (parts.length === 2) {{
98
+ if (svg[parts[0]] && svg[parts[0]][parts[1]]) {{
99
+ svg[parts[0]][parts[1]](cmd.args);
100
+ }} else {{
101
+ console.warn("Method " + cmd.name + " not found");
102
+ }}
103
+ }}
104
+ }}
105
+ }});
106
+
107
+ if (svg) {{
108
+ document.body.appendChild(svg.render());
109
+ }}
110
+
111
+ {layer_control_js}
112
+ {export_control_js}
113
+ </script>
114
+ </body>
115
+ </html>
116
+ """
117
+
118
+ def render_html(self, filename="map.html"):
119
+ """Render the map to an HTML file."""
120
+ html_content = self._get_html_content()
121
+ with open(filename, "w") as f:
122
+ f.write(html_content)
123
+ print(f"Map saved to {filename}")
124
+
125
+ def show(self, width=800, height=600):
126
+ """
127
+ Display the map in a Jupyter notebook using an IFrame.
128
+
129
+ Args:
130
+ width (int/str): Width of the display area (default 800).
131
+ height (int/str): Height of the display area (default 600).
132
+ """
133
+ try:
134
+ from IPython.display import IFrame
135
+ import base64
136
+ except ImportError:
137
+ print("IPython is required to display the map. Please install it with 'pip install ipython'.")
138
+ return
139
+
140
+ html_content = self._get_html_content()
141
+ b64_content = base64.b64encode(html_content.encode('utf-8')).decode('utf-8')
142
+ data_uri = f"data:text/html;base64,{b64_content}"
143
+
144
+ return IFrame(src=data_uri, width=width, height=height)
145
+
146
+ def save(self, filename="map.html"):
147
+ """
148
+ Save the map to a file.
149
+
150
+ If filename ends with .html, saves the interactive map.
151
+ If filename ends with .png or .svg, saves a static image.
152
+
153
+ For image export, 'playwright' is required. Install it with:
154
+ pip install geovizpy[export]
155
+ playwright install
156
+ """
157
+ if filename.endswith(".html"):
158
+ self.render_html(filename)
159
+ elif filename.endswith(".png") or filename.endswith(".svg"):
160
+ self._save_image(filename)
161
+ else:
162
+ print("Error: filename must end with .html, .png, or .svg")
163
+
164
+ def _save_image(self, filename):
165
+ """Internal method to save as PNG or SVG using Playwright via a subprocess."""
166
+
167
+ # Check if playwright is installed
168
+ try:
169
+ import playwright
170
+ except ImportError:
171
+ print("Error: Playwright is required for image export.")
172
+ print("Please install it with: pip install geovizpy[export] && playwright install")
173
+ return
174
+
175
+ # Create a temporary HTML file
176
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html") as tmp_file:
177
+ self.render_html(tmp_file.name)
178
+ tmp_path = tmp_file.name
179
+
180
+ # Create a temporary Python script to run Playwright
181
+ # This isolates Playwright from the current asyncio loop (Jupyter)
182
+ script_content = f"""
183
+ import os
184
+ from playwright.sync_api import sync_playwright
185
+
186
+ def run():
187
+ try:
188
+ with sync_playwright() as p:
189
+ browser = p.chromium.launch()
190
+ page = browser.new_page(viewport={{"width": 1000, "height": 800}})
191
+ page.goto(f"file://{{os.path.abspath('{tmp_path}')}}")
192
+ page.wait_for_timeout(2000)
193
+
194
+ if "{filename}".endswith(".svg"):
195
+ svg_outer = page.locator("svg").first.evaluate("el => el.outerHTML")
196
+ with open("{filename}", "w") as f:
197
+ f.write(svg_outer)
198
+ else:
199
+ page.locator("svg").first.screenshot(path="{filename}")
200
+
201
+ browser.close()
202
+ print(f"Image saved to {filename}")
203
+ except Exception as e:
204
+ print(f"Error in subprocess: {{e}}")
205
+ exit(1)
206
+
207
+ if __name__ == "__main__":
208
+ run()
209
+ """
210
+
211
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".py") as tmp_script:
212
+ tmp_script.write(script_content)
213
+ tmp_script_path = tmp_script.name
214
+
215
+ try:
216
+ # Run the script in a subprocess
217
+ result = subprocess.run([sys.executable, tmp_script_path], capture_output=True, text=True)
218
+ if result.returncode != 0:
219
+ print(f"Error saving image: {result.stderr}")
220
+ else:
221
+ print(result.stdout.strip())
222
+ finally:
223
+ # Cleanup
224
+ if os.path.exists(tmp_path):
225
+ os.remove(tmp_path)
226
+ if os.path.exists(tmp_script_path):
227
+ os.remove(tmp_script_path)
228
+
229
+ def _get_layer_control_js(self):
230
+ if not self.layer_control_config:
231
+ return ""
232
+ config = self.layer_control_config
233
+ layers_json = json.dumps(config["layers"]) if config["layers"] else "null"
234
+ return f"""
235
+ const layerConfig = {{
236
+ layers: {layers_json},
237
+ pos: "{config.get('pos')}",
238
+ x: {config.get('x', 10)},
239
+ y: {config.get('y', 10)},
240
+ title: "{config.get('title', 'Layers')}"
241
+ }};
242
+
243
+ function createLayerControl() {{
244
+ const wrapper = document.createElement("div");
245
+ wrapper.style.position = "absolute";
246
+ wrapper.style.zIndex = "1000";
247
+
248
+ const button = document.createElement("div");
249
+ 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>`;
250
+ button.style.width = "32px";
251
+ button.style.height = "32px";
252
+ button.style.cursor = "pointer";
253
+ button.style.border = "1px solid #ccc";
254
+ button.style.borderRadius = "4px";
255
+ button.style.backgroundColor = "white";
256
+ button.style.display = "flex";
257
+ button.style.alignItems = "center";
258
+ button.style.justifyContent = "center";
259
+ button.style.boxShadow = "0 1px 3px rgba(0,0,0,0.2)";
260
+
261
+ const panel = document.createElement("div");
262
+ panel.style.display = "none";
263
+ panel.style.backgroundColor = "white";
264
+ panel.style.padding = "10px";
265
+ panel.style.border = "1px solid #ccc";
266
+ panel.style.borderRadius = "5px";
267
+ panel.style.fontFamily = "sans-serif";
268
+ panel.style.fontSize = "12px";
269
+ panel.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
270
+ panel.style.marginTop = "5px";
271
+ panel.style.minWidth = "100px";
272
+
273
+ wrapper.addEventListener("mouseenter", () => panel.style.display = "block");
274
+ wrapper.addEventListener("mouseleave", () => panel.style.display = "none");
275
+
276
+ if (layerConfig.pos === "top-right") {{
277
+ wrapper.style.top = "10px"; wrapper.style.right = "10px";
278
+ }} else if (layerConfig.pos === "bottom-right") {{
279
+ wrapper.style.bottom = "10px"; wrapper.style.right = "10px";
280
+ }} else if (layerConfig.pos === "bottom-left") {{
281
+ wrapper.style.bottom = "10px"; wrapper.style.left = "10px";
282
+ }} else {{
283
+ wrapper.style.top = `${{layerConfig.y}}px`;
284
+ wrapper.style.left = `${{layerConfig.x}}px`;
285
+ }}
286
+
287
+ const title = document.createElement("div");
288
+ title.innerText = layerConfig.title;
289
+ title.style.fontWeight = "bold";
290
+ title.style.marginBottom = "8px";
291
+ title.style.borderBottom = "1px solid #eee";
292
+ title.style.paddingBottom = "5px";
293
+ panel.appendChild(title);
294
+
295
+ let layers = layerConfig.layers;
296
+ if (!layers) {{
297
+ layers = Array.from(svg.selectAll("g[id]").nodes()).map(n => n.id);
298
+ }}
299
+
300
+ let count = 0;
301
+ layers.forEach(layerId => {{
302
+ const layer = svg.select("#" + layerId);
303
+ if (!layer.empty()) {{
304
+ count++;
305
+ const row = document.createElement("div");
306
+ row.style.marginBottom = "5px";
307
+ row.style.display = "flex";
308
+ row.style.alignItems = "center";
309
+
310
+ const checkbox = document.createElement("input");
311
+ checkbox.type = "checkbox";
312
+ checkbox.id = "chk_" + layerId;
313
+ checkbox.checked = true;
314
+ checkbox.style.marginRight = "8px";
315
+ checkbox.style.cursor = "pointer";
316
+
317
+ checkbox.addEventListener("change", (e) => {{
318
+ const display = e.target.checked ? "inline" : "none";
319
+ layer.style("display", display);
320
+ const legLayer = svg.select("#leg_" + layerId);
321
+ if (!legLayer.empty()) legLayer.style("display", display);
322
+ }});
323
+
324
+ const label = document.createElement("label");
325
+ label.htmlFor = "chk_" + layerId;
326
+ label.innerText = layerId;
327
+ label.style.cursor = "pointer";
328
+
329
+ row.appendChild(checkbox);
330
+ row.appendChild(label);
331
+ panel.appendChild(row);
332
+ }}
333
+ }});
334
+
335
+ if (count > 0) {{
336
+ wrapper.appendChild(button);
337
+ wrapper.appendChild(panel);
338
+ document.body.appendChild(wrapper);
339
+ }}
340
+ }}
341
+ setTimeout(createLayerControl, 100);
342
+ """
343
+
344
+ def _get_export_control_js(self):
345
+ if not self.export_control_config:
346
+ return ""
347
+ ex_config = self.export_control_config
348
+ return f"""
349
+ const exportConfig = {{
350
+ pos: "{ex_config.get('pos')}",
351
+ x: {ex_config.get('x', 10)},
352
+ y: {ex_config.get('y', 50)},
353
+ title: "{ex_config.get('title', 'Export')}"
354
+ }};
355
+
356
+ function createExportControl() {{
357
+ const wrapper = document.createElement("div");
358
+ wrapper.style.position = "absolute";
359
+ wrapper.style.zIndex = "1000";
360
+
361
+ const button = document.createElement("div");
362
+ 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>`;
363
+ button.style.width = "32px";
364
+ button.style.height = "32px";
365
+ button.style.cursor = "pointer";
366
+ button.style.border = "1px solid #ccc";
367
+ button.style.borderRadius = "4px";
368
+ button.style.backgroundColor = "white";
369
+ button.style.display = "flex";
370
+ button.style.alignItems = "center";
371
+ button.style.justifyContent = "center";
372
+ button.style.boxShadow = "0 1px 3px rgba(0,0,0,0.2)";
373
+
374
+ const panel = document.createElement("div");
375
+ panel.style.display = "none";
376
+ panel.style.backgroundColor = "white";
377
+ panel.style.padding = "5px";
378
+ panel.style.border = "1px solid #ccc";
379
+ panel.style.borderRadius = "5px";
380
+ panel.style.marginTop = "5px";
381
+ panel.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
382
+ panel.style.display = "none";
383
+ panel.style.flexDirection = "column";
384
+ panel.style.gap = "5px";
385
+
386
+ wrapper.addEventListener("mouseenter", () => panel.style.display = "flex");
387
+ wrapper.addEventListener("mouseleave", () => panel.style.display = "none");
388
+
389
+ if (exportConfig.pos === "top-right") {{
390
+ wrapper.style.top = "50px"; wrapper.style.right = "10px";
391
+ }} else if (exportConfig.pos === "bottom-right") {{
392
+ wrapper.style.bottom = "50px"; wrapper.style.right = "10px";
393
+ }} else {{
394
+ wrapper.style.top = `${{exportConfig.y}}px`;
395
+ wrapper.style.left = `${{exportConfig.x}}px`;
396
+ }}
397
+
398
+ const btnSVG = document.createElement("button");
399
+ btnSVG.innerText = "SVG";
400
+ btnSVG.style.cursor = "pointer";
401
+ btnSVG.style.padding = "5px 10px";
402
+ btnSVG.onclick = () => {{
403
+ geoviz.exportSVG(svg, {{filename: "map.svg"}});
404
+ }};
405
+
406
+ const btnPNG = document.createElement("button");
407
+ btnPNG.innerText = "PNG";
408
+ btnPNG.style.cursor = "pointer";
409
+ btnPNG.style.padding = "5px 10px";
410
+ btnPNG.onclick = () => {{
411
+ geoviz.exportPNG(svg, {{filename: "map.png"}});
412
+ }};
413
+
414
+ panel.appendChild(btnSVG);
415
+ panel.appendChild(btnPNG);
416
+ wrapper.appendChild(button);
417
+ wrapper.appendChild(panel);
418
+ document.body.appendChild(wrapper);
419
+ }}
420
+ setTimeout(createExportControl, 100);
421
+ """