@svene/geosvg
v0.1.0
Published
Headless GeoJSON LineString and MultiLineString to SVG engine.
Readme
@svene/geosvg
@svene/geosvg converts GeoJSON line geometry to SVG path records and SVG strings. It handles LineString, MultiLineString, and line geometry inside nested GeometryCollection values.
The package has no DOM dependency. The main entry works in Node 18+ and bundlers. The /browser entry provides a dependency-bundled ESM build for browsers.
Install
pnpm add @svene/geosvgQuick start
import { toSvg } from "@svene/geosvg";
const { svg } = toSvg(geojson, {
width: 1200,
height: 800,
padding: 64
});toSvg() returns the serialized SVG, the normalized line document, and the rendered scene:
{
svg: string;
document: LineDocument;
scene: SvgScene;
}If you only need a file, use svg. If your application needs line IDs, diagnostics, properties, or path data, keep document or scene.
Work with each stage
The same conversion is available as three functions:
import { normalize, layout, serializeSvg } from "@svene/geosvg";
const document = normalize(geojson, {
sourceName: "routes.geojson"
});
const scene = layout(document, {
width: 1200,
height: 800,
coordinates: "geographic",
style(line) {
const color = line.properties.color;
if (typeof color === "string") {
return { stroke: color, strokeWidth: 4 };
}
return { stroke: "#111111", strokeWidth: 4 };
}
});
const svg = serializeSvg(scene, {
background: null,
metadata: false,
pretty: true
});scene.paths contains the final d values and styles. A UI can render those records directly without parsing the SVG string.
Input
normalize() accepts parsed GeoJSON. It keeps line geometry and reports unsupported or unusable geometry through document.diagnostics.
Normalization records the source location of each line:
interface LineSource {
featureIndex: number;
partIndex: number;
geometryPath: readonly number[];
}geometryPath contains indexes inside nested GeometryCollection values. partIndex identifies a part of a MultiLineString.
Generated IDs come from that source location:
f0
f1-p1
f2-g0
f2-g1-g0-p1Normalization does not use UUIDs or timestamps.
Missing names
GeoJSON does not require a feature name. When a line has no usable name, normalize() checks these values in order:
name,title,label, and route-name properties- origin and destination properties
- a route identifier with a direction
- a route identifier
- the GeoJSON feature ID
- the first useful primitive property
- the source filename with the line number
- the line number
unknown, unnamed, null, and N/A are ignored as names. Duplicate generated names receive a deterministic qualifier.
You can supply the first naming rule:
const document = normalize(geojson, {
getName(line) {
const value = line.properties.my_name;
if (typeof value === "string") {
return value;
}
return undefined;
}
});If getName() returns a blank string, null, or undefined, the built-in naming rules continue.
Rendering
The default coordinate mode treats positions as RFC 7946 longitude and latitude and projects them with Mercator:
layout(document, {
coordinates: "geographic"
});For schematic or other planar coordinates, use raw mode:
layout(document, {
coordinates: "raw",
reflectY: true
});The package does not infer a coordinate reference system.
A single style object applies to every line:
layout(document, {
style: {
stroke: "#222222",
strokeWidth: 3,
strokeOpacity: 1,
lineCap: "round",
lineJoin: "round"
}
});A style function can vary the result by line:
layout(document, {
style(line) {
if (line.properties.kind === "express") {
return { stroke: "#ef4444" };
}
return { stroke: "#3b82f6" };
}
});Route categories and color rules stay in the calling application.
Validate JSON text
normalize() expects an object. If your input starts as JSON text, parse and validate it through the /validate entry:
import { parseGeoJSON } from "@svene/geosvg/validate";
const geojson = parseGeoJSON(fileText);parseGeoJSON() throws GeoJSONParseError for invalid JSON or invalid GeoJSON.
To get validation issues without throwing:
import { validateGeoJSON } from "@svene/geosvg/validate";
const issues = validateGeoJSON(fileText);Each issue has this shape:
interface ValidationIssue {
code: "INVALID_JSON" | "INVALID_GEOJSON";
message: string;
offset?: number;
line?: number;
column?: number;
}The validator package's own error types are not part of this package's public API.
Diagnostics
document.diagnostics can contain these codes:
UNSUPPORTED_GEOMETRY
EMPTY_GEOMETRY
INVALID_LINE
AUTO_NAMED
DUPLICATE_NAME
DEGENERATE_GEOMETRYUse diagnostic.code in application logic. Messages are text for people and can change between releases.
A Polygon, for example, produces UNSUPPORTED_GEOMETRY. Polygon rings are not converted to lines.
SVG output
Metadata export is off by default. Set metadata: true to write primitive GeoJSON properties as escaped data-* attributes:
const svg = serializeSvg(scene, {
metadata: true
});Objects and arrays are not written as metadata attributes. If two property names normalize to the same data-* name, the serializer adds numeric suffixes to keep the attribute names unique.
For the same input, options, and package version, line order, generated IDs, metadata order, numeric precision, and SVG attribute order stay stable.
Normalization copies line coordinates and JSON properties. Rendering does not mutate the GeoJSON object passed by the caller.
serializeSvg() escapes XML text and metadata values. It also checks a manually created SvgScene before serialization and rejects invalid dimensions, path values, opacity values, line caps, and line joins.
Browser entry
import { toSvg } from "@svene/geosvg/browser";The browser entry bundles the rendering dependency. It does not include the /validate entry.
Package scope
This package converts GeoJSON line geometry to SVG. It does not include:
- basemaps or map tiles
- route editing
- pan and zoom controls
- file pickers or drag and drop
- downloads or clipboard access
- ZIP creation
- geometry simplification
- coordinate-system detection
- selection state
Those features belong in the application that uses the package.
Development
Install dependencies:
pnpm installRun the package checks:
pnpm checkpnpm check runs TypeScript, tests, the package build, built-entry smoke tests, publint, and @arethetypeswrong/cli.
