April 05, 2016
After the map tag from yesterday, here are two more plugins I built to make this blog feel like a proper GIS tool.
One is for GPX tracks — drop a file from your GPS, get a map with the full track and an elevation profile. The other is for data tables — take a CSV or GeoJSON and get a sortable table without touching JavaScript.
Both work the same way: one Liquid tag, no scripts, no includes.
Here is a GPX track from a hike at Bukit Lima in Sibu — about 4.8km with ~280m of gain:
That is just:
{% gpx_track file="/assets/datasets/demo_hike.gpx" caption="Bukit Lima Summit Trail..." %}
The map, the SVG elevation profile, and the stats (distance, gain, loss, elevation range) are all generated at build time. The elevation chart is an inline SVG — no JavaScript charting library needed.
The start and end points get circle markers so you can see which direction the track runs. The map auto-fits to the track bounds.
If you work with field data, you know how tedious it is to copy-paste attribute tables into markdown. This tag reads a CSV file and gives you a sortable table:
| site_id | name | land_cover | elevation_m | date_surveyed | species_count | notes |
|---|---|---|---|---|---|---|
| SG-01 | Bako National Park | Mangrove | 12 | 2024-03-15 | 47 | High proboscis monkey activity |
| SG-02 | Gunung Gading | Dipterocarp | 450 | 2024-04-02 | 82 | Rafflesia spotted at km 3.2 |
| SG-03 | Kubah National Park | Kerangas | 180 | 2024-04-18 | 63 | Orangutan nest count: 14 |
| SG-04 | Santubong Peninsula | Mixed dipterocarp | 310 | 2024-05-10 | 91 | Good canopy access via existing trails |
| SG-05 | Bako Terminal | Beach forest | 5 | 2024-05-22 | 28 | Erosion monitoring site |
| SG-06 | Lambir Hills | Lowland dipterocarp | 265 | 2024-06-05 | 124 | Long-term 52-ha plot |
| SG-07 | Similajau Coast | Beach | 8 | 2024-06-20 | 19 | Sea turtle landing site |
| SG-08 | Usun Apau Plateau | Montane | 980 | 2024-07-01 | 56 | Accessible by logging road only |
| SG-09 | Mulu Summit trail | Limestone forest | 1200 | 2024-07-12 | 44 | Nepenthes rajah zone |
| SG-10 | Maludam Peninsula | Peat swamp | 3 | 2024-07-25 | 35 | Rare stork colony |
Click any column header to sort. Click again to reverse. Numeric columns like elevation and species count sort by value instead of alphabetically.
You can also filter which columns to show and pick a default sort:
| site_id | name | elevation_m | species_count |
|---|---|---|---|
| SG-07 | Similajau Coast | 8 | 19 |
| SG-05 | Bako Terminal | 5 | 28 |
| SG-10 | Maludam Peninsula | 3 | 35 |
| SG-09 | Mulu Summit trail | 1200 | 44 |
| SG-01 | Bako National Park | 12 | 47 |
| SG-08 | Usun Apau Plateau | 980 | 56 |
| SG-03 | Kubah National Park | 180 | 63 |
| SG-02 | Gunung Gading | 450 | 82 |
| SG-04 | Santubong Peninsula | 310 | 91 |
| SG-06 | Lambir Hills | 265 | 124 |
The same tag works with GeoJSON — the feature properties become the columns:
| overview | order | headline | text | media | caption | credit | ||||
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | The Great Sarawak Kolo Mee Trail | <p>A (completely fictional) culinary expedition across Borneo in search of the perfect bowl of <em>kolo mee</em>. Five towns, five legends, zero calories counted. Buckle up — the noodles are calling.</p> | https://en.wikipedia.org/wiki/Kolo_mee | Warning: contains dramatised noodle facts. | Ministry of Imaginary Cuisine | ||||
| 1 | Kuching — The Origin Bowl | <p>Legend says the very first bowl of kolo mee was tossed here at 3:47 a.m. by a cat who refused to sleep. Locals still leave out a saucer of char siu in tribute. The city's stray cats are rumoured to review hawker stalls with a strict five-whisker rating system.</p> | https://en.wikipedia.org/wiki/Kuching | Cat City. Coincidence? The felines think not. | Whisker Times | Kuching | 8 | http://maps.gstatic.com/intl/en_us/mapfiles/ms/micons/blue-pushpin.png | 1 | 1908-01-01 |
| 2 | Mukah — The Sago Detour | <p>Our expedition briefly abandons noodles for <em>umai</em> and sago pearls. Scientists (imaginary ones) estimate that Mukah produces enough sago each year to build a bouncy castle the size of a small island. No one has tried. Yet.</p> | https://en.wikipedia.org/wiki/Mukah | Sago: chewy, wobbly, structurally underrated. | Institute of Bouncy Studies | Mukah | 8 | http://maps.gstatic.com/intl/en_us/mapfiles/ms/micons/blue-pushpin.png | 1 | |
| 3 | Sibu — The Kampua Rivalry | <p>Here the trail turns tense. Sibu insists its <em>kampua mee</em> is the true champion, not kolo mee. Diplomatic noodle summits are held nightly at the pasar. So far, the only treaty signed is that everyone orders a second bowl.</p> | https://en.wikipedia.org/wiki/Sibu | Neutral ground: the shared chilli sauce bottle. | Council of Noodle Affairs | Sibu | 8 | http://maps.gstatic.com/intl/en_us/mapfiles/ms/micons/blue-pushpin.png | 1 | |
| 4 | Bintulu — Refuel at the Energy Town | <p>Powered by natural gas and, apparently, unlimited belacan, Bintulu is where our travellers refuel — both the van and themselves. Local myth claims a bowl of noodles here gives you enough energy to walk to Miri. It does not. We checked.</p> | https://en.wikipedia.org/wiki/Bintulu | Fuel gauge: full. Stomach gauge: fuller. | Department of Optimistic Distances | Bintulu | 8 | http://maps.gstatic.com/intl/en_us/mapfiles/ms/micons/blue-pushpin.png | 1 | |
| 5 | Miri — The Final Bowl | <p>The trail ends where it began, in oil-town Miri, with a sunset, a seahorse mascot, and the biggest bowl of kolo mee anyone dared to order. Verdict after 1,200 km: they are all the best. This is the only scientifically imaginary conclusion possible.</p> | https://en.wikipedia.org/wiki/Miri,_Malaysia | The Seahorse City approves this expedition. | Bureau of Happy Endings | Miri | 8 | http://maps.gstatic.com/intl/en_us/mapfiles/ms/micons/blue-pushpin.png | 1 |
At this point the blog has six custom Liquid tags covering pretty much everything I need for GIS writing:
| Tag | Takes | Gives you |
|---|---|---|
{% map %} | lat/lon | Leaflet marker map |
{% storymap %} | GeoJSON/CSV/JSON | KnightLab StoryMapJS |
{% lungkui %} | GeoJSON/JSON | lungkui.js MapLibre scrollytelling |
{% gpx_track %} | GPX file | Leaflet track + SVG profile + stats |
{% datatable %} | GeoJSON/CSV | Sortable HTML table |
No webpack, no npm, no node_modules. Everything that can be computed at build time — GeoJSON transforms, CSV parsing, SVG generation, GPX stats, Haversine distances — is. The only runtime dependencies are Leaflet and MapLibre from CDN, and those are loaded once per page.
For a solo GIS blog, this is about as good as it gets.