From 395fa0855c8514015b1924860768dd1a4c408d30 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:14 +0100 Subject: [PATCH 1/2] Add PyScript examples for pyshp Generated by apply_llm_response.py from prompts/pyshp/response.toml. Examples included: - write_and_read_points: Write and read a points shapefile - polygons_and_geojson: Polygons and the GeoJSON interface - filter_and_edit: Filter, edit, and rewrite a shapefile Generated-By: apply_llm_response.py --- examples/pyshp/README.md | 18 +++ examples/pyshp/filter_and_edit/code.py | 116 ++++++++++++++++++ examples/pyshp/filter_and_edit/config.toml | 1 + examples/pyshp/filter_and_edit/setup.py | 22 ++++ examples/pyshp/order.json | 5 + examples/pyshp/polygons_and_geojson/code.py | 89 ++++++++++++++ .../pyshp/polygons_and_geojson/config.toml | 1 + examples/pyshp/polygons_and_geojson/setup.py | 22 ++++ examples/pyshp/write_and_read_points/code.py | 78 ++++++++++++ .../pyshp/write_and_read_points/config.toml | 1 + examples/pyshp/write_and_read_points/setup.py | 41 +++++++ 11 files changed, 394 insertions(+) create mode 100644 examples/pyshp/README.md create mode 100644 examples/pyshp/filter_and_edit/code.py create mode 100644 examples/pyshp/filter_and_edit/config.toml create mode 100644 examples/pyshp/filter_and_edit/setup.py create mode 100644 examples/pyshp/order.json create mode 100644 examples/pyshp/polygons_and_geojson/code.py create mode 100644 examples/pyshp/polygons_and_geojson/config.toml create mode 100644 examples/pyshp/polygons_and_geojson/setup.py create mode 100644 examples/pyshp/write_and_read_points/code.py create mode 100644 examples/pyshp/write_and_read_points/config.toml create mode 100644 examples/pyshp/write_and_read_points/setup.py diff --git a/examples/pyshp/README.md b/examples/pyshp/README.md new file mode 100644 index 0000000..6be703f --- /dev/null +++ b/examples/pyshp/README.md @@ -0,0 +1,18 @@ +# pyshp Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/pyshp/filter_and_edit/code.py b/examples/pyshp/filter_and_edit/code.py new file mode 100644 index 0000000..dd16333 --- /dev/null +++ b/examples/pyshp/filter_and_edit/code.py @@ -0,0 +1,116 @@ +# --------------------------------------------------------------------- +# A realistic editing workflow: build a source shapefile, then stream +# through it filtering by attribute and bounding box, writing only the +# matching features (and only the fields we care about) to a new file. +# --------------------------------------------------------------------- + +heading("Source data: weather stations across a region") +note( + "We'll synthesize a polyline shapefile of fictional river " + "segments tagged by basin and length, then produce a derived " + "shapefile containing only the long segments in one basin." +) + +# Build the source shapefile in memory. +src_shp, src_shx, src_dbf = io.BytesIO(), io.BytesIO(), io.BytesIO() + +rivers = [ + ("Otter Brook", "North", 4.2, [(0, 0), (1, 2), (2, 5)]), + ("Heron Creek", "North", 1.1, [(2, 5), (3, 5)]), + ("Pine River", "North", 8.7, [(3, 5), (5, 6), (8, 8), (10, 9)]), + ("Willow Run", "South", 2.5, [(0, -1), (2, -2), (4, -2)]), + ("Birch Stream", "South", 6.0, [(4, -2), (7, -3), (10, -4)]), + ("Cedar Wash", "South", 0.9, [(7, -3), (8, -3)]), +] + +with shapefile.Writer(shp=src_shp, shx=src_shx, dbf=src_dbf) as writer: + writer.field("name", "C", size=40) + writer.field("basin", "C", size=10) + writer.field("length_km", "N", decimal=1) + writer.field("notes", "C", size=80) # a field we'll later drop + + for name, basin, length, coords in rivers: + writer.line([coords]) + writer.record(name, basin, length, "auto-generated sample") + +heading("Streaming filter and rewrite", level=3) +note( + "We open the source with a Reader, request only the fields we " + "need via iterShapeRecords(fields=...), and use a " + "bbox argument so the Reader's spatial index " + "skips features whose bounding boxes can't possibly match." +) + +for buf in (src_shp, src_shx, src_dbf): + buf.seek(0) + +dst_shp, dst_shx, dst_dbf = io.BytesIO(), io.BytesIO(), io.BytesIO() + +# Region of interest: a bounding box covering the eastern half. +region_bbox = [3, -5, 11, 10] +keep_fields = ["name", "basin", "length_km"] + +reader = shapefile.Reader(shp=src_shp, shx=src_shx, dbf=src_dbf) +writer = shapefile.Writer(shp=dst_shp, shx=dst_shx, dbf=dst_dbf) + +# Copy only the fields we want to keep (skip the leading DeletionFlag). +for field in reader.fields[1:]: + if field.name in keep_fields: + writer.field(*field) + +kept = [] +for shape_record in reader.iterShapeRecords( + bbox=region_bbox, fields=keep_fields, +): + record = shape_record.record + if record["basin"] == "North" and record["length_km"] >= 4.0: + writer.shape(shape_record.shape) + writer.record(*[record[name] for name in keep_fields]) + kept.append(record["name"]) + +writer.close() +reader.close() + +note(f"Kept {len(kept)} feature(s): " + ", ".join(kept)) + +# Read the new shapefile back and visualize the result. +for buf in (dst_shp, dst_shx, dst_dbf): + buf.seek(0) + +result = shapefile.Reader(shp=dst_shp, shx=dst_shx, dbf=dst_dbf) + +note( + f"Output shapefile: {result.shapeTypeName}, " + f"{len(result)} feature(s), " + f"{len(result.fields) - 1} field(s) " + "(notice the notes field was dropped)." +) + +fig, ax = plt.subplots(figsize=(7, 4)) + +# Draw the original rivers in light gray for context. +src_shp.seek(0); src_shx.seek(0); src_dbf.seek(0) +context = shapefile.Reader(shp=src_shp, shx=src_shx, dbf=src_dbf) +for shape in context.iterShapes(): + xs, ys = zip(*shape.points) + ax.plot(xs, ys, color="lightgray", linewidth=1) +context.close() + +# Overlay the surviving features in bold. +for shape_record in result.iterShapeRecords(): + xs, ys = zip(*shape_record.shape.points) + ax.plot(xs, ys, linewidth=2.5, + label=shape_record.record["name"]) + +# Show the query bounding box. +x0, y0, x1, y1 = region_bbox +ax.plot([x0, x1, x1, x0, x0], [y0, y0, y1, y1, y0], + linestyle="--", color="darkorange", label="query bbox") + +ax.set_aspect("equal") +ax.set_title("Long rivers in the North basin, within the query box") +ax.legend(loc="lower right", fontsize=8) +fig.tight_layout() +display(fig, append=True) + +result.close() diff --git a/examples/pyshp/filter_and_edit/config.toml b/examples/pyshp/filter_and_edit/config.toml new file mode 100644 index 0000000..4b2116f --- /dev/null +++ b/examples/pyshp/filter_and_edit/config.toml @@ -0,0 +1 @@ +packages = ["pyshp", "matplotlib"] diff --git a/examples/pyshp/filter_and_edit/setup.py b/examples/pyshp/filter_and_edit/setup.py new file mode 100644 index 0000000..476e2eb --- /dev/null +++ b/examples/pyshp/filter_and_edit/setup.py @@ -0,0 +1,22 @@ +"""Lightweight setup for the third example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import shapefile +import io +import matplotlib.pyplot as plt diff --git a/examples/pyshp/order.json b/examples/pyshp/order.json new file mode 100644 index 0000000..3fc1195 --- /dev/null +++ b/examples/pyshp/order.json @@ -0,0 +1,5 @@ +[ + "write_and_read_points", + "polygons_and_geojson", + "filter_and_edit" +] diff --git a/examples/pyshp/polygons_and_geojson/code.py b/examples/pyshp/polygons_and_geojson/code.py new file mode 100644 index 0000000..1b22149 --- /dev/null +++ b/examples/pyshp/polygons_and_geojson/code.py @@ -0,0 +1,89 @@ +# --------------------------------------------------------------------- +# Polygon shapefiles, holes, and GeoJSON via __geo_interface__. +# --------------------------------------------------------------------- + +heading("Tiny parks dataset") +note( + "Polygons in shapefiles must be closed (the last point repeats " + "the first), and holes are signalled by reversing the winding " + "order: outer rings clockwise, holes counterclockwise. PyShp " + "auto-closes rings if you forget the last point." +) + +shp_buf, shx_buf, dbf_buf = io.BytesIO(), io.BytesIO(), io.BytesIO() + +# A square park with a square pond cut out, plus a triangular plaza. +park_outer = [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)] +pond_hole = [(3, 3), (7, 3), (7, 7), (3, 7), (3, 3)] # counterclockwise +plaza = [(15, 2), (20, 2), (17, 8), (15, 2)] + +with shapefile.Writer(shp=shp_buf, shx=shx_buf, dbf=dbf_buf) as writer: + writer.field("name", "C", size=30) + writer.field("area_sqm", "N", decimal=1) + + # A single polygon feature with an outer ring and a hole. + writer.poly([park_outer, pond_hole]) + writer.record("Riverside Park", 84.0) + + # A separate polygon feature with no holes. + writer.poly([plaza]) + writer.record("Civic Plaza", 15.0) + +for buf in (shp_buf, shx_buf, dbf_buf): + buf.seek(0) + +reader = shapefile.Reader(shp=shp_buf, shx=shx_buf, dbf=dbf_buf) + +heading("Inspecting parts and points", level=3) +for shape_record in reader.iterShapeRecords(): + shape = shape_record.shape + name = shape_record.record["name"] + note( + f"{name}: shape type " + f"{shape.shapeTypeName}, " + f"{len(shape.points)} points across " + f"{len(shape.parts)} ring(s) " + f"(part start indices: {list(shape.parts)})." + ) + +heading("GeoJSON for free", level=3) +note( + "Every Shape, Record, and Reader implements " + "__geo_interface__, so converting to GeoJSON is " + "a one-liner that any geospatial tool can consume." +) +geojson = reader.__geo_interface__ +display(HTML( + f"
type: {geojson['type']}\\n"
+    f"features: {len(geojson['features'])}\\n"
+    f"first geometry type: {geojson['features'][0]['geometry']['type']}"
+    "
" +), append=True) + +# Plot the polygons using their GeoJSON coordinates. +fig, ax = plt.subplots(figsize=(7, 4)) +for feature in geojson["features"]: + geom = feature["geometry"] + name = feature["properties"]["name"] + # GeoJSON nests rings as [[outer, hole, ...]] for Polygons, + # and one level deeper for MultiPolygons. + polygons = ( + geom["coordinates"] + if geom["type"] == "MultiPolygon" + else [geom["coordinates"]] + ) + for rings in polygons: + outer = rings[0] + xs, ys = zip(*outer) + ax.fill(xs, ys, alpha=0.4, label=name) + for hole in rings[1:]: + hx, hy = zip(*hole) + ax.fill(hx, hy, color="white") + +ax.set_aspect("equal") +ax.set_title("Parks polygons (with a pond-shaped hole)") +ax.legend(loc="upper right") +fig.tight_layout() +display(fig, append=True) + +reader.close() diff --git a/examples/pyshp/polygons_and_geojson/config.toml b/examples/pyshp/polygons_and_geojson/config.toml new file mode 100644 index 0000000..4b2116f --- /dev/null +++ b/examples/pyshp/polygons_and_geojson/config.toml @@ -0,0 +1 @@ +packages = ["pyshp", "matplotlib"] diff --git a/examples/pyshp/polygons_and_geojson/setup.py b/examples/pyshp/polygons_and_geojson/setup.py new file mode 100644 index 0000000..855ae2e --- /dev/null +++ b/examples/pyshp/polygons_and_geojson/setup.py @@ -0,0 +1,22 @@ +"""Lightweight setup for the second example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import shapefile +import io +import matplotlib.pyplot as plt diff --git a/examples/pyshp/write_and_read_points/code.py b/examples/pyshp/write_and_read_points/code.py new file mode 100644 index 0000000..3356516 --- /dev/null +++ b/examples/pyshp/write_and_read_points/code.py @@ -0,0 +1,78 @@ +""" +A first look at PyShp: write a small Point shapefile entirely in +memory, then read it back to verify the geometry and attributes. + +PyShp is the pure-Python reader/writer for the ESRI Shapefile format. +The PyPI distribution is named `pyshp`, but the import name is +`shapefile`. Docs and source: https://github.com/GeospatialPython/pyshp +""" +from IPython.core.display import display, HTML + +heading("A handful of lighthouses") +note( + "A shapefile is really three files working together: " + ".shp (geometry), .shx (index), " + "and .dbf (attribute table). PyShp can read and " + "write each one to a file-like object, which is exactly what " + "we want in the browser." +) + +# Three buffers stand in for the three files on disk. +shp_buffer = io.BytesIO() +shx_buffer = io.BytesIO() +dbf_buffer = io.BytesIO() + +# A small dataset: famous lighthouses with their (lon, lat) coordinates. +lighthouses = [ + ("Eddystone", -4.2636, 50.1922, 1882), + ("Fastnet", -9.6033, 51.3833, 1904), + ("Cape Hatteras", -75.5290, 35.2503, 1870), + ("Tower of Hercules", -8.4063, 43.3863, 100), +] + +# Build the shapefile by streaming records into the Writer. +with shapefile.Writer( + shp=shp_buffer, shx=shx_buffer, dbf=dbf_buffer, +) as writer: + # Define the attribute schema before adding any records. + writer.field("name", "C", size=40) # text + writer.field("year", "N", decimal=0) # integer year built + + for name, lon, lat, year in lighthouses: + writer.point(lon, lat) + writer.record(name, year) + +note(f"Wrote {len(lighthouses)} point features to in-memory buffers.") + +# Reading is symmetric: hand the buffers to a Reader. +for buf in (shp_buffer, shx_buffer, dbf_buffer): + buf.seek(0) + +reader = shapefile.Reader( + shp=shp_buffer, shx=shx_buffer, dbf=dbf_buffer, +) + +heading("What did we just write?", level=3) +note( + f"Shape type: {reader.shapeTypeName}. " + f"Feature count: {len(reader)}. " + f"Bounding box: {tuple(round(c, 3) for c in reader.bbox)}." +) + +heading("Iterating shape/record pairs", level=3) +rows = [] +for shape_record in reader.iterShapeRecords(): + (lon, lat) = shape_record.shape.points[0] + name = shape_record.record["name"] + year = shape_record.record["year"] + rows.append(f"{name}{year}" + f"{lon:.3f}, {lat:.3f}") + +table = ( + "" + "" + + "".join(rows) + "
NameBuiltLon, Lat
" +) +display(HTML(table), append=True) + +reader.close() diff --git a/examples/pyshp/write_and_read_points/config.toml b/examples/pyshp/write_and_read_points/config.toml new file mode 100644 index 0000000..5a340a9 --- /dev/null +++ b/examples/pyshp/write_and_read_points/config.toml @@ -0,0 +1 @@ +packages = ["pyshp"] diff --git a/examples/pyshp/write_and_read_points/setup.py b/examples/pyshp/write_and_read_points/setup.py new file mode 100644 index 0000000..b48f338 --- /dev/null +++ b/examples/pyshp/write_and_read_points/setup.py @@ -0,0 +1,41 @@ +"""Shim setup for the first example. Includes the full IPython shim.""" +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +# pyshp is imported as the `shapefile` module. +import shapefile +import io From 27a9a75962df4d8645c6003ca8f5dbe90e5d5ff8 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 11 Jun 2026 15:48:24 +0100 Subject: [PATCH 2/2] Fix imports and final example. --- examples/pyshp/filter_and_edit/code.py | 33 ++++++---- examples/pyshp/filter_and_edit/setup.py | 4 -- examples/pyshp/polygons_and_geojson/code.py | 63 ++++++++++++++----- examples/pyshp/polygons_and_geojson/setup.py | 5 -- examples/pyshp/write_and_read_points/code.py | 4 ++ examples/pyshp/write_and_read_points/setup.py | 4 -- 6 files changed, 71 insertions(+), 42 deletions(-) diff --git a/examples/pyshp/filter_and_edit/code.py b/examples/pyshp/filter_and_edit/code.py index dd16333..f709c4f 100644 --- a/examples/pyshp/filter_and_edit/code.py +++ b/examples/pyshp/filter_and_edit/code.py @@ -3,8 +3,12 @@ # through it filtering by attribute and bounding box, writing only the # matching features (and only the fields we care about) to a new file. # --------------------------------------------------------------------- +import shapefile +import io +import matplotlib.pyplot as plt -heading("Source data: weather stations across a region") + +heading("Source data: river segments across two basins") note( "We'll synthesize a polyline shapefile of fictional river " "segments tagged by basin and length, then produce a derived " @@ -15,19 +19,19 @@ src_shp, src_shx, src_dbf = io.BytesIO(), io.BytesIO(), io.BytesIO() rivers = [ - ("Otter Brook", "North", 4.2, [(0, 0), (1, 2), (2, 5)]), - ("Heron Creek", "North", 1.1, [(2, 5), (3, 5)]), - ("Pine River", "North", 8.7, [(3, 5), (5, 6), (8, 8), (10, 9)]), - ("Willow Run", "South", 2.5, [(0, -1), (2, -2), (4, -2)]), - ("Birch Stream", "South", 6.0, [(4, -2), (7, -3), (10, -4)]), - ("Cedar Wash", "South", 0.9, [(7, -3), (8, -3)]), + ("Otter Brook", "North", 4.2, [(0, 0), (1, 2), (2, 5)]), + ("Heron Creek", "North", 1.1, [(2, 5), (3, 5)]), + ("Pine River", "North", 8.7, [(3, 5), (5, 6), (8, 8), (10, 9)]), + ("Willow Run", "South", 2.5, [(0, -1), (2, -2), (4, -2)]), + ("Birch Stream", "South", 6.0, [(4, -2), (7, -3), (10, -4)]), + ("Cedar Wash", "South", 0.9, [(7, -3), (8, -3)]), ] with shapefile.Writer(shp=src_shp, shx=src_shx, dbf=src_dbf) as writer: - writer.field("name", "C", size=40) - writer.field("basin", "C", size=10) + writer.field("name", "C", size=40) + writer.field("basin", "C", size=10) writer.field("length_km", "N", decimal=1) - writer.field("notes", "C", size=80) # a field we'll later drop + writer.field("notes", "C", size=80) # a field we'll later drop for name, basin, length, coords in rivers: writer.line([coords]) @@ -53,9 +57,12 @@ reader = shapefile.Reader(shp=src_shp, shx=src_shx, dbf=src_dbf) writer = shapefile.Writer(shp=dst_shp, shx=dst_shx, dbf=dst_dbf) -# Copy only the fields we want to keep (skip the leading DeletionFlag). +# Copy only the fields we want to keep. PyShp returns each field as a +# plain list, [name, type, size, decimal], not an object with named +# attributes -- so we index by position. The first entry is always a +# DeletionFlag, which we skip. for field in reader.fields[1:]: - if field.name in keep_fields: + if field[0] in keep_fields: writer.field(*field) kept = [] @@ -113,4 +120,4 @@ fig.tight_layout() display(fig, append=True) -result.close() +result.close() \ No newline at end of file diff --git a/examples/pyshp/filter_and_edit/setup.py b/examples/pyshp/filter_and_edit/setup.py index 476e2eb..60d91f9 100644 --- a/examples/pyshp/filter_and_edit/setup.py +++ b/examples/pyshp/filter_and_edit/setup.py @@ -16,7 +16,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import shapefile -import io -import matplotlib.pyplot as plt diff --git a/examples/pyshp/polygons_and_geojson/code.py b/examples/pyshp/polygons_and_geojson/code.py index 1b22149..b4eaaf9 100644 --- a/examples/pyshp/polygons_and_geojson/code.py +++ b/examples/pyshp/polygons_and_geojson/code.py @@ -2,24 +2,33 @@ # Polygon shapefiles, holes, and GeoJSON via __geo_interface__. # --------------------------------------------------------------------- +import shapefile +import io +import matplotlib.pyplot as plt +from matplotlib.path import Path +from matplotlib.patches import PathPatch + heading("Tiny parks dataset") note( "Polygons in shapefiles must be closed (the last point repeats " - "the first), and holes are signalled by reversing the winding " - "order: outer rings clockwise, holes counterclockwise. PyShp " - "auto-closes rings if you forget the last point." + "the first). Crucially, the shapefile format has no flag to " + "distinguish an outer ring from a hole — the only signal is " + "winding order: outer rings clockwise, holes counterclockwise. " + "Get this wrong and tools that consume the file (including " + "PyShp's own GeoJSON conversion) will misread your geometry." ) shp_buf, shx_buf, dbf_buf = io.BytesIO(), io.BytesIO(), io.BytesIO() # A square park with a square pond cut out, plus a triangular plaza. -park_outer = [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)] -pond_hole = [(3, 3), (7, 3), (7, 7), (3, 7), (3, 3)] # counterclockwise -plaza = [(15, 2), (20, 2), (17, 8), (15, 2)] +# Outer rings are clockwise; holes are counterclockwise. +park_outer = [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)] # clockwise +pond_hole = [(3, 3), (3, 7), (7, 7), (7, 3), (3, 3)] # counterclockwise +plaza = [(15, 2), (17, 8), (20, 2), (15, 2)] # clockwise with shapefile.Writer(shp=shp_buf, shx=shx_buf, dbf=dbf_buf) as writer: - writer.field("name", "C", size=30) - writer.field("area_sqm", "N", decimal=1) + writer.field("name", "C", size=30) + writer.field("area_sqm", "N", decimal=1) # A single polygon feature with an outer ring and a hole. writer.poly([park_outer, pond_hole]) @@ -60,11 +69,25 @@ "" ), append=True) +heading("Plotting polygons with real holes", level=3) +note( + "GeoJSON's nested-ring structure encodes holes semantically, but " + "matplotlib needs you to translate that into a compound " + "Path: outer ring plus inner rings as sub-paths in a " + "single PathPatch. This renders holes as genuinely " + "transparent — unlike the common shortcut of overpainting with a " + "white fill, which breaks the moment you have a non-white " + "background or layered features." +) + # Plot the polygons using their GeoJSON coordinates. fig, ax = plt.subplots(figsize=(7, 4)) -for feature in geojson["features"]: +colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] + +for i, feature in enumerate(geojson["features"]): geom = feature["geometry"] name = feature["properties"]["name"] + color = colors[i % len(colors)] # GeoJSON nests rings as [[outer, hole, ...]] for Polygons, # and one level deeper for MultiPolygons. polygons = ( @@ -73,17 +96,25 @@ else [geom["coordinates"]] ) for rings in polygons: - outer = rings[0] - xs, ys = zip(*outer) - ax.fill(xs, ys, alpha=0.4, label=name) - for hole in rings[1:]: - hx, hy = zip(*hole) - ax.fill(hx, hy, color="white") + # Build a compound path: outer ring + holes as sub-paths. + # Matplotlib renders the holes as genuinely transparent when + # the path alternates winding between outer and inner rings. + vertices = [] + codes = [] + for ring in rings: + vertices.extend(ring) + codes.append(Path.MOVETO) + codes.extend([Path.LINETO] * (len(ring) - 2)) + codes.append(Path.CLOSEPOLY) + path = Path(vertices, codes) + patch = PathPatch(path, facecolor=color, alpha=0.4, label=name) + ax.add_patch(patch) ax.set_aspect("equal") +ax.autoscale_view() ax.set_title("Parks polygons (with a pond-shaped hole)") ax.legend(loc="upper right") fig.tight_layout() display(fig, append=True) -reader.close() +reader.close() \ No newline at end of file diff --git a/examples/pyshp/polygons_and_geojson/setup.py b/examples/pyshp/polygons_and_geojson/setup.py index 855ae2e..1e05eaa 100644 --- a/examples/pyshp/polygons_and_geojson/setup.py +++ b/examples/pyshp/polygons_and_geojson/setup.py @@ -15,8 +15,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import shapefile -import io -import matplotlib.pyplot as plt diff --git a/examples/pyshp/write_and_read_points/code.py b/examples/pyshp/write_and_read_points/code.py index 3356516..09b5130 100644 --- a/examples/pyshp/write_and_read_points/code.py +++ b/examples/pyshp/write_and_read_points/code.py @@ -7,6 +7,10 @@ `shapefile`. Docs and source: https://github.com/GeospatialPython/pyshp """ from IPython.core.display import display, HTML +# pyshp is imported as the `shapefile` module. +import shapefile +import io + heading("A handful of lighthouses") note( diff --git a/examples/pyshp/write_and_read_points/setup.py b/examples/pyshp/write_and_read_points/setup.py index b48f338..17b1db0 100644 --- a/examples/pyshp/write_and_read_points/setup.py +++ b/examples/pyshp/write_and_read_points/setup.py @@ -35,7 +35,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -# pyshp is imported as the `shapefile` module. -import shapefile -import io