blob: 875ef5c4c02f795aa8f145b413fbce4c5d4ee4fc [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Layer from 'ol/layer/Layer';
import { FrameState } from 'ol/Map';
import { apply as applyTransform } from 'ol/transform';
import ReactDOM from 'react-dom';
import { SupersetTheme } from '@superset-ui/core';
import { ChartConfig, ChartLayerOptions, ChartSizeValues } from '../types';
import { createChartComponent } from '../util/chartUtil';
import { getProjectedCoordinateFromPointGeoJson } from '../util/geometryUtil';
import Loader from '../images/loading.gif';
/**
* Custom OpenLayers layer that displays charts on given locations.
*/
export class ChartLayer extends Layer {
charts: any[] = [];
chartConfigs: ChartConfig = {
type: 'FeatureCollection',
features: [],
};
chartSizeValues: ChartSizeValues = {};
chartVizType: string;
div: HTMLDivElement;
loadingMask: HTMLDivElement;
chartBackgroundCssColor = '';
chartBackgroundBorderRadius = 0;
theme: SupersetTheme;
locale: string;
/**
* Create a ChartLayer.
*
* @param {ChartLayerOptions} options The options to create a ChartLayer
* @param {ChartHtmlElement[]} options.charts An array with the chart objects containing the HTML element and the coordinate
* @param {ChartConfig} options.chartConfigs The chart configuration for the charts
* @param {ChartSizeValues} options.chartSizeValues The values for the chart sizes
* @param {String} options.chartVizType The viztype of the charts
* @param {String} options.chartBackgroundCssColor The color of the additionally added chart background
* @param {Number} options.chartBackgroundBorderRadius The border radius in percent of the additionally added chart background
* @param {Function} options.onMouseOver The handler function to execute when the mouse entering a HTML element
* @param {Function} options.onMouseOut The handler function to execute when the mouse leaves a HTML element
* @param {SupersetTheme} options.theme The superset theme
*/
constructor(options: ChartLayerOptions) {
super(options);
this.chartVizType = options.chartVizType;
if (options.chartConfigs) {
this.chartConfigs = options.chartConfigs;
}
if (options.chartSizeValues) {
this.chartSizeValues = options.chartSizeValues;
}
if (options.chartBackgroundCssColor) {
this.chartBackgroundCssColor = options.chartBackgroundCssColor;
}
if (options.chartBackgroundBorderRadius) {
this.chartBackgroundBorderRadius = options.chartBackgroundBorderRadius;
}
if (options.theme) {
this.theme = options.theme;
}
if (options.locale) {
this.locale = options.locale;
}
const spinner = document.createElement('img');
spinner.src = Loader;
spinner.style.position = 'relative';
spinner.style.width = '50px';
spinner.style.top = '50%';
spinner.style.left = '50%';
spinner.style.transform = 'translate(-50%, -50%)';
this.loadingMask = document.createElement('div');
this.loadingMask.style.position = 'relative';
this.loadingMask.style.height = '100%';
this.loadingMask.appendChild(spinner);
this.div = document.createElement('div');
// TODO: consider creating an OpenLayers event
if (options.onMouseOver) {
this.div.onmouseover = options.onMouseOver;
}
// TODO: consider creating an OpenLayers event
if (options.onMouseOut) {
this.div.onmouseout = options.onMouseOut;
}
}
setChartConfig(chartConfigs: ChartConfig, silent = false) {
this.chartConfigs = chartConfigs;
if (!silent) {
this.changed();
}
}
setChartVizType(chartVizType: string, silent = false) {
this.chartVizType = chartVizType;
if (!silent) {
this.changed();
}
}
setChartSizeValues(chartSizeValues: ChartSizeValues, silent = false) {
this.chartSizeValues = chartSizeValues;
if (!silent) {
this.changed();
}
}
setChartBackgroundCssColor(chartBackgroundCssColor: string, silent = false) {
this.chartBackgroundCssColor = chartBackgroundCssColor;
if (!silent) {
this.changed();
}
}
setChartBackgroundBorderRadius(
chartBackgroundBorderRadius: number,
silent = false,
) {
this.chartBackgroundBorderRadius = chartBackgroundBorderRadius;
if (!silent) {
this.changed();
}
}
/**
* Unmount and remove all created chart elements from the DOM.
*/
removeAllChartElements() {
this.charts.forEach(chart => {
ReactDOM.unmountComponentAtNode(chart.htmlElement);
chart.htmlElement.remove();
});
this.charts = [];
}
createCharts(zoom: number) {
const charts = this.chartConfigs.features.map(feature => {
const container = document.createElement('div');
let chartWidth = 0;
let chartHeight = 0;
if (this.chartSizeValues[zoom]) {
chartWidth = this.chartSizeValues[zoom].width;
chartHeight = this.chartSizeValues[zoom].height;
}
const chartComponent = createChartComponent(
this.chartVizType,
feature,
chartWidth,
chartHeight,
this.theme,
this.locale,
);
ReactDOM.render(chartComponent, container);
return {
htmlElement: container,
coordinate: getProjectedCoordinateFromPointGeoJson(feature.geometry),
width: chartWidth,
height: chartHeight,
feature,
};
});
this.charts = charts;
}
updateCharts(zoom: number) {
const charts = this.charts.map(chart => {
let chartWidth = 0;
let chartHeight = 0;
if (this.chartSizeValues[zoom]) {
chartWidth = this.chartSizeValues[zoom].width;
chartHeight = this.chartSizeValues[zoom].height;
}
// only rerender chart if size changes
if (chartWidth === chart.width && chartHeight === chart.height) {
return chart;
}
const chartComponent = createChartComponent(
this.chartVizType,
chart.feature,
chartWidth,
chartHeight,
this.theme,
this.locale,
);
ReactDOM.render(chartComponent, chart.htmlElement);
return {
...chart,
width: chartWidth,
height: chartHeight,
};
});
this.charts = charts;
}
render(frameState: FrameState | null) {
if (!frameState) {
return this.div;
}
const { viewState } = frameState;
const currentZoom = Math.round(viewState.zoom);
// nextResolution is only defined while an animation
// is in action. For this time we show a loading mask
// to keep the amount of chart rerenderings as low as possible.
if (viewState.nextResolution) {
return this.loadingMask;
}
if (this.charts.length === 0) {
this.createCharts(currentZoom);
} else {
this.updateCharts(currentZoom);
}
this.charts.forEach(chartObject => {
const { htmlElement, coordinate, width, height } = chartObject;
// clone, because applyTransform modifies in place
const coordCopy = [...coordinate];
const [x, y] = applyTransform(
frameState.coordinateToPixelTransform,
coordCopy,
);
// left and top are corrected to place the center of the chart to its location
htmlElement.style.left = `${x - width / 2}px`;
htmlElement.style.top = `${y - height / 2}px`;
htmlElement.style.position = 'absolute';
htmlElement.style['background-color' as any] =
this.chartBackgroundCssColor;
htmlElement.style['border-radius' as any] =
`${this.chartBackgroundBorderRadius}%`;
});
// TODO should we always replace the html elements or is there a better way?
const htmlElements = this.charts.map(c => c.htmlElement);
this.div.replaceChildren(...htmlElements);
return this.div;
}
}