<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Puspad Sharma</title>
    <description>The latest articles on DEV Community by Puspad Sharma (@puspad_sharma_aa3284f890e).</description>
    <link>https://dev.to/puspad_sharma_aa3284f890e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3605128%2F737a6f13-44da-4bee-ac38-946f0df8819a.png</url>
      <title>DEV Community: Puspad Sharma</title>
      <link>https://dev.to/puspad_sharma_aa3284f890e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/puspad_sharma_aa3284f890e"/>
    <language>en</language>
    <item>
      <title>Building a Custom MapTiler Visualization Plugin for Apache Superset 3.1.0 (Complete Step-by-Step Guide)</title>
      <dc:creator>Puspad Sharma</dc:creator>
      <pubDate>Tue, 11 Nov 2025 07:58:36 +0000</pubDate>
      <link>https://dev.to/puspad_sharma_aa3284f890e/building-a-custom-maptiler-visualization-plugin-for-apache-superset-310-complete-step-by-step-24cf</link>
      <guid>https://dev.to/puspad_sharma_aa3284f890e/building-a-custom-maptiler-visualization-plugin-for-apache-superset-310-complete-step-by-step-24cf</guid>
      <description>&lt;p&gt;&lt;strong&gt;INTRODUCTION&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache Superset is a powerful open-source BI and data-visualization platform — but sometimes, you need a chart that doesn’t exist yet.&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Everything here is open, organization-neutral, and safe to reuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ENVIRONMENT&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apache Superset 3.1.0&lt;/li&gt;
&lt;li&gt;Node.js 16 or 18&lt;/li&gt;
&lt;li&gt;npm 7/8&lt;/li&gt;
&lt;li&gt;React 18&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;maplibre-gl ^3.6.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;1. Scaffold a New Plugin&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a dist/ folder — that’s your compiled plugin bundle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Register the Plugin in Superset&lt;/strong&gt;&lt;br&gt;
Add or install your plugin inside superset-frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# From &amp;lt;superset-root&amp;gt;/superset-frontend
npm i -S /absolute/path/to/superset-plugin-chart-map-tiler

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit src/visualizations/presets/MainPreset.tsx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import MapTilerPlugin from 'superset-plugin-chart-map-tiler';
new MapTilerPlugin().configure({ key: 'map-tiler' }).register();

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart the dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install
npm run dev-server

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll now see “MapTiler Map” among chart types in Explore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Define the Plugin Structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;superset-plugin-chart-map-tiler/
  package.json
  tsconfig.json
  src/
    plugin/
      index.ts
      controlPanel.ts
      buildQuery.ts
      transformProps.ts
      MapTilerChart.tsx
      types.ts

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Minimal Configuration Files&lt;/strong&gt;&lt;br&gt;
package.json&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "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" }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tsconfig.json&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "compilerOptions": {
    "target": "ES2018",
    "module": "ESNext",
    "declaration": true,
    "outDir": "dist",
    "jsx": "react-jsx",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Define Types → types.ts&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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[];
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Control Panel → controlPanel.ts&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
  ]
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Query &amp;amp; Transform Logic&lt;/strong&gt;&lt;br&gt;
buildQuery.ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { BuildQuery, QueryFormData } from '@superset-ui/core';

const buildQuery: BuildQuery = (formData: QueryFormData) =&amp;gt; ({
  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;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;transformProps.ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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&amp;lt;string, any&amp;gt;[];

  const data: MapPoint[] =
    rows.length
      ? rows.map(r =&amp;gt; ({
          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 };
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Render Map → MapTilerChart.tsx&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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 }[]) =&amp;gt; ({
  type: 'FeatureCollection',
  features: points.map(p =&amp;gt; ({
    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&amp;lt;HTMLDivElement | null&amp;gt;(null);
  const mapRef = useRef&amp;lt;Map | null&amp;gt;(null);
  const geojson = useMemo(() =&amp;gt; toGeoJSON(data), [data]);

  useEffect(() =&amp;gt; {
    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', () =&amp;gt; {
      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 () =&amp;gt; { mapRef.current?.remove(); mapRef.current = null; };
  }, []);

  useEffect(() =&amp;gt; {
    const src = mapRef.current?.getSource(sourceId) as GeoJSONSource | undefined;
    if (src) src.setData(geojson as any);
  }, [geojson]);

  return &amp;lt;div ref={ref} style={{ width, height }} /&amp;gt;;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;9. Plugin Entry → index.ts&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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: () =&amp;gt; Promise.resolve(ReactComponent), metadata, controlPanel, buildQuery, transformProps });
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;10. Try It in Explore&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create a new chart → select MapTiler Map&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Map Style URL: &lt;a href="https://api.maptiler.com/maps/streets/style.json?key=YOUR_MAPTILER_KEY" rel="noopener noreferrer"&gt;https://api.maptiler.com/maps/streets/style.json?key=YOUR_MAPTILER_KEY&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Initial Center: 0,20&lt;/li&gt;
&lt;li&gt;Initial Zoom: 2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the query → you’ll see the map render with demo points if your dataset is empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick demo dataset:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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';

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Because everything is generic and open, you can adapt this pattern for Leaflet, OpenLayers, or other map sources just by swapping the renderer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://superset.apache.org/docs/intro?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Apache Superset Docs&lt;/a&gt;&lt;br&gt;
&lt;a href="https://maplibre.org/maplibre-gl-js/docs/" rel="noopener noreferrer"&gt;MapLibre GL JS&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.maptiler.com/cloud/api/" rel="noopener noreferrer"&gt;MapTiler Docs&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/apache/superset/tree/master/superset-frontend/packages" rel="noopener noreferrer"&gt;Superset Plugin Development Guide (GitHub)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article uses only open-source tools and Replace YOUR_MAPTILER_KEY with your own free key from MapTiler Cloud&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>javascript</category>
      <category>maptiler</category>
      <category>superset</category>
    </item>
  </channel>
</rss>
