INTRODUCTION
Apache Superset is a powerful open-source BI and data-visualization platform — but sometimes, you need a chart that doesn’t exist yet.
In this article, I’ll walk you through creating a custom visualization plugin for Apache Superset 3.1.0 that renders a MapLibre GL map using a MapTiler style.json.
We will build it completely from scratch with React + TypeScript, and include a fake-data fallback so the map still renders even if your dataset is empty.
Everything here is open, organization-neutral, and safe to reuse.
ENVIRONMENT
- Apache Superset 3.1.0
- Node.js 16 or 18
- npm 7/8
- React 18
- TypeScript
- maplibre-gl ^3.6.0
1. Scaffold a New Plugin
npm i -g yo
mkdir superset-plugin-chart-map-tiler
cd superset-plugin-chart-map-tiler
yo @superset-ui/superset
# Answer prompts:
# Package name: superset-plugin-chart-map-tiler
# Description: MapTiler Map
# Type: Regular chart
npm install
npm run build
You should see a dist/ folder — that’s your compiled plugin bundle.
2. Register the Plugin in Superset
Add or install your plugin inside superset-frontend:
# From <superset-root>/superset-frontend
npm i -S /absolute/path/to/superset-plugin-chart-map-tiler
Edit src/visualizations/presets/MainPreset.tsx:
import MapTilerPlugin from 'superset-plugin-chart-map-tiler';
new MapTilerPlugin().configure({ key: 'map-tiler' }).register();
Restart the dev server:
npm install
npm run dev-server
You’ll now see “MapTiler Map” among chart types in Explore.
3. Define the Plugin Structure
superset-plugin-chart-map-tiler/
package.json
tsconfig.json
src/
plugin/
index.ts
controlPanel.ts
buildQuery.ts
transformProps.ts
MapTilerChart.tsx
types.ts
4. Minimal Configuration Files
package.json
{
"name": "superset-plugin-chart-map-tiler",
"version": "0.1.0",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsc -w -p tsconfig.json"
},
"peerDependencies": {
"@superset-ui/chart-controls": "^3",
"@superset-ui/core": "^3",
"react": "^18",
"react-dom": "^18"
},
"dependencies": { "maplibre-gl": "^3.6.0" }
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"declaration": true,
"outDir": "dist",
"jsx": "react-jsx",
"moduleResolution": "Node",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
5. Define Types → types.ts
export type MapTilerFormData = {
mapStyleUrl: string;
initialZoom: number;
initialCenter: [number, number];
latColumn?: string;
lonColumn?: string;
labelColumn?: string;
};
export type MapPoint = { lon: number; lat: number; label?: string };
export type MapTilerProps = {
width: number;
height: number;
formData: MapTilerFormData;
data: MapPoint[];
};
6. Control Panel → controlPanel.ts
import { t, validateNumber } from '@superset-ui/core';
import { sections } from '@superset-ui/chart-controls';
export default {
controlPanelSections: [
{
label: t('Map'),
expanded: true,
controlSetRows: [
[{
name: 'mapStyleUrl',
config: {
type: 'TextControl',
label: t('Map Style URL'),
description: t('MapTiler style.json URL'),
default: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_MAPTILER_KEY'
},
}],
[{
name: 'initialCenter',
config: {
type: 'TextControl',
label: t('Initial Center [lng,lat]'),
default: '0,20',
renderTrigger: true
},
}],
[{
name: 'initialZoom',
config: {
type: 'TextControl',
label: t('Initial Zoom'),
default: '2',
renderTrigger: true,
validators: [validateNumber]
},
}]
]
},
{
label: t('Data'),
expanded: true,
controlSetRows: [
[{ name: 'latColumn', config: { type: 'SelectControl', freeForm: true, label: t('Latitude Column'), default: 'lat' } }],
[{ name: 'lonColumn', config: { type: 'SelectControl', freeForm: true, label: t('Longitude Column'), default: 'lon' } }],
[{ name: 'labelColumn',config: { type: 'SelectControl', freeForm: true, label: t('Label Column'), default: 'name' } }],
]
},
sections.annotationsAndLayersSection
]
};
7. Query & Transform Logic
buildQuery.ts
import { BuildQuery, QueryFormData } from '@superset-ui/core';
const buildQuery: BuildQuery = (formData: QueryFormData) => ({
queries: [{
...formData,
columns: [
(formData as any)['lonColumn'] || 'lon',
(formData as any)['latColumn'] || 'lat',
(formData as any)['labelColumn'] || 'name'
],
metrics: [],
orderby: [],
filters: [],
time_range: 'No filter'
}]
});
export default buildQuery;
transformProps.ts
import { ChartProps } from '@superset-ui/core';
import { MapPoint, MapTilerFormData, MapTilerProps } from './types';
export default function transformProps(chartProps: ChartProps): MapTilerProps {
const { width, height, formData, queriesData } = chartProps;
const fd = formData as any as MapTilerFormData;
const center: [number, number] = String(fd.initialCenter || '0,0').split(',').map(Number) as any;
const rows = (queriesData?.[0]?.data || []) as Record<string, any>[];
const data: MapPoint[] =
rows.length
? rows.map(r => ({
lon: Number(r[fd.lonColumn || 'lon']),
lat: Number(r[fd.latColumn || 'lat']),
label: r[fd.labelColumn || 'name']
}))
: [
{ lon: 0, lat: 20, label: 'Demo A' },
{ lon: 12.5, lat: 41.9, label: 'Demo B' },
{ lon: 103.8, lat: 1.35, label: 'Demo C' }
];
return { width, height, formData: { ...fd, initialCenter: center, initialZoom: +fd.initialZoom || 2 }, data };
}
8. Render Map → MapTilerChart.tsx
import React, { useEffect, useMemo, useRef } from 'react';
import maplibregl, { Map, GeoJSONSource } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { MapTilerProps } from './types';
const sourceId = 'points-source';
const layerId = 'points-layer';
const toGeoJSON = (points: { lon: number; lat: number; label?: string }[]) => ({
type: 'FeatureCollection',
features: points.map(p => ({
type: 'Feature',
properties: { label: p.label || '' },
geometry: { type: 'Point', coordinates: [p.lon, p.lat] }
}))
});
export default function MapTilerChart({ width, height, formData, data }: MapTilerProps) {
const ref = useRef<HTMLDivElement | null>(null);
const mapRef = useRef<Map | null>(null);
const geojson = useMemo(() => toGeoJSON(data), [data]);
useEffect(() => {
if (!ref.current || mapRef.current) return;
mapRef.current = new maplibregl.Map({
container: ref.current,
style: formData.mapStyleUrl,
center: formData.initialCenter,
zoom: formData.initialZoom,
});
mapRef.current.on('load', () => {
const map = mapRef.current!;
map.addSource(sourceId, { type: 'geojson', data: geojson as any });
map.addLayer({
id: layerId,
type: 'circle',
source: sourceId,
paint: { 'circle-radius': 6, 'circle-stroke-width': 1, 'circle-stroke-color': '#000', 'circle-color': '#3bb2d0' }
});
});
return () => { mapRef.current?.remove(); mapRef.current = null; };
}, []);
useEffect(() => {
const src = mapRef.current?.getSource(sourceId) as GeoJSONSource | undefined;
if (src) src.setData(geojson as any);
}, [geojson]);
return <div ref={ref} style={{ width, height }} />;
}
9. Plugin Entry → index.ts
import { ChartPlugin, ChartMetadata, t } from '@superset-ui/core';
import controlPanel from './controlPanel';
import buildQuery from './buildQuery';
import transformProps from './transformProps';
import ReactComponent from './MapTilerChart';
const metadata = new ChartMetadata({
name: t('MapTiler Map'),
thumbnail: '',
description: t('Render a MapLibre map using a MapTiler style and point features'),
tags: ['Map', 'MapLibre', 'MapTiler']
});
export default class MapTilerPlugin extends ChartPlugin {
constructor() {
super({ loadChart: () => Promise.resolve(ReactComponent), metadata, controlPanel, buildQuery, transformProps });
}
}
10. Try It in Explore
Create a new chart → select MapTiler Map
Set
- Map Style URL: https://api.maptiler.com/maps/streets/style.json?key=YOUR_MAPTILER_KEY
- Initial Center: 0,20
- Initial Zoom: 2
Run the query → you’ll see the map render with demo points if your dataset is empty.
Quick demo dataset:
SELECT 0 AS lon, 20 AS lat, 'Demo A' AS name
UNION ALL SELECT 12.5, 41.9, 'Demo B'
UNION ALL SELECT 103.8, 1.35, 'Demo C';
Conclusion
You now have a fully functional MapTiler visualization plugin for Superset 3.1.0 — complete with a MapLibre GL map, configurable controls, and a fake-data fallback.
Because everything is generic and open, you can adapt this pattern for Leaflet, OpenLayers, or other map sources just by swapping the renderer.
Resources:
Apache Superset Docs
MapLibre GL JS
MapTiler Docs
Superset Plugin Development Guide (GitHub)
Note
This article uses only open-source tools and Replace YOUR_MAPTILER_KEY with your own free key from MapTiler Cloud
Top comments (0)