import { FieldType, getFieldColorModeForField, GrafanaTheme2, MapLayerOptions, MapLayerRegistryItem, PanelData, } from '@grafana/data'; import Map from 'ol/Map'; import Feature from 'ol/Feature'; import * as layer from 'ol/layer'; import * as source from 'ol/source'; import { dataFrameToPoints, getLocationMatchers } from '../../utils/location'; import { ScaleDimensionConfig, getScaledDimension } from 'app/features/dimensions'; import { ScaleDimensionEditor } from 'app/features/dimensions/editors'; // Configuration options for Heatmap overlays export interface HeatmapConfig { weight: ScaleDimensionConfig; blur: number; radius: number; } const defaultOptions: HeatmapConfig = { weight: { fixed: 1, min: 0, max: 1, }, blur: 15, radius: 5, }; /** * Map layer configuration for heatmap overlay */ export const heatmapLayer: MapLayerRegistryItem = { id: 'heatmap', name: 'Heatmap', description: 'visualizes a heatmap of the data', isBaseMap: false, showLocation: true, /** * Function that configures transformation and returns a transformer * @param options */ create: async (map: Map, options: MapLayerOptions, theme: GrafanaTheme2) => { const config = { ...defaultOptions, ...options.config }; const matchers = await getLocationMatchers(options.location); const vectorSource = new source.Vector(); // Create a new Heatmap layer // Weight function takes a feature as attribute and returns a normalized weight value const vectorLayer = new layer.Heatmap({ source: vectorSource, blur: config.blur, radius: config.radius, weight: function (feature) { var weight = feature.get('value'); return weight; }, }); return { init: () => vectorLayer, update: (data: PanelData) => { const frame = data.series[0]; // Remove previous data before updating const features = vectorLayer.getSource().getFeatures(); features.forEach((feature) => { vectorLayer.getSource().removeFeature(feature); }); if (!frame) { return; } // Get data points (latitude and longitude coordinates) const info = dataFrameToPoints(frame, matchers); if (info.warning) { console.log('WARN', info.warning); return; // ??? } const weightDim = getScaledDimension(frame, config.weight); // Map each data value into new points for (let i = 0; i < frame.length; i++) { const cluster = new Feature({ geometry: info.points[i], value: weightDim.get(i), }); vectorSource.addFeature(cluster); } vectorLayer.setSource(vectorSource); // Set heatmap gradient colors let colors = ['#00f', '#0ff', '#0f0', '#ff0', '#f00']; // Either the configured field or the first numeric field value const field = weightDim.field ?? frame.fields.find((field) => field.type === FieldType.number); if (field) { const colorMode = getFieldColorModeForField(field); if (colorMode.isContinuous && colorMode.getColors) { // getColors return an array of color string from the color scheme chosen colors = colorMode.getColors(theme); } } vectorLayer.setGradient(colors); }, // Heatmap overlay options registerOptionsUI: (builder) => { builder .addCustomEditor({ id: 'config.weight', path: 'config.weight', name: 'Weight values', description: 'Scale the distribution for each row', editor: ScaleDimensionEditor, settings: { min: 0, // no contribution max: 1, hideRange: true, // Don't show the scale factor }, defaultValue: { // Configured values fixed: 1, min: 0, max: 1, }, }) .addSliderInput({ path: 'config.radius', description: 'configures the size of clusters', name: 'Radius', defaultValue: defaultOptions.radius, settings: { min: 1, max: 50, step: 1, }, }) .addSliderInput({ path: 'config.blur', description: 'configures the amount of blur of clusters', name: 'Blur', defaultValue: defaultOptions.blur, settings: { min: 1, max: 50, step: 1, }, }); }, }; }, // fill in the default values defaultOptions, };