All examplessymbologyrenderers
§ examples · symbology

Renderers

Same ten cities rendered four different ways. Segmented control swaps the IRenderer instance on the layer — Simple / UniqueValue (by region) / ClassBreaks (by population) / Callback (size+color from multiple attributes).

slugrenderers
source172 lines
statuslive
tsexamples-src/renderers.ts
1/**
2 * Renderers — the four `IRenderer` implementations on the same 10-city
3 * point layer. Segmented control swaps the renderer at runtime; each
4 * renderer consumes the same feature attributes but produces different
5 * symbology.
6 */
7 
8import { MapView } from "mapgpu";
9import { GeoJSONLayer, RasterTileLayer } from "mapgpu/layers";
10import { RenderEngine } from "mapgpu/render";
11import {
12 CallbackRenderer,
13 ClassBreaksRenderer,
14 SimpleRenderer,
15 UniqueValueRenderer,
16} from "mapgpu/core";
17 
18import type { RunResultObject } from "@/components/examples/ExampleCanvas";
19 
20type Region = "coastal" | "inland" | "eastern";
21 
22interface CityProps {
23 name: string;
24 region: Region;
25 /** millions */
26 pop: number;
27}
28 
29// Inline data: 10 cities with region + population attributes so each
30// renderer can key off a different field.
31const CITIES = {
32 type: "FeatureCollection" as const,
33 features: [
34 { type: "Feature" as const, properties: { name: "Istanbul", region: "coastal", pop: 15.8 }, geometry: { type: "Point" as const, coordinates: [28.979, 41.015] } },
35 { type: "Feature" as const, properties: { name: "Ankara", region: "inland", pop: 5.7 }, geometry: { type: "Point" as const, coordinates: [32.866, 39.925] } },
36 { type: "Feature" as const, properties: { name: "Izmir", region: "coastal", pop: 4.4 }, geometry: { type: "Point" as const, coordinates: [27.140, 38.423] } },
37 { type: "Feature" as const, properties: { name: "Bursa", region: "inland", pop: 3.2 }, geometry: { type: "Point" as const, coordinates: [29.060, 40.188] } },
38 { type: "Feature" as const, properties: { name: "Antalya", region: "coastal", pop: 2.6 }, geometry: { type: "Point" as const, coordinates: [30.714, 36.897] } },
39 { type: "Feature" as const, properties: { name: "Konya", region: "inland", pop: 2.3 }, geometry: { type: "Point" as const, coordinates: [32.484, 37.871] } },
40 { type: "Feature" as const, properties: { name: "Gaziantep", region: "eastern", pop: 2.1 }, geometry: { type: "Point" as const, coordinates: [37.378, 37.067] } },
41 { type: "Feature" as const, properties: { name: "Diyarbakır",region: "eastern", pop: 1.8 }, geometry: { type: "Point" as const, coordinates: [40.230, 37.913] } },
42 { type: "Feature" as const, properties: { name: "Trabzon", region: "coastal", pop: 0.8 }, geometry: { type: "Point" as const, coordinates: [39.727, 41.005] } },
43 { type: "Feature" as const, properties: { name: "Erzurum", region: "eastern", pop: 0.76}, geometry: { type: "Point" as const, coordinates: [41.277, 39.904] } },
44 ],
45};
46 
47type RendererId = "simple" | "unique-value" | "class-breaks" | "callback";
48 
49function buildRenderer(kind: RendererId) {
50 switch (kind) {
51 case "simple":
52 return new SimpleRenderer({
53 type: "simple-marker",
54 color: [255, 92, 26, 240],
55 size: 9,
56 outlineColor: [255, 255, 255, 255],
57 outlineWidth: 2,
58 });
59 
60 case "unique-value":
61 // Color by region — categorical mapping via UniqueValueRenderer.
62 return new UniqueValueRenderer({
63 field: "region",
64 defaultSymbol: {
65 type: "simple-marker",
66 color: [128, 128, 128, 240],
67 size: 9,
68 outlineColor: [255, 255, 255, 255],
69 outlineWidth: 2,
70 },
71 uniqueValues: [
72 { value: "coastal", symbol: { type: "simple-marker", color: [67, 176, 255, 240], size: 9, outlineColor: [255, 255, 255, 255], outlineWidth: 2 } },
73 { value: "inland", symbol: { type: "simple-marker", color: [255, 176, 32, 240], size: 9, outlineColor: [255, 255, 255, 255], outlineWidth: 2 } },
74 { value: "eastern", symbol: { type: "simple-marker", color: [120, 220, 120, 240], size: 9, outlineColor: [255, 255, 255, 255], outlineWidth: 2 } },
75 ],
76 });
77 
78 case "class-breaks":
79 // Size by population — numeric range mapping via ClassBreaksRenderer.
80 return new ClassBreaksRenderer({
81 field: "pop",
82 defaultSymbol: {
83 type: "simple-marker",
84 color: [255, 92, 26, 240],
85 size: 6,
86 outlineColor: [255, 255, 255, 255],
87 outlineWidth: 2,
88 },
89 breaks: [
90 { min: 0, max: 1, symbol: { type: "simple-marker", color: [255, 92, 26, 240], size: 6, outlineColor: [255, 255, 255, 255], outlineWidth: 2 } },
91 { min: 1, max: 3, symbol: { type: "simple-marker", color: [255, 92, 26, 240], size: 11, outlineColor: [255, 255, 255, 255], outlineWidth: 2 } },
92 { min: 3, max: 6, symbol: { type: "simple-marker", color: [255, 92, 26, 240], size: 16, outlineColor: [255, 255, 255, 255], outlineWidth: 2 } },
93 { min: 6, max: Infinity, symbol: { type: "simple-marker", color: [255, 92, 26, 240], size: 22, outlineColor: [255, 255, 255, 255], outlineWidth: 2 } },
94 ],
95 });
96 
97 case "callback":
98 // Fully programmatic — size driven by sqrt(population), hue by
99 // longitude. Shows how to blend multiple attributes into one symbol.
100 return new CallbackRenderer((feature) => {
101 // `attributes` is `Record<string, unknown>`; narrow via unknown.
102 const p = feature.attributes as unknown as CityProps;
103 const size = 4 + Math.sqrt(p.pop) * 3.2;
104 // `geometry.coordinates` is a discriminated union; for points it
105 // is `[lon, lat]`. Narrow through an explicit cast.
106 const lon = feature.geometry.type === "Point"
107 ? (feature.geometry.coordinates as [number, number])[0]
108 : 34;
109 // Longitude 26°E → red end, 42°E → blue end (east Anatolia cooler).
110 const t = Math.max(0, Math.min(1, (lon - 26) / 16));
111 const r = Math.round(255 * (1 - t) + 80 * t);
112 const g = Math.round(120 * (1 - t) + 180 * t);
113 const b = Math.round(60 * (1 - t) + 255 * t);
114 return {
115 type: "simple-marker",
116 color: [r, g, b, 240],
117 size,
118 outlineColor: [255, 255, 255, 255],
119 outlineWidth: 2,
120 };
121 });
122 }
123}
124 
125export async function run(container: HTMLElement): Promise<RunResultObject> {
126 const view = new MapView({
127 container,
128 renderEngine: new RenderEngine(),
129 mode: "2d",
130 center: [35.0, 39.5],
131 zoom: 5,
132 minZoom: 2,
133 maxZoom: 18,
134 backgroundColor: "transparent",
135 });
136 
137 view.map.add(
138 new RasterTileLayer({
139 id: "osm",
140 urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
141 maxZoom: 19,
142 attribution: "© OpenStreetMap contributors",
143 }),
144 );
145 
146 const cities = new GeoJSONLayer({ id: "cities", data: CITIES });
147 cities.renderer = buildRenderer("simple");
148 view.map.add(cities);
149 
150 await view.when();
151 
152 return {
153 dispose: () => view.destroy(),
154 controls: [
155 {
156 kind: "segmented",
157 id: "renderer",
158 initial: "simple",
159 options: [
160 { value: "simple", label: "Simple" },
161 { value: "unique-value", label: "By region" },
162 { value: "class-breaks", label: "By population" },
163 { value: "callback", label: "Callback" },
164 ],
165 onChange: (value) => {
166 cities.renderer = buildRenderer(value as RendererId);
167 },
168 },
169 ],
170 };
171}