5187 lines
230 KiB
JavaScript
5187 lines
230 KiB
JavaScript
/**
|
|
* @license Highcharts JS v11.1.0 (2023-06-05)
|
|
*
|
|
* (c) 2009-2022
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*/
|
|
(function (factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
factory['default'] = factory;
|
|
module.exports = factory;
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
define('highcharts/modules/flowmap', ['highcharts'], function (Highcharts) {
|
|
factory(Highcharts);
|
|
factory.Highcharts = Highcharts;
|
|
return factory;
|
|
});
|
|
} else {
|
|
factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined);
|
|
}
|
|
}(function (Highcharts) {
|
|
'use strict';
|
|
var _modules = Highcharts ? Highcharts._modules : {};
|
|
function _registerModule(obj, path, args, fn) {
|
|
if (!obj.hasOwnProperty(path)) {
|
|
obj[path] = fn.apply(null, args);
|
|
|
|
if (typeof CustomEvent === 'function') {
|
|
window.dispatchEvent(
|
|
new CustomEvent(
|
|
'HighchartsModuleLoaded',
|
|
{ detail: { path: path, module: obj[path] }
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
_registerModule(_modules, 'Series/FlowMap/FlowMapPoint.js', [_modules['Core/Series/SeriesRegistry.js'], _modules['Core/Utilities.js']], function (SeriesRegistry, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2022 Askel Eirik Johansson, Piotr Madej
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { seriesTypes: { mapline: { prototype: { pointClass: MapLinePoint } } } } = SeriesRegistry;
|
|
const { pick, isString, isNumber } = U;
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
class FlowMapPoint extends MapLinePoint {
|
|
constructor() {
|
|
/* *
|
|
*
|
|
* Properties
|
|
*
|
|
* */
|
|
super(...arguments);
|
|
this.options = void 0;
|
|
this.series = void 0;
|
|
}
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
/**
|
|
* @private
|
|
*/
|
|
isValid() {
|
|
let valid = !!(this.options.to && this.options.from);
|
|
[this.options.to, this.options.from]
|
|
.forEach(function (toOrFrom) {
|
|
valid = !!(valid && (toOrFrom && (isString(toOrFrom) || ( // point id or has lat/lon coords
|
|
isNumber(pick(toOrFrom[0], toOrFrom.lat)) &&
|
|
isNumber(pick(toOrFrom[1], toOrFrom.lon))))));
|
|
});
|
|
return valid;
|
|
}
|
|
}
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return FlowMapPoint;
|
|
});
|
|
_registerModule(_modules, 'Series/ColorMapComposition.js', [_modules['Core/Series/SeriesRegistry.js'], _modules['Core/Utilities.js']], function (SeriesRegistry, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { column: { prototype: columnProto } } = SeriesRegistry.seriesTypes;
|
|
const { addEvent, defined } = U;
|
|
/* *
|
|
*
|
|
* Composition
|
|
*
|
|
* */
|
|
var ColorMapComposition;
|
|
(function (ColorMapComposition) {
|
|
/* *
|
|
*
|
|
* Constants
|
|
*
|
|
* */
|
|
const composedMembers = [];
|
|
ColorMapComposition.pointMembers = {
|
|
dataLabelOnNull: true,
|
|
moveToTopOnHover: true,
|
|
isValid: pointIsValid
|
|
};
|
|
ColorMapComposition.seriesMembers = {
|
|
colorKey: 'value',
|
|
axisTypes: ['xAxis', 'yAxis', 'colorAxis'],
|
|
parallelArrays: ['x', 'y', 'value'],
|
|
pointArrayMap: ['value'],
|
|
trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
|
|
colorAttribs: seriesColorAttribs,
|
|
pointAttribs: columnProto.pointAttribs
|
|
};
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
/**
|
|
* @private
|
|
*/
|
|
function compose(SeriesClass) {
|
|
const PointClass = SeriesClass.prototype.pointClass;
|
|
if (U.pushUnique(composedMembers, PointClass)) {
|
|
addEvent(PointClass, 'afterSetState', onPointAfterSetState);
|
|
}
|
|
return SeriesClass;
|
|
}
|
|
ColorMapComposition.compose = compose;
|
|
/**
|
|
* Move points to the top of the z-index order when hovered.
|
|
* @private
|
|
*/
|
|
function onPointAfterSetState(e) {
|
|
const point = this;
|
|
if (point.moveToTopOnHover && point.graphic) {
|
|
point.graphic.attr({
|
|
zIndex: e && e.state === 'hover' ? 1 : 0
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Color points have a value option that determines whether or not it is
|
|
* a null point
|
|
* @private
|
|
*/
|
|
function pointIsValid() {
|
|
return (this.value !== null &&
|
|
this.value !== Infinity &&
|
|
this.value !== -Infinity &&
|
|
// undefined is allowed, but NaN is not (#17279)
|
|
(this.value === void 0 || !isNaN(this.value)));
|
|
}
|
|
/**
|
|
* Get the color attibutes to apply on the graphic
|
|
* @private
|
|
* @function Highcharts.colorMapSeriesMixin.colorAttribs
|
|
* @param {Highcharts.Point} point
|
|
* @return {Highcharts.SVGAttributes}
|
|
* The SVG attributes
|
|
*/
|
|
function seriesColorAttribs(point) {
|
|
const ret = {};
|
|
if (defined(point.color) &&
|
|
(!point.state || point.state === 'normal') // #15746
|
|
) {
|
|
ret[this.colorProp || 'fill'] = point.color;
|
|
}
|
|
return ret;
|
|
}
|
|
})(ColorMapComposition || (ColorMapComposition = {}));
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return ColorMapComposition;
|
|
});
|
|
_registerModule(_modules, 'Maps/MapSymbols.js', [_modules['Core/Renderer/SVG/SVGRenderer.js']], function (SVGRenderer) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { prototype: { symbols } } = SVGRenderer;
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
/* eslint-disable require-jsdoc, valid-jsdoc */
|
|
function bottomButton(x, y, w, h, options) {
|
|
if (options) {
|
|
const r = (options === null || options === void 0 ? void 0 : options.r) || 0;
|
|
options.brBoxY = y - r;
|
|
options.brBoxHeight = h + r;
|
|
}
|
|
return symbols.roundedRect(x, y, w, h, options);
|
|
}
|
|
function topButton(x, y, w, h, options) {
|
|
if (options) {
|
|
const r = (options === null || options === void 0 ? void 0 : options.r) || 0;
|
|
options.brBoxHeight = h + r;
|
|
}
|
|
return symbols.roundedRect(x, y, w, h, options);
|
|
}
|
|
symbols.bottombutton = bottomButton;
|
|
symbols.topbutton = topButton;
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return symbols;
|
|
});
|
|
_registerModule(_modules, 'Core/Chart/MapChart.js', [_modules['Core/Chart/Chart.js'], _modules['Core/Defaults.js'], _modules['Core/Renderer/SVG/SVGRenderer.js'], _modules['Core/Utilities.js']], function (Chart, D, SVGRenderer, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { getOptions } = D;
|
|
const { merge, pick } = U;
|
|
/**
|
|
* Map-optimized chart. Use {@link Highcharts.Chart|Chart} for common charts.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @class
|
|
* @name Highcharts.MapChart
|
|
* @extends Highcharts.Chart
|
|
*/
|
|
class MapChart extends Chart {
|
|
/**
|
|
* Initializes the chart. The constructor's arguments are passed on
|
|
* directly.
|
|
*
|
|
* @function Highcharts.MapChart#init
|
|
*
|
|
* @param {Highcharts.Options} userOptions
|
|
* Custom options.
|
|
*
|
|
* @param {Function} [callback]
|
|
* Function to run when the chart has loaded and and all external
|
|
* images are loaded.
|
|
*
|
|
*
|
|
* @emits Highcharts.MapChart#event:init
|
|
* @emits Highcharts.MapChart#event:afterInit
|
|
*/
|
|
init(userOptions, callback) {
|
|
const defaultCreditsOptions = getOptions().credits;
|
|
const options = merge({
|
|
chart: {
|
|
panning: {
|
|
enabled: true,
|
|
type: 'xy'
|
|
},
|
|
type: 'map'
|
|
},
|
|
credits: {
|
|
mapText: pick(defaultCreditsOptions.mapText, ' \u00a9 <a href="{geojson.copyrightUrl}">' +
|
|
'{geojson.copyrightShort}</a>'),
|
|
mapTextFull: pick(defaultCreditsOptions.mapTextFull, '{geojson.copyright}')
|
|
},
|
|
mapView: {},
|
|
tooltip: {
|
|
followTouchMove: false
|
|
}
|
|
}, userOptions // user's options
|
|
);
|
|
super.init(options, callback);
|
|
}
|
|
}
|
|
/* eslint-disable valid-jsdoc */
|
|
(function (MapChart) {
|
|
/**
|
|
* Contains all loaded map data for Highmaps.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @name Highcharts.maps
|
|
* @type {Record<string,*>}
|
|
*/
|
|
MapChart.maps = {};
|
|
/**
|
|
* The factory function for creating new map charts. Creates a new {@link
|
|
* Highcharts.MapChart|MapChart} object with different default options than
|
|
* the basic Chart.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @function Highcharts.mapChart
|
|
*
|
|
* @param {string|Highcharts.HTMLDOMElement} [renderTo]
|
|
* The DOM element to render to, or its id.
|
|
*
|
|
* @param {Highcharts.Options} options
|
|
* The chart options structure as described in the
|
|
* [options reference](https://api.highcharts.com/highstock).
|
|
*
|
|
* @param {Highcharts.ChartCallbackFunction} [callback]
|
|
* A function to execute when the chart object is finished
|
|
* rendering and all external image files (`chart.backgroundImage`,
|
|
* `chart.plotBackgroundImage` etc) are loaded. Defining a
|
|
* [chart.events.load](https://api.highcharts.com/highstock/chart.events.load)
|
|
* handler is equivalent.
|
|
*
|
|
* @return {Highcharts.MapChart}
|
|
* The chart object.
|
|
*/
|
|
function mapChart(a, b, c) {
|
|
return new MapChart(a, b, c);
|
|
}
|
|
MapChart.mapChart = mapChart;
|
|
/**
|
|
* Utility for reading SVG paths directly.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @function Highcharts.splitPath
|
|
*
|
|
* @param {string|Array<string|number>} path
|
|
*
|
|
* @return {Highcharts.SVGPathArray}
|
|
* Splitted SVG path
|
|
*/
|
|
function splitPath(path) {
|
|
let arr;
|
|
if (typeof path === 'string') {
|
|
path = path
|
|
// Move letters apart
|
|
.replace(/([A-Za-z])/g, ' $1 ')
|
|
// Trim
|
|
.replace(/^\s*/, '').replace(/\s*$/, '');
|
|
// Split on spaces and commas. The semicolon is bogus, designed to
|
|
// circumvent string replacement in the pre-v7 assembler that built
|
|
// specific styled mode files.
|
|
const split = path.split(/[ ,;]+/);
|
|
arr = split.map((item) => {
|
|
if (!/[A-za-z]/.test(item)) {
|
|
return parseFloat(item);
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
else {
|
|
arr = path;
|
|
}
|
|
return SVGRenderer.prototype.pathToSegments(arr);
|
|
}
|
|
MapChart.splitPath = splitPath;
|
|
})(MapChart || (MapChart = {}));
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return MapChart;
|
|
});
|
|
_registerModule(_modules, 'Maps/MapUtilities.js', [], function () {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
// Compute bounds from a path element
|
|
const boundsFromPath = function (path) {
|
|
let x2 = -Number.MAX_VALUE, x1 = Number.MAX_VALUE, y2 = -Number.MAX_VALUE, y1 = Number.MAX_VALUE, validBounds;
|
|
path.forEach((seg) => {
|
|
const x = seg[seg.length - 2], y = seg[seg.length - 1];
|
|
if (typeof x === 'number' &&
|
|
typeof y === 'number') {
|
|
x1 = Math.min(x1, x);
|
|
x2 = Math.max(x2, x);
|
|
y1 = Math.min(y1, y);
|
|
y2 = Math.max(y2, y);
|
|
validBounds = true;
|
|
}
|
|
});
|
|
if (validBounds) {
|
|
return { x1, y1, x2, y2 };
|
|
}
|
|
};
|
|
/**
|
|
* Test for point in polygon. Polygon defined as array of [x,y] points.
|
|
* @private
|
|
*/
|
|
const pointInPolygon = function (point, polygon) {
|
|
let i, j, rel1, rel2, c = false, x = point.x, y = point.y;
|
|
for (i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
rel1 = polygon[i][1] > y;
|
|
rel2 = polygon[j][1] > y;
|
|
if (rel1 !== rel2 &&
|
|
(x < (polygon[j][0] - polygon[i][0]) * (y - polygon[i][1]) /
|
|
(polygon[j][1] - polygon[i][1]) +
|
|
polygon[i][0])) {
|
|
c = !c;
|
|
}
|
|
}
|
|
return c;
|
|
};
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
const MapUtilities = {
|
|
boundsFromPath,
|
|
pointInPolygon
|
|
};
|
|
|
|
return MapUtilities;
|
|
});
|
|
_registerModule(_modules, 'Series/Map/MapPoint.js', [_modules['Series/ColorMapComposition.js'], _modules['Maps/MapUtilities.js'], _modules['Core/Series/SeriesRegistry.js'], _modules['Core/Utilities.js']], function (ColorMapComposition, MapUtilities, SeriesRegistry, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { boundsFromPath } = MapUtilities;
|
|
const {
|
|
// indirect dependency to keep product size low
|
|
seriesTypes: { scatter: ScatterSeries } } = SeriesRegistry;
|
|
const { extend, isNumber, pick } = U;
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
class MapPoint extends ScatterSeries.prototype.pointClass {
|
|
constructor() {
|
|
/* *
|
|
*
|
|
* Properties
|
|
*
|
|
* */
|
|
super(...arguments);
|
|
this.options = void 0;
|
|
this.path = void 0;
|
|
this.series = void 0;
|
|
/* eslint-enable valid-jsdoc */
|
|
}
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
/* eslint-disable valid-jsdoc */
|
|
// Get the projected path based on the geometry. May also be called on
|
|
// mapData options (not point instances), hence static.
|
|
static getProjectedPath(point, projection) {
|
|
if (!point.projectedPath) {
|
|
if (projection && point.geometry) {
|
|
// Always true when given GeoJSON coordinates
|
|
projection.hasCoordinates = true;
|
|
point.projectedPath = projection.path(point.geometry);
|
|
// SVG path given directly in point options
|
|
}
|
|
else {
|
|
point.projectedPath = point.path;
|
|
}
|
|
}
|
|
return point.projectedPath || [];
|
|
}
|
|
/**
|
|
* Extend the Point object to split paths.
|
|
* @private
|
|
*/
|
|
applyOptions(options, x) {
|
|
const series = this.series, point = super.applyOptions.call(this, options, x), joinBy = series.joinBy;
|
|
if (series.mapData && series.mapMap) {
|
|
const joinKey = joinBy[1], mapKey = super.getNestedProperty.call(point, joinKey), mapPoint = typeof mapKey !== 'undefined' &&
|
|
series.mapMap[mapKey];
|
|
if (mapPoint) {
|
|
extend(point, mapPoint); // copy over properties
|
|
}
|
|
else if (series.pointArrayMap.indexOf('value') !== -1) {
|
|
point.value = point.value || null;
|
|
}
|
|
}
|
|
return point;
|
|
}
|
|
/*
|
|
* Get the bounds in terms of projected units
|
|
* @param projection
|
|
* @return MapBounds|undefined The computed bounds
|
|
*/
|
|
getProjectedBounds(projection) {
|
|
const path = MapPoint.getProjectedPath(this, projection), bounds = boundsFromPath(path), properties = this.properties, mapView = this.series.chart.mapView;
|
|
if (bounds) {
|
|
// Cache point bounding box for use to position data labels, bubbles
|
|
// etc
|
|
const propMiddleLon = properties && properties['hc-middle-lon'], propMiddleLat = properties && properties['hc-middle-lat'];
|
|
if (mapView && isNumber(propMiddleLon) && isNumber(propMiddleLat)) {
|
|
const projectedPoint = projection.forward([propMiddleLon, propMiddleLat]);
|
|
bounds.midX = projectedPoint[0];
|
|
bounds.midY = projectedPoint[1];
|
|
}
|
|
else {
|
|
const propMiddleX = properties && properties['hc-middle-x'], propMiddleY = properties && properties['hc-middle-y'];
|
|
bounds.midX = (bounds.x1 + (bounds.x2 - bounds.x1) * pick(this.middleX, isNumber(propMiddleX) ? propMiddleX : 0.5));
|
|
let middleYFraction = pick(this.middleY, isNumber(propMiddleY) ? propMiddleY : 0.5);
|
|
// No geographic geometry, only path given => flip
|
|
if (!this.geometry) {
|
|
middleYFraction = 1 - middleYFraction;
|
|
}
|
|
bounds.midY =
|
|
bounds.y2 - (bounds.y2 - bounds.y1) * middleYFraction;
|
|
}
|
|
return bounds;
|
|
}
|
|
}
|
|
/**
|
|
* Stop the fade-out
|
|
* @private
|
|
*/
|
|
onMouseOver(e) {
|
|
U.clearTimeout(this.colorInterval);
|
|
if (
|
|
// Valid...
|
|
(!this.isNull && this.visible) ||
|
|
// ... or interact anyway
|
|
this.series.options.nullInteraction) {
|
|
super.onMouseOver.call(this, e);
|
|
}
|
|
else {
|
|
// #3401 Tooltip doesn't hide when hovering over null points
|
|
this.series.onMouseOut(e);
|
|
}
|
|
}
|
|
setVisible(vis) {
|
|
const method = vis ? 'show' : 'hide';
|
|
this.visible = this.options.visible = !!vis;
|
|
// Show and hide associated elements
|
|
if (this.dataLabel) {
|
|
this.dataLabel[method]();
|
|
}
|
|
// For invisible map points, render them as null points rather than
|
|
// fully removing them. Makes more sense for color axes with data
|
|
// classes.
|
|
if (this.graphic) {
|
|
this.graphic.attr(this.series.pointAttribs(this));
|
|
}
|
|
}
|
|
/**
|
|
* Highmaps only. Zoom in on the point using the global animation.
|
|
*
|
|
* @sample maps/members/point-zoomto/
|
|
* Zoom to points from buttons
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @function Highcharts.Point#zoomTo
|
|
*/
|
|
zoomTo(animOptions) {
|
|
const point = this, chart = point.series.chart, mapView = chart.mapView;
|
|
let bounds = point.bounds;
|
|
if (mapView && bounds) {
|
|
const inset = isNumber(point.insetIndex) &&
|
|
mapView.insets[point.insetIndex];
|
|
if (inset) {
|
|
// If in an inset, translate the bounds to pixels ...
|
|
const px1 = inset.projectedUnitsToPixels({
|
|
x: bounds.x1,
|
|
y: bounds.y1
|
|
}), px2 = inset.projectedUnitsToPixels({
|
|
x: bounds.x2,
|
|
y: bounds.y2
|
|
}),
|
|
// ... then back to projected units in the main mapView
|
|
proj1 = mapView.pixelsToProjectedUnits({
|
|
x: px1.x,
|
|
y: px1.y
|
|
}), proj2 = mapView.pixelsToProjectedUnits({
|
|
x: px2.x,
|
|
y: px2.y
|
|
});
|
|
bounds = {
|
|
x1: proj1.x,
|
|
y1: proj1.y,
|
|
x2: proj2.x,
|
|
y2: proj2.y
|
|
};
|
|
}
|
|
mapView.fitToBounds(bounds, void 0, false);
|
|
point.series.isDirty = true;
|
|
chart.redraw(animOptions);
|
|
}
|
|
}
|
|
}
|
|
extend(MapPoint.prototype, {
|
|
dataLabelOnNull: ColorMapComposition.pointMembers.dataLabelOnNull,
|
|
moveToTopOnHover: ColorMapComposition.pointMembers.moveToTopOnHover,
|
|
isValid: ColorMapComposition.pointMembers.isValid
|
|
});
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return MapPoint;
|
|
});
|
|
_registerModule(_modules, 'Maps/MapViewOptionsDefault.js', [], function () {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
/**
|
|
* The `mapView` options control the initial view of the chart, and how
|
|
* projection is set up for raw geoJSON maps (beta as of v9.3).
|
|
*
|
|
* To set the view dynamically after chart generation, see
|
|
* [mapView.setView](/class-reference/Highcharts.MapView#setView).
|
|
*
|
|
* @since 9.3.0
|
|
* @product highmaps
|
|
* @optionparent mapView
|
|
*/
|
|
const defaultOptions = {
|
|
/**
|
|
* The center of the map in terms of longitude and latitude. For
|
|
* preprojected maps (like the GeoJSON files in Map Collection v1.x), the
|
|
* units are projected x and y units.
|
|
*
|
|
* @default [0, 0]
|
|
* @type {Highcharts.LonLatArray}
|
|
*
|
|
* @sample {highmaps} maps/mapview/center-zoom Custom view of a world map
|
|
* @sample {highmaps} maps/mapview/get-view Report the current view of a
|
|
* preprojected map
|
|
*/
|
|
center: [0, 0],
|
|
/**
|
|
* Fit the map to a geometry object consisting of individual points or
|
|
* polygons. This is practical for responsive maps where we want to focus on
|
|
* a specific area regardless of map size - unlike setting `center` and
|
|
* `zoom`, where the view doesn't scale with different map sizes.
|
|
*
|
|
* The geometry can be combined with the [padding](#mapView.padding) option
|
|
* to avoid touching the edges of the chart.
|
|
*
|
|
* @type {object}
|
|
* @since 10.3.3
|
|
*
|
|
* @sample maps/mapview/fittogeometry Fitting the view to geometries
|
|
*/
|
|
fitToGeometry: void 0,
|
|
/**
|
|
* Prevents the end user from zooming too far in on the map. See
|
|
* [zoom](#mapView.zoom).
|
|
*
|
|
* @type {number|undefined}
|
|
*
|
|
* @sample {highmaps} maps/mapview/maxzoom
|
|
* Prevent zooming in too far
|
|
*/
|
|
maxZoom: void 0,
|
|
/**
|
|
* The padding inside the plot area when auto fitting to the map bounds. A
|
|
* number signifies pixels, and a percentage is relative to the plot area
|
|
* size.
|
|
*
|
|
* An array sets individual padding for the sides in the order [top, right,
|
|
* bottom, left].
|
|
*
|
|
* @sample {highmaps} maps/chart/plotbackgroundcolor-color
|
|
* Visible plot area and percentage padding
|
|
* @sample {highmaps} maps/demo/mappoint-mapmarker
|
|
* Padding for individual sides
|
|
* @type {number|string|Array<number|string>}
|
|
*/
|
|
padding: 0,
|
|
/**
|
|
* The projection options allow applying client side projection to a map
|
|
* given in geographic coordinates, typically from TopoJSON or GeoJSON.
|
|
*
|
|
* @type {Object}
|
|
*
|
|
* @sample maps/demo/projection-explorer
|
|
* Projection explorer
|
|
* @sample maps/demo/topojson-projection
|
|
* Orthographic projection
|
|
* @sample maps/mapview/projection-custom-proj4js
|
|
* Custom UTM projection definition
|
|
* @sample maps/mapview/projection-custom-d3geo
|
|
* Custom Robinson projection definition
|
|
*/
|
|
projection: {
|
|
/**
|
|
* Projection name. Built-in projections are `EqualEarth`,
|
|
* `LambertConformalConic`, `Miller`, `Orthographic` and `WebMercator`.
|
|
*
|
|
* @type {string}
|
|
* @sample maps/demo/projection-explorer
|
|
* Projection explorer
|
|
* @sample maps/mapview/projection-custom-proj4js
|
|
* Custom UTM projection definition
|
|
* @sample maps/mapview/projection-custom-d3geo
|
|
* Custom Robinson projection definition
|
|
* @sample maps/demo/topojson-projection
|
|
* Orthographic projection
|
|
*/
|
|
name: void 0,
|
|
/**
|
|
* The two standard parallels that define the map layout in conic
|
|
* projections, like the LambertConformalConic projection. If only one
|
|
* number is given, the second parallel will be the same as the first.
|
|
*
|
|
* @sample maps/mapview/projection-parallels
|
|
* LCC projection with parallels
|
|
* @sample maps/demo/projection-explorer
|
|
* Projection explorer
|
|
* @type {Array<number>}
|
|
*/
|
|
parallels: void 0,
|
|
/**
|
|
* Rotation of the projection in terms of degrees `[lambda, phi,
|
|
* gamma]`. When given, a three-axis spherical rotation is be applied
|
|
* to the globe prior to the projection.
|
|
*
|
|
* * `lambda` shifts the longitudes by the given value.
|
|
* * `phi` shifts the latitudes by the given value. Can be omitted.
|
|
* * `gamma` applies a _roll_. Can be omitted.
|
|
*
|
|
* @sample maps/demo/projection-explorer
|
|
* Projection explorer
|
|
* @sample maps/mapview/projection-america-centric
|
|
* America-centric world map
|
|
*/
|
|
rotation: void 0
|
|
},
|
|
/**
|
|
* The zoom level of a map. Higher zoom levels means more zoomed in. An
|
|
* increase of 1 zooms in to a quarter of the viewed area (half the width
|
|
* and height). Defaults to fitting to the map bounds.
|
|
*
|
|
* In a `WebMercator` projection, a zoom level of 0 represents
|
|
* the world in a 256x256 pixel square. This is a common concept for WMS
|
|
* tiling software.
|
|
*
|
|
* @type {number|undefined}
|
|
* @sample {highmaps} maps/mapview/center-zoom
|
|
* Custom view of a world map
|
|
* @sample {highmaps} maps/mapview/get-view
|
|
* Report the current view of a preprojected map
|
|
*/
|
|
zoom: void 0
|
|
};
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return defaultOptions;
|
|
});
|
|
_registerModule(_modules, 'Maps/MapViewInsetsOptionsDefault.js', [], function () {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
/**
|
|
* Generic options for the placement and appearance of map insets like
|
|
* non-contiguous territories.
|
|
*
|
|
* @since 10.0.0
|
|
* @product highmaps
|
|
* @optionparent mapView.insetOptions
|
|
*/
|
|
const defaultOptions = {
|
|
/**
|
|
* The border color of the insets.
|
|
*
|
|
* @sample maps/mapview/insetoptions-border
|
|
* Inset border options
|
|
* @type {Highcharts.ColorType}
|
|
*/
|
|
borderColor: "#cccccc" /* Palette.neutralColor20 */,
|
|
/**
|
|
* The pixel border width of the insets.
|
|
*
|
|
* @sample maps/mapview/insetoptions-border
|
|
* Inset border options
|
|
*/
|
|
borderWidth: 1,
|
|
/**
|
|
* @ignore-option
|
|
*/
|
|
center: [0, 0],
|
|
/**
|
|
* The padding of the insets. Can be either a number of pixels, a percentage
|
|
* string, or an array of either. If an array is given, it sets the top,
|
|
* right, bottom, left paddings respectively.
|
|
*
|
|
* @type {number|string|Array<number|string>}
|
|
*/
|
|
padding: '10%',
|
|
/**
|
|
* What coordinate system the `field` and `borderPath` should relate to. If
|
|
* `plotBox`, they will be fixed to the plot box and responsively move in
|
|
* relation to the main map. If `mapBoundingBox`, they will be fixed to the
|
|
* map bounding box, which is constant and centered in different chart sizes
|
|
* and ratios.
|
|
*
|
|
* @validvalue ["plotBox", "mapBoundingBox"]
|
|
*/
|
|
relativeTo: 'mapBoundingBox',
|
|
/**
|
|
* What units to use for the `field` and `borderPath` geometries. If
|
|
* `percent` (default), they relate to the box given in `relativeTo`. If
|
|
* `pixels`, they are absolute values.
|
|
*
|
|
* @validvalue ["percent", "pixels"]
|
|
*/
|
|
units: 'percent'
|
|
};
|
|
/**
|
|
* The individual MapView insets, typically used for non-contiguous areas of a
|
|
* country. Each item inherits from the generic `insetOptions`.
|
|
*
|
|
* Some of the TopoJSON files of the [Highcharts Map
|
|
* Collection](https://code.highcharts.com/mapdata/) include a property called
|
|
* `hc-recommended-mapview`, and some of these include insets. In order to
|
|
* override the recommended inset options, an inset option with a matching id
|
|
* can be applied, and it will be merged into the embedded settings.
|
|
*
|
|
* @sample maps/mapview/insets-extended
|
|
* Extending the embedded insets
|
|
* @sample maps/mapview/insets-complete
|
|
* Complete inset config from scratch
|
|
*
|
|
* @extends mapView.insetOptions
|
|
* @type Array<Object>
|
|
* @product highmaps
|
|
* @apioption mapView.insets
|
|
*/
|
|
/**
|
|
* A geometry object of type `MultiLineString` defining the border path of the
|
|
* inset in terms of `units`. If undefined, a border is rendered around the
|
|
* `field` geometry. It is recommended that the `borderPath` partly follows the
|
|
* outline of the `field` in order to make pointer positioning consistent.
|
|
*
|
|
* @sample maps/mapview/insets-complete
|
|
* Complete inset config with `borderPath`
|
|
*
|
|
* @product highmaps
|
|
* @type {Object|undefined}
|
|
* @apioption mapView.insets.borderPath
|
|
*/
|
|
/**
|
|
* A geometry object of type `Polygon` defining where in the chart the inset
|
|
* should be rendered, in terms of `units` and relative to the `relativeTo`
|
|
* setting. If a `borderPath` is omitted, a border is rendered around the field.
|
|
* If undefined, the inset is rendered in the full plot area.
|
|
*
|
|
* @sample maps/mapview/insets-extended
|
|
* Border path emitted, field is rendered
|
|
*
|
|
* @product highmaps
|
|
* @type {Object|undefined}
|
|
* @apioption mapView.insets.field
|
|
*/
|
|
/**
|
|
* A geometry object of type `Polygon` encircling the shapes that should be
|
|
* rendered in the inset, in terms of geographic coordinates. Geometries within
|
|
* this geometry are removed from the default map view and rendered in the
|
|
* inset.
|
|
*
|
|
* @sample maps/mapview/insets-complete
|
|
* Complete inset config with `geoBounds`
|
|
*
|
|
* @product highmaps
|
|
* @type {Object}
|
|
* @apioption mapView.insets.geoBounds
|
|
*/
|
|
/**
|
|
* The id of the inset, used for internal reference.
|
|
*
|
|
* @sample maps/mapview/insets-extended
|
|
* Extending recommended insets by id
|
|
*
|
|
* @product highmaps
|
|
* @type {string}
|
|
* @apioption mapView.insets.id
|
|
*/
|
|
/**
|
|
* The projection options for the inset.
|
|
*
|
|
* @product highmaps
|
|
* @type {Object}
|
|
* @extends mapView.projection
|
|
* @apioption mapView.insets.projection
|
|
*/
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return defaultOptions;
|
|
});
|
|
_registerModule(_modules, 'Extensions/GeoJSON.js', [_modules['Core/Chart/Chart.js'], _modules['Core/Templating.js'], _modules['Core/Globals.js'], _modules['Core/Utilities.js']], function (Chart, F, H, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { format } = F;
|
|
const { win } = H;
|
|
const { error, extend, merge, wrap } = U;
|
|
/**
|
|
* Represents the loose structure of a geographic JSON file.
|
|
*
|
|
* @interface Highcharts.GeoJSON
|
|
*/ /**
|
|
* Full copyright note of the geographic data.
|
|
* @name Highcharts.GeoJSON#copyright
|
|
* @type {string|undefined}
|
|
*/ /**
|
|
* Short copyright note of the geographic data suitable for watermarks.
|
|
* @name Highcharts.GeoJSON#copyrightShort
|
|
* @type {string|undefined}
|
|
*/ /**
|
|
* Additional meta information based on the coordinate reference system.
|
|
* @name Highcharts.GeoJSON#crs
|
|
* @type {Highcharts.Dictionary<any>|undefined}
|
|
*/ /**
|
|
* Data sets of geographic features.
|
|
* @name Highcharts.GeoJSON#features
|
|
* @type {Array<Highcharts.GeoJSONFeature>}
|
|
*/ /**
|
|
* Map projections and transformations to be used when calculating between
|
|
* lat/lon and chart values. Required for lat/lon support on maps. Allows
|
|
* resizing, rotating, and moving portions of a map within its projected
|
|
* coordinate system while still retaining lat/lon support. If using lat/lon
|
|
* on a portion of the map that does not match a `hitZone`, the definition with
|
|
* the key `default` is used.
|
|
* @name Highcharts.GeoJSON#hc-transform
|
|
* @type {Highcharts.Dictionary<Highcharts.GeoJSONTranslation>|undefined}
|
|
*/ /**
|
|
* Title of the geographic data.
|
|
* @name Highcharts.GeoJSON#title
|
|
* @type {string|undefined}
|
|
*/ /**
|
|
* Type of the geographic data. Type of an optimized map collection is
|
|
* `FeatureCollection`.
|
|
* @name Highcharts.GeoJSON#type
|
|
* @type {string|undefined}
|
|
*/ /**
|
|
* Version of the geographic data.
|
|
* @name Highcharts.GeoJSON#version
|
|
* @type {string|undefined}
|
|
*/
|
|
/**
|
|
* Data set of a geographic feature.
|
|
* @interface Highcharts.GeoJSONFeature
|
|
* @extends Highcharts.Dictionary<*>
|
|
*/ /**
|
|
* Data type of the geographic feature.
|
|
* @name Highcharts.GeoJSONFeature#type
|
|
* @type {string}
|
|
*/
|
|
/**
|
|
* Describes the map projection and transformations applied to a portion of
|
|
* a map.
|
|
* @interface Highcharts.GeoJSONTranslation
|
|
*/ /**
|
|
* The coordinate reference system used to generate this portion of the map.
|
|
* @name Highcharts.GeoJSONTranslation#crs
|
|
* @type {string}
|
|
*/ /**
|
|
* Define the portion of the map that this defintion applies to. Defined as a
|
|
* GeoJSON polygon feature object, with `type` and `coordinates` properties.
|
|
* @name Highcharts.GeoJSONTranslation#hitZone
|
|
* @type {Highcharts.Dictionary<*>|undefined}
|
|
*/ /**
|
|
* Property for internal use for maps generated by Highsoft.
|
|
* @name Highcharts.GeoJSONTranslation#jsonmarginX
|
|
* @type {number|undefined}
|
|
*/ /**
|
|
* Property for internal use for maps generated by Highsoft.
|
|
* @name Highcharts.GeoJSONTranslation#jsonmarginY
|
|
* @type {number|undefined}
|
|
*/ /**
|
|
* Property for internal use for maps generated by Highsoft.
|
|
* @name Highcharts.GeoJSONTranslation#jsonres
|
|
* @type {number|undefined}
|
|
*/ /**
|
|
* Specifies clockwise rotation of the coordinates after the projection, but
|
|
* before scaling and panning. Defined in radians, relative to the coordinate
|
|
* system origin.
|
|
* @name Highcharts.GeoJSONTranslation#rotation
|
|
* @type {number|undefined}
|
|
*/ /**
|
|
* The scaling factor applied to the projected coordinates.
|
|
* @name Highcharts.GeoJSONTranslation#scale
|
|
* @type {number|undefined}
|
|
*/ /**
|
|
* Property for internal use for maps generated by Highsoft.
|
|
* @name Highcharts.GeoJSONTranslation#xoffset
|
|
* @type {number|undefined}
|
|
*/ /**
|
|
* X offset of projected coordinates after scaling.
|
|
* @name Highcharts.GeoJSONTranslation#xpan
|
|
* @type {number|undefined}
|
|
*/ /**
|
|
* Property for internal use for maps generated by Highsoft.
|
|
* @name Highcharts.GeoJSONTranslation#yoffset
|
|
* @type {number|undefined}
|
|
*/ /**
|
|
* Y offset of projected coordinates after scaling.
|
|
* @name Highcharts.GeoJSONTranslation#ypan
|
|
* @type {number|undefined}
|
|
*/
|
|
/**
|
|
* Result object of a map transformation.
|
|
*
|
|
* @interface Highcharts.ProjectedXY
|
|
*/ /**
|
|
* X coordinate in projected units.
|
|
* @name Highcharts.ProjectedXY#x
|
|
* @type {number}
|
|
*/ /**
|
|
* Y coordinate in projected units
|
|
* @name Highcharts.ProjectedXY#y
|
|
* @type {number}
|
|
*/
|
|
/**
|
|
* A latitude/longitude object.
|
|
*
|
|
* @interface Highcharts.MapLonLatObject
|
|
*/ /**
|
|
* The latitude.
|
|
* @name Highcharts.MapLonLatObject#lat
|
|
* @type {number}
|
|
*/ /**
|
|
* The longitude.
|
|
* @name Highcharts.MapLonLatObject#lon
|
|
* @type {number}
|
|
*/
|
|
/**
|
|
* An array of longitude, latitude.
|
|
*
|
|
* @typedef {Array<number>} Highcharts.LonLatArray
|
|
*/
|
|
/**
|
|
* A TopoJSON object, see description on the
|
|
* [project's GitHub page](https://github.com/topojson/topojson).
|
|
*
|
|
* @typedef {Object} Highcharts.TopoJSON
|
|
*/
|
|
''; // detach doclets above
|
|
/* eslint-disable no-invalid-this, valid-jsdoc */
|
|
/**
|
|
* Highcharts Maps only. Get point from latitude and longitude using specified
|
|
* transform definition.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @sample maps/series/latlon-transform/
|
|
* Use specific transformation for lat/lon
|
|
*
|
|
* @function Highcharts.Chart#transformFromLatLon
|
|
*
|
|
* @param {Highcharts.MapLonLatObject} latLon
|
|
* A latitude/longitude object.
|
|
*
|
|
* @param {*} transform
|
|
* The transform definition to use as explained in the
|
|
* {@link https://www.highcharts.com/docs/maps/latlon|documentation}.
|
|
*
|
|
* @return {ProjectedXY}
|
|
* An object with `x` and `y` properties.
|
|
*/
|
|
Chart.prototype.transformFromLatLon = function (latLon, transform) {
|
|
/**
|
|
* Allows to manually load the proj4 library from Highcharts options
|
|
* instead of the `window`.
|
|
* In case of loading the library from a `script` tag,
|
|
* this option is not needed, it will be loaded from there by default.
|
|
*
|
|
* @type {Function}
|
|
* @product highmaps
|
|
* @apioption chart.proj4
|
|
*/
|
|
const proj4 = this.options.chart.proj4 || win.proj4;
|
|
if (!proj4) {
|
|
error(21, false, this);
|
|
return;
|
|
}
|
|
const { jsonmarginX = 0, jsonmarginY = 0, jsonres = 1, scale = 1, xoffset = 0, xpan = 0, yoffset = 0, ypan = 0 } = transform;
|
|
const projected = proj4(transform.crs, [latLon.lon, latLon.lat]), cosAngle = transform.cosAngle ||
|
|
(transform.rotation && Math.cos(transform.rotation)), sinAngle = transform.sinAngle ||
|
|
(transform.rotation && Math.sin(transform.rotation)), rotated = transform.rotation ? [
|
|
projected[0] * cosAngle + projected[1] * sinAngle,
|
|
-projected[0] * sinAngle + projected[1] * cosAngle
|
|
] : projected;
|
|
return {
|
|
x: ((rotated[0] - xoffset) * scale + xpan) * jsonres + jsonmarginX,
|
|
y: -(((yoffset - rotated[1]) * scale + ypan) * jsonres - jsonmarginY)
|
|
};
|
|
};
|
|
/**
|
|
* Highcharts Maps only. Get latLon from point using specified transform
|
|
* definition. The method returns an object with the numeric properties `lat`
|
|
* and `lon`.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @sample maps/series/latlon-transform/ Use specific transformation for lat/lon
|
|
*
|
|
* @function Highcharts.Chart#transformToLatLon
|
|
*
|
|
* @param {Highcharts.Point|Highcharts.ProjectedXY} point A `Point` instance, or
|
|
* any object containing the properties `x` and `y` with numeric values.
|
|
*
|
|
* @param {*} transform The transform definition to use as explained in the
|
|
* {@link https://www.highcharts.com/docs/maps/latlon|documentation}.
|
|
*
|
|
* @return {Highcharts.MapLonLatObject|undefined} An object with `lat` and `lon`
|
|
* properties.
|
|
*/
|
|
Chart.prototype.transformToLatLon = function (point, transform) {
|
|
const proj4 = this.options.chart.proj4 || win.proj4;
|
|
if (!proj4) {
|
|
error(21, false, this);
|
|
return;
|
|
}
|
|
if (point.y === null) {
|
|
return;
|
|
}
|
|
const { jsonmarginX = 0, jsonmarginY = 0, jsonres = 1, scale = 1, xoffset = 0, xpan = 0, yoffset = 0, ypan = 0 } = transform;
|
|
const normalized = {
|
|
x: ((point.x - jsonmarginX) / jsonres - xpan) / scale + xoffset,
|
|
y: ((point.y - jsonmarginY) / jsonres + ypan) / scale + yoffset
|
|
}, cosAngle = transform.cosAngle ||
|
|
(transform.rotation && Math.cos(transform.rotation)), sinAngle = transform.sinAngle ||
|
|
(transform.rotation && Math.sin(transform.rotation)),
|
|
// Note: Inverted sinAngle to reverse rotation direction
|
|
projected = proj4(transform.crs, 'WGS84', transform.rotation ? {
|
|
x: normalized.x * cosAngle + normalized.y * -sinAngle,
|
|
y: normalized.x * sinAngle + normalized.y * cosAngle
|
|
} : normalized);
|
|
return { lat: projected.y, lon: projected.x };
|
|
};
|
|
/**
|
|
* Deprecated. Use `MapView.projectedUnitsToLonLat` instead.
|
|
*
|
|
* @deprecated
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @function Highcharts.Chart#fromPointToLatLon
|
|
*
|
|
* @param {Highcharts.Point|Highcharts.ProjectedXY} point A `Point`
|
|
* instance or anything containing `x` and `y` properties with numeric
|
|
* values.
|
|
*
|
|
* @return {Highcharts.MapLonLatObject|undefined} An object with `lat` and `lon`
|
|
* properties.
|
|
*/
|
|
Chart.prototype.fromPointToLatLon = function (point) {
|
|
return this.mapView && this.mapView.projectedUnitsToLonLat(point);
|
|
};
|
|
/**
|
|
* Deprecated. Use `MapView.lonLatToProjectedUnits` instead.
|
|
*
|
|
* @deprecated
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @function Highcharts.Chart#fromLatLonToPoint
|
|
*
|
|
* @param {Highcharts.MapLonLatObject} lonLat Coordinates.
|
|
*
|
|
* @return {Highcharts.ProjectedXY}
|
|
* X and Y coordinates in terms of projected values
|
|
*/
|
|
Chart.prototype.fromLatLonToPoint = function (lonLat) {
|
|
return this.mapView && this.mapView.lonLatToProjectedUnits(lonLat);
|
|
};
|
|
/*
|
|
* Convert a TopoJSON topology to GeoJSON. By default the first object is
|
|
* handled.
|
|
* Based on https://github.com/topojson/topojson-specification
|
|
*/
|
|
function topo2geo(topology, objectName) {
|
|
// Decode first object/feature as default
|
|
if (!objectName) {
|
|
objectName = Object.keys(topology.objects)[0];
|
|
}
|
|
const object = topology.objects[objectName];
|
|
// Already decoded => return cache
|
|
if (object['hc-decoded-geojson']) {
|
|
return object['hc-decoded-geojson'];
|
|
}
|
|
// Do the initial transform
|
|
let arcsArray = topology.arcs;
|
|
if (topology.transform) {
|
|
const { scale, translate } = topology.transform;
|
|
arcsArray = topology.arcs.map((arc) => {
|
|
let x = 0, y = 0;
|
|
return arc.map((position) => {
|
|
position = position.slice();
|
|
position[0] = (x += position[0]) * scale[0] + translate[0];
|
|
position[1] = (y += position[1]) * scale[1] + translate[1];
|
|
return position;
|
|
});
|
|
});
|
|
}
|
|
// Recurse down any depth of multi-dimentional arrays of arcs and insert
|
|
// the coordinates
|
|
const arcsToCoordinates = (arcs) => {
|
|
if (typeof arcs[0] === 'number') {
|
|
return arcs.reduce((coordinates, arcNo, i) => {
|
|
let arc = arcNo < 0 ? arcsArray[~arcNo] : arcsArray[arcNo];
|
|
// The first point of an arc is always identical to the last
|
|
// point of the previes arc, so slice it off to save further
|
|
// processing.
|
|
if (arcNo < 0) {
|
|
arc = arc.slice(0, i === 0 ? arc.length : arc.length - 1);
|
|
arc.reverse();
|
|
}
|
|
else if (i) {
|
|
arc = arc.slice(1);
|
|
}
|
|
return coordinates.concat(arc);
|
|
}, []);
|
|
}
|
|
return arcs.map(arcsToCoordinates);
|
|
};
|
|
const features = object.geometries
|
|
.map((geometry) => ({
|
|
type: 'Feature',
|
|
properties: geometry.properties,
|
|
geometry: {
|
|
type: geometry.type,
|
|
coordinates: geometry.coordinates ||
|
|
arcsToCoordinates(geometry.arcs)
|
|
}
|
|
}));
|
|
const geojson = {
|
|
type: 'FeatureCollection',
|
|
copyright: topology.copyright,
|
|
copyrightShort: topology.copyrightShort,
|
|
copyrightUrl: topology.copyrightUrl,
|
|
features,
|
|
'hc-recommended-mapview': object['hc-recommended-mapview'],
|
|
bbox: topology.bbox,
|
|
title: topology.title
|
|
};
|
|
object['hc-decoded-geojson'] = geojson;
|
|
return geojson;
|
|
}
|
|
/**
|
|
* Highcharts Maps only. Restructure a GeoJSON or TopoJSON object in preparation
|
|
* to be read directly by the
|
|
* {@link https://api.highcharts.com/highmaps/plotOptions.series.mapData|series.mapData}
|
|
* option. The object will be broken down to fit a specific Highcharts type,
|
|
* either `map`, `mapline` or `mappoint`. Meta data in GeoJSON's properties
|
|
* object will be copied directly over to {@link Point.properties} in Highcharts
|
|
* Maps.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @sample maps/demo/geojson/ Simple areas
|
|
* @sample maps/demo/mapline-mappoint/ Multiple types
|
|
* @sample maps/series/mapdata-multiple/ Multiple map sources
|
|
*
|
|
* @function Highcharts.geojson
|
|
*
|
|
* @param {Highcharts.GeoJSON|Highcharts.TopoJSON} json The GeoJSON or TopoJSON
|
|
* structure to parse, represented as a JavaScript object.
|
|
*
|
|
* @param {string} [hType=map] The Highcharts Maps series type to prepare for.
|
|
* Setting "map" will return GeoJSON polygons and multipolygons. Setting
|
|
* "mapline" will return GeoJSON linestrings and multilinestrings.
|
|
* Setting "mappoint" will return GeoJSON points and multipoints.
|
|
*
|
|
*
|
|
* @return {Array<*>} An object ready for the `mapData` option.
|
|
*/
|
|
function geojson(json, hType = 'map', series) {
|
|
const mapData = [];
|
|
const geojson = json.type === 'Topology' ? topo2geo(json) : json;
|
|
geojson.features.forEach(function (feature) {
|
|
const geometry = feature.geometry || {}, type = geometry.type, coordinates = geometry.coordinates, properties = feature.properties;
|
|
let pointOptions;
|
|
if ((hType === 'map' || hType === 'mapbubble') &&
|
|
(type === 'Polygon' || type === 'MultiPolygon')) {
|
|
if (coordinates.length) {
|
|
pointOptions = { geometry: { coordinates, type } };
|
|
}
|
|
}
|
|
else if (hType === 'mapline' &&
|
|
(type === 'LineString' ||
|
|
type === 'MultiLineString')) {
|
|
if (coordinates.length) {
|
|
pointOptions = { geometry: { coordinates, type } };
|
|
}
|
|
}
|
|
else if (hType === 'mappoint' && type === 'Point') {
|
|
if (coordinates.length) {
|
|
pointOptions = { geometry: { coordinates, type } };
|
|
}
|
|
}
|
|
if (pointOptions) {
|
|
const name = properties && (properties.name || properties.NAME), lon = properties && properties.lon, lat = properties && properties.lat;
|
|
mapData.push(extend(pointOptions, {
|
|
lat: typeof lat === 'number' ? lat : void 0,
|
|
lon: typeof lon === 'number' ? lon : void 0,
|
|
name: typeof name === 'string' ? name : void 0,
|
|
/**
|
|
* In Highcharts Maps, when data is loaded from GeoJSON, the
|
|
* GeoJSON item's properies are copied over here.
|
|
*
|
|
* @requires modules/map
|
|
* @name Highcharts.Point#properties
|
|
* @type {*}
|
|
*/
|
|
properties
|
|
}));
|
|
}
|
|
});
|
|
// Create a credits text that includes map source, to be picked up in
|
|
// Chart.addCredits
|
|
if (series && geojson.copyrightShort) {
|
|
series.chart.mapCredits = format(series.chart.options.credits.mapText, { geojson: geojson });
|
|
series.chart.mapCreditsFull = format(series.chart.options.credits.mapTextFull, { geojson: geojson });
|
|
}
|
|
return mapData;
|
|
}
|
|
// Override addCredits to include map source by default
|
|
wrap(Chart.prototype, 'addCredits', function (proceed, credits) {
|
|
credits = merge(true, this.options.credits, credits);
|
|
// Disable credits link if map credits enabled. This to allow for in-text
|
|
// anchors.
|
|
if (this.mapCredits) {
|
|
credits.href = null;
|
|
}
|
|
proceed.call(this, credits);
|
|
// Add full map credits to hover
|
|
if (this.credits && this.mapCreditsFull) {
|
|
this.credits.attr({
|
|
title: this.mapCreditsFull
|
|
});
|
|
}
|
|
});
|
|
H.geojson = geojson;
|
|
H.topo2geo = topo2geo;
|
|
const GeoJSONModule = {
|
|
geojson,
|
|
topo2geo
|
|
};
|
|
|
|
return GeoJSONModule;
|
|
});
|
|
_registerModule(_modules, 'Core/Geometry/PolygonClip.js', [], function () {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Highsoft AS
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const isInside = (clipEdge1, clipEdge2, p) => (clipEdge2[0] - clipEdge1[0]) * (p[1] - clipEdge1[1]) >
|
|
(clipEdge2[1] - clipEdge1[1]) * (p[0] - clipEdge1[0]);
|
|
const intersection = (clipEdge1, clipEdge2, prevPoint, currentPoint) => {
|
|
const dc = [
|
|
clipEdge1[0] - clipEdge2[0],
|
|
clipEdge1[1] - clipEdge2[1]
|
|
], dp = [
|
|
prevPoint[0] - currentPoint[0],
|
|
prevPoint[1] - currentPoint[1]
|
|
], n1 = clipEdge1[0] * clipEdge2[1] - clipEdge1[1] * clipEdge2[0], n2 = prevPoint[0] * currentPoint[1] - prevPoint[1] * currentPoint[0], n3 = 1 / (dc[0] * dp[1] - dc[1] * dp[0]), intersection = [
|
|
(n1 * dp[0] - n2 * dc[0]) * n3,
|
|
(n1 * dp[1] - n2 * dc[1]) * n3
|
|
];
|
|
intersection.isIntersection = true;
|
|
return intersection;
|
|
};
|
|
var PolygonClip;
|
|
(function (PolygonClip) {
|
|
// Simple line string clipping. Clip to bounds and insert intersection
|
|
// points.
|
|
PolygonClip.clipLineString = (line, boundsPolygon) => {
|
|
const ret = [], l = PolygonClip.clipPolygon(line, boundsPolygon, false);
|
|
for (let i = 1; i < l.length; i++) {
|
|
// Insert gap where two intersections follow each other
|
|
if (l[i].isIntersection && l[i - 1].isIntersection) {
|
|
ret.push(l.splice(0, i));
|
|
i = 0;
|
|
}
|
|
// Push the rest
|
|
if (i === l.length - 1) {
|
|
ret.push(l);
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
// Clip a polygon to another polygon using the Sutherland/Hodgman algorithm.
|
|
PolygonClip.clipPolygon = (subjectPolygon, boundsPolygon, closed = true) => {
|
|
let clipEdge1 = boundsPolygon[boundsPolygon.length - 1], clipEdge2, prevPoint, currentPoint, outputList = subjectPolygon;
|
|
for (let j = 0; j < boundsPolygon.length; j++) {
|
|
const inputList = outputList;
|
|
clipEdge2 = boundsPolygon[j];
|
|
outputList = [];
|
|
prevPoint = closed ?
|
|
// Polygon, wrap around
|
|
inputList[inputList.length - 1] :
|
|
// Open line string, don't wrap
|
|
inputList[0];
|
|
for (let i = 0; i < inputList.length; i++) {
|
|
currentPoint = inputList[i];
|
|
if (isInside(clipEdge1, clipEdge2, currentPoint)) {
|
|
if (!isInside(clipEdge1, clipEdge2, prevPoint)) {
|
|
outputList.push(intersection(clipEdge1, clipEdge2, prevPoint, currentPoint));
|
|
}
|
|
outputList.push(currentPoint);
|
|
}
|
|
else if (isInside(clipEdge1, clipEdge2, prevPoint)) {
|
|
outputList.push(intersection(clipEdge1, clipEdge2, prevPoint, currentPoint));
|
|
}
|
|
prevPoint = currentPoint;
|
|
}
|
|
clipEdge1 = clipEdge2;
|
|
}
|
|
return outputList;
|
|
};
|
|
})(PolygonClip || (PolygonClip = {}));
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
|
|
return PolygonClip;
|
|
});
|
|
_registerModule(_modules, 'Maps/Projections/LambertConformalConic.js', [], function () {
|
|
/* *
|
|
* Lambert Conformal Conic projection
|
|
* */
|
|
const sign = Math.sign ||
|
|
((n) => (n === 0 ? 0 : n > 0 ? 1 : -1)), scale = 63.78137, deg2rad = Math.PI / 180, halfPI = Math.PI / 2, eps10 = 1e-6, tany = (y) => Math.tan((halfPI + y) / 2);
|
|
class LambertConformalConic {
|
|
constructor(options) {
|
|
var _a;
|
|
const parallels = (options.parallels || [])
|
|
.map((n) => n * deg2rad), lat1 = parallels[0] || 0, lat2 = (_a = parallels[1]) !== null && _a !== void 0 ? _a : lat1, cosLat1 = Math.cos(lat1);
|
|
if (typeof options.projectedBounds === 'object') {
|
|
this.projectedBounds = options.projectedBounds;
|
|
}
|
|
// Apply the global variables
|
|
let n = lat1 === lat2 ?
|
|
Math.sin(lat1) :
|
|
Math.log(cosLat1 / Math.cos(lat2)) / Math.log(tany(lat2) / tany(lat1));
|
|
if (Math.abs(n) < 1e-10) {
|
|
n = (sign(n) || 1) * 1e-10;
|
|
}
|
|
this.n = n;
|
|
this.c = cosLat1 * Math.pow(tany(lat1), n) / n;
|
|
}
|
|
forward(lonLat) {
|
|
const lon = lonLat[0] * deg2rad, { c, n, projectedBounds } = this;
|
|
let lat = lonLat[1] * deg2rad;
|
|
if (c > 0) {
|
|
if (lat < -halfPI + eps10) {
|
|
lat = -halfPI + eps10;
|
|
}
|
|
}
|
|
else {
|
|
if (lat > halfPI - eps10) {
|
|
lat = halfPI - eps10;
|
|
}
|
|
}
|
|
const r = c / Math.pow(tany(lat), n), x = r * Math.sin(n * lon) * scale, y = (c - r * Math.cos(n * lon)) * scale, xy = [x, y];
|
|
if (projectedBounds && (x < projectedBounds.x1 ||
|
|
x > projectedBounds.x2 ||
|
|
y < projectedBounds.y1 ||
|
|
y > projectedBounds.y2)) {
|
|
xy.outside = true;
|
|
}
|
|
return xy;
|
|
}
|
|
inverse(xy) {
|
|
const x = xy[0] / scale, y = xy[1] / scale, { c, n } = this, cy = c - y, rho = sign(n) * Math.sqrt(x * x + cy * cy);
|
|
let l = Math.atan2(x, Math.abs(cy)) * sign(cy);
|
|
if (cy * n < 0) {
|
|
l -= Math.PI * sign(x) * sign(cy);
|
|
}
|
|
return [
|
|
(l / n) / deg2rad,
|
|
(2 * Math.atan(Math.pow(c / rho, 1 / n)) - halfPI) / deg2rad
|
|
];
|
|
}
|
|
}
|
|
|
|
return LambertConformalConic;
|
|
});
|
|
_registerModule(_modules, 'Maps/Projections/EqualEarth.js', [], function () {
|
|
/* *
|
|
*
|
|
* Equal Earth projection, an equal-area projection designed to minimize
|
|
* distortion and remain pleasing to the eye.
|
|
*
|
|
* Invented by Bojan Šavrič, Bernhard Jenny, and Tom Patterson in 2018. It is
|
|
* inspired by the widely used Robinson projection.
|
|
*
|
|
* */
|
|
const A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796, M = Math.sqrt(3) / 2.0, scale = 74.03120656864502;
|
|
class EqualEarth {
|
|
constructor() {
|
|
this.bounds = {
|
|
x1: -200.37508342789243,
|
|
x2: 200.37508342789243,
|
|
y1: -97.52595454902263,
|
|
y2: 97.52595454902263
|
|
};
|
|
}
|
|
forward(lonLat) {
|
|
const d = Math.PI / 180, paramLat = Math.asin(M * Math.sin(lonLat[1] * d)), paramLatSq = paramLat * paramLat, paramLatPow6 = paramLatSq * paramLatSq * paramLatSq;
|
|
const x = lonLat[0] * d * Math.cos(paramLat) * scale / (M *
|
|
(A1 +
|
|
3 * A2 * paramLatSq +
|
|
paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq)));
|
|
const y = paramLat * scale * (A1 + A2 * paramLatSq + paramLatPow6 * (A3 + A4 * paramLatSq));
|
|
return [x, y];
|
|
}
|
|
inverse(xy) {
|
|
const x = xy[0] / scale, y = xy[1] / scale, d = 180 / Math.PI, epsilon = 1e-9, iterations = 12;
|
|
let paramLat = y, paramLatSq, paramLatPow6, fy, fpy, dlat, i;
|
|
for (i = 0; i < iterations; ++i) {
|
|
paramLatSq = paramLat * paramLat;
|
|
paramLatPow6 = paramLatSq * paramLatSq * paramLatSq;
|
|
fy = paramLat * (A1 + A2 * paramLatSq + paramLatPow6 * (A3 + A4 * paramLatSq)) - y;
|
|
fpy = A1 + 3 * A2 * paramLatSq + paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq);
|
|
paramLat -= dlat = fy / fpy;
|
|
if (Math.abs(dlat) < epsilon) {
|
|
break;
|
|
}
|
|
}
|
|
paramLatSq = paramLat * paramLat;
|
|
paramLatPow6 = paramLatSq * paramLatSq * paramLatSq;
|
|
const lon = d * M * x * (A1 + 3 * A2 * paramLatSq + paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq)) / Math.cos(paramLat);
|
|
const lat = d * Math.asin(Math.sin(paramLat) / M);
|
|
return [lon, lat];
|
|
}
|
|
}
|
|
|
|
return EqualEarth;
|
|
});
|
|
_registerModule(_modules, 'Maps/Projections/Miller.js', [], function () {
|
|
/* *
|
|
* Miller projection
|
|
* */
|
|
const quarterPI = Math.PI / 4, deg2rad = Math.PI / 180, scale = 63.78137;
|
|
class Miller {
|
|
constructor() {
|
|
this.bounds = {
|
|
x1: -200.37508342789243,
|
|
x2: 200.37508342789243,
|
|
y1: -146.91480769173063,
|
|
y2: 146.91480769173063
|
|
};
|
|
}
|
|
forward(lonLat) {
|
|
return [
|
|
lonLat[0] * deg2rad * scale,
|
|
1.25 * scale * Math.log(Math.tan(quarterPI + 0.4 * lonLat[1] * deg2rad))
|
|
];
|
|
}
|
|
inverse(xy) {
|
|
return [
|
|
(xy[0] / scale) / deg2rad,
|
|
2.5 * (Math.atan(Math.exp(0.8 * (xy[1] / scale))) - quarterPI) / deg2rad
|
|
];
|
|
}
|
|
}
|
|
|
|
return Miller;
|
|
});
|
|
_registerModule(_modules, 'Maps/Projections/Orthographic.js', [], function () {
|
|
/* *
|
|
* Orthographic projection
|
|
* */
|
|
const deg2rad = Math.PI / 180, scale = 63.78460826781007;
|
|
class Orthographic {
|
|
constructor() {
|
|
this.antimeridianCutting = false;
|
|
this.bounds = {
|
|
x1: -scale,
|
|
x2: scale,
|
|
y1: -scale,
|
|
y2: scale
|
|
};
|
|
}
|
|
forward(lonLat) {
|
|
const lonDeg = lonLat[0], latDeg = lonLat[1];
|
|
const lat = latDeg * deg2rad;
|
|
const xy = [
|
|
Math.cos(lat) * Math.sin(lonDeg * deg2rad) * scale,
|
|
Math.sin(lat) * scale
|
|
];
|
|
if (lonDeg < -90 || lonDeg > 90) {
|
|
xy.outside = true;
|
|
}
|
|
return xy;
|
|
}
|
|
inverse(xy) {
|
|
const x = xy[0] / scale, y = xy[1] / scale, z = Math.sqrt(x * x + y * y), c = Math.asin(z), cSin = Math.sin(c), cCos = Math.cos(c);
|
|
return [
|
|
Math.atan2(x * cSin, z * cCos) / deg2rad,
|
|
Math.asin(z && y * cSin / z) / deg2rad
|
|
];
|
|
}
|
|
}
|
|
|
|
return Orthographic;
|
|
});
|
|
_registerModule(_modules, 'Maps/Projections/WebMercator.js', [], function () {
|
|
/* *
|
|
* Web Mercator projection, used for most online map tile services
|
|
* */
|
|
const maxLatitude = 85.0511287798, // The latitude that defines a square
|
|
r = 63.78137, deg2rad = Math.PI / 180;
|
|
class WebMercator {
|
|
constructor() {
|
|
this.bounds = {
|
|
x1: -200.37508342789243,
|
|
x2: 200.37508342789243,
|
|
y1: -200.3750834278071,
|
|
y2: 200.3750834278071
|
|
};
|
|
this.maxLatitude = maxLatitude;
|
|
}
|
|
forward(lonLat) {
|
|
const sinLat = Math.sin(lonLat[1] * deg2rad);
|
|
const xy = [
|
|
r * lonLat[0] * deg2rad,
|
|
r * Math.log((1 + sinLat) / (1 - sinLat)) / 2
|
|
];
|
|
if (Math.abs(lonLat[1]) > maxLatitude) {
|
|
xy.outside = true;
|
|
}
|
|
return xy;
|
|
}
|
|
inverse(xy) {
|
|
return [
|
|
xy[0] / (r * deg2rad),
|
|
(2 * Math.atan(Math.exp(xy[1] / r)) - (Math.PI / 2)) / deg2rad
|
|
];
|
|
}
|
|
}
|
|
|
|
return WebMercator;
|
|
});
|
|
_registerModule(_modules, 'Maps/Projections/ProjectionRegistry.js', [_modules['Maps/Projections/LambertConformalConic.js'], _modules['Maps/Projections/EqualEarth.js'], _modules['Maps/Projections/Miller.js'], _modules['Maps/Projections/Orthographic.js'], _modules['Maps/Projections/WebMercator.js']], function (LambertConformalConic, EqualEarth, Miller, Orthographic, WebMercator) {
|
|
/* *
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const registry = {
|
|
EqualEarth,
|
|
LambertConformalConic,
|
|
Miller,
|
|
Orthographic,
|
|
WebMercator
|
|
};
|
|
|
|
return registry;
|
|
});
|
|
_registerModule(_modules, 'Maps/Projection.js', [_modules['Core/Geometry/PolygonClip.js'], _modules['Maps/Projections/ProjectionRegistry.js'], _modules['Core/Utilities.js']], function (PC, registry, U) {
|
|
/* *
|
|
*
|
|
* (c) 2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { clipLineString, clipPolygon } = PC;
|
|
const { clamp, erase } = U;
|
|
const deg2rad = Math.PI * 2 / 360;
|
|
// Safe padding on either side of the antimeridian to avoid points being
|
|
// projected to the wrong side of the plane
|
|
const floatCorrection = 0.000001;
|
|
// Keep longitude within -180 and 180. This is faster than using the modulo
|
|
// operator, and preserves the distinction between -180 and 180.
|
|
const wrapLon = (lon) => {
|
|
// Replacing the if's with while would increase the range, but make it prone
|
|
// to crashes on bad data
|
|
if (lon < -180) {
|
|
lon += 360;
|
|
}
|
|
if (lon > 180) {
|
|
lon -= 360;
|
|
}
|
|
return lon;
|
|
};
|
|
class Projection {
|
|
// Add a projection definition to the registry, accessible by its `name`.
|
|
static add(name, definition) {
|
|
Projection.registry[name] = definition;
|
|
}
|
|
// Calculate the great circle between two given coordinates
|
|
static greatCircle(point1, point2, inclusive) {
|
|
const { atan2, cos, sin, sqrt } = Math;
|
|
const lat1 = point1[1] * deg2rad;
|
|
const lon1 = point1[0] * deg2rad;
|
|
const lat2 = point2[1] * deg2rad;
|
|
const lon2 = point2[0] * deg2rad;
|
|
const deltaLat = lat2 - lat1;
|
|
const deltaLng = lon2 - lon1;
|
|
const calcA = sin(deltaLat / 2) * sin(deltaLat / 2) +
|
|
cos(lat1) * cos(lat2) * sin(deltaLng / 2) * sin(deltaLng / 2);
|
|
const calcB = 2 * atan2(sqrt(calcA), sqrt(1 - calcA));
|
|
const distance = calcB * 6371e3; // in meters
|
|
const jumps = Math.round(distance / 500000); // 500 km each jump
|
|
const lineString = [];
|
|
if (inclusive) {
|
|
lineString.push(point1);
|
|
}
|
|
if (jumps > 1) {
|
|
const step = 1 / jumps;
|
|
for (let fraction = step; fraction < 0.999; // Account for float errors
|
|
fraction += step) {
|
|
const A = sin((1 - fraction) * calcB) / sin(calcB);
|
|
const B = sin(fraction * calcB) / sin(calcB);
|
|
const x = A * cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2);
|
|
const y = A * cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2);
|
|
const z = A * sin(lat1) + B * sin(lat2);
|
|
const lat3 = atan2(z, sqrt(x * x + y * y));
|
|
const lon3 = atan2(y, x);
|
|
lineString.push([lon3 / deg2rad, lat3 / deg2rad]);
|
|
}
|
|
}
|
|
if (inclusive) {
|
|
lineString.push(point2);
|
|
}
|
|
return lineString;
|
|
}
|
|
static insertGreatCircles(poly) {
|
|
let i = poly.length - 1;
|
|
while (i--) {
|
|
// Distance in degrees, either in lon or lat. Avoid heavy
|
|
// calculation of true distance.
|
|
const roughDistance = Math.max(Math.abs(poly[i][0] - poly[i + 1][0]), Math.abs(poly[i][1] - poly[i + 1][1]));
|
|
if (roughDistance > 10) {
|
|
const greatCircle = Projection.greatCircle(poly[i], poly[i + 1]);
|
|
if (greatCircle.length) {
|
|
poly.splice(i + 1, 0, ...greatCircle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static toString(options) {
|
|
const { name, rotation } = options || {};
|
|
return [name, rotation && rotation.join(',')].join(';');
|
|
}
|
|
constructor(options = {}) {
|
|
// Whether the chart has points, lines or polygons given as coordinates
|
|
// with positive up, as opposed to paths in the SVG plane with positive
|
|
// down.
|
|
this.hasCoordinates = false;
|
|
// Whether the chart has true projection as opposed to pre-projected geojson
|
|
// as in the legacy map collection.
|
|
this.hasGeoProjection = false;
|
|
this.maxLatitude = 90;
|
|
this.options = options;
|
|
const { name, projectedBounds, rotation } = options;
|
|
this.rotator = rotation ? this.getRotator(rotation) : void 0;
|
|
const ProjectionDefinition = name ? Projection.registry[name] : void 0;
|
|
if (ProjectionDefinition) {
|
|
this.def = new ProjectionDefinition(options);
|
|
}
|
|
const { def, rotator } = this;
|
|
if (def) {
|
|
this.maxLatitude = def.maxLatitude || 90;
|
|
this.hasGeoProjection = true;
|
|
}
|
|
if (rotator && def) {
|
|
this.forward = (lonLat) => def.forward(rotator.forward(lonLat));
|
|
this.inverse = (xy) => rotator.inverse(def.inverse(xy));
|
|
}
|
|
else if (def) {
|
|
this.forward = (lonLat) => def.forward(lonLat);
|
|
this.inverse = (xy) => def.inverse(xy);
|
|
}
|
|
else if (rotator) {
|
|
this.forward = rotator.forward;
|
|
this.inverse = rotator.inverse;
|
|
}
|
|
// Projected bounds/clipping
|
|
this.bounds = projectedBounds === 'world' ?
|
|
def && def.bounds :
|
|
projectedBounds;
|
|
}
|
|
lineIntersectsBounds(line) {
|
|
const { x1, x2, y1, y2 } = this.bounds || {};
|
|
const getIntersect = (line, dim, val) => {
|
|
const [p1, p2] = line, otherDim = dim ? 0 : 1;
|
|
// Check if points are on either side of the line
|
|
if (typeof val === 'number' && p1[dim] >= val !== p2[dim] >= val) {
|
|
const fraction = ((val - p1[dim]) / (p2[dim] - p1[dim])), crossingVal = p1[otherDim] +
|
|
fraction * (p2[otherDim] - p1[otherDim]);
|
|
return dim ? [crossingVal, val] : [val, crossingVal];
|
|
}
|
|
};
|
|
let intersection, ret = line[0];
|
|
if ((intersection = getIntersect(line, 0, x1))) {
|
|
ret = intersection;
|
|
// Assuming line[1] was originally outside, replace it with the
|
|
// intersection point so that the horizontal intersection will
|
|
// be correct.
|
|
line[1] = intersection;
|
|
}
|
|
else if ((intersection = getIntersect(line, 0, x2))) {
|
|
ret = intersection;
|
|
line[1] = intersection;
|
|
}
|
|
if ((intersection = getIntersect(line, 1, y1))) {
|
|
ret = intersection;
|
|
}
|
|
else if ((intersection = getIntersect(line, 1, y2))) {
|
|
ret = intersection;
|
|
}
|
|
return ret;
|
|
}
|
|
/*
|
|
* Take the rotation options and return the appropriate projection functions
|
|
*/
|
|
getRotator(rotation) {
|
|
const deltaLambda = rotation[0] * deg2rad, deltaPhi = (rotation[1] || 0) * deg2rad, deltaGamma = (rotation[2] || 0) * deg2rad;
|
|
const cosDeltaPhi = Math.cos(deltaPhi), sinDeltaPhi = Math.sin(deltaPhi), cosDeltaGamma = Math.cos(deltaGamma), sinDeltaGamma = Math.sin(deltaGamma);
|
|
if (deltaLambda === 0 && deltaPhi === 0 && deltaGamma === 0) {
|
|
// Don't waste processing time
|
|
return;
|
|
}
|
|
return {
|
|
forward: (lonLat) => {
|
|
// Lambda (lon) rotation
|
|
const lon = lonLat[0] * deg2rad + deltaLambda;
|
|
// Phi (lat) and gamma rotation
|
|
const lat = lonLat[1] * deg2rad, cosLat = Math.cos(lat), x = Math.cos(lon) * cosLat, y = Math.sin(lon) * cosLat, sinLat = Math.sin(lat), k = sinLat * cosDeltaPhi + x * sinDeltaPhi;
|
|
return [
|
|
Math.atan2(y * cosDeltaGamma - k * sinDeltaGamma, x * cosDeltaPhi - sinLat * sinDeltaPhi) / deg2rad,
|
|
Math.asin(k * cosDeltaGamma + y * sinDeltaGamma) / deg2rad
|
|
];
|
|
},
|
|
inverse: (rLonLat) => {
|
|
// Lambda (lon) unrotation
|
|
const lon = rLonLat[0] * deg2rad;
|
|
// Phi (lat) and gamma unrotation
|
|
const lat = rLonLat[1] * deg2rad, cosLat = Math.cos(lat), x = Math.cos(lon) * cosLat, y = Math.sin(lon) * cosLat, sinLat = Math.sin(lat), k = sinLat * cosDeltaGamma - y * sinDeltaGamma;
|
|
return [
|
|
(Math.atan2(y * cosDeltaGamma + sinLat * sinDeltaGamma, x * cosDeltaPhi + k * sinDeltaPhi) - deltaLambda) / deg2rad,
|
|
Math.asin(k * cosDeltaPhi - x * sinDeltaPhi) / deg2rad
|
|
];
|
|
}
|
|
};
|
|
}
|
|
// Project a lonlat coordinate position to xy. Dynamically overridden when
|
|
// projection is set.
|
|
forward(lonLat) {
|
|
return lonLat;
|
|
}
|
|
// Unproject an xy chart coordinate position to lonlat. Dynamically
|
|
// overridden when projection is set.
|
|
inverse(xy) {
|
|
return xy;
|
|
}
|
|
cutOnAntimeridian(poly, isPolygon) {
|
|
const antimeridian = 180;
|
|
const intersections = [];
|
|
const polygons = [poly];
|
|
poly.forEach((lonLat, i) => {
|
|
let previousLonLat = poly[i - 1];
|
|
if (!i) {
|
|
if (!isPolygon) {
|
|
return;
|
|
}
|
|
// Else, wrap to beginning
|
|
previousLonLat = poly[poly.length - 1];
|
|
}
|
|
const lon1 = previousLonLat[0], lon2 = lonLat[0];
|
|
if (
|
|
// Both points, after rotating for antimeridian, are on the far
|
|
// side of the Earth
|
|
(lon1 < -90 || lon1 > 90) &&
|
|
(lon2 < -90 || lon2 > 90) &&
|
|
// ... and on either side of the plane
|
|
(lon1 > 0) !== (lon2 > 0)) {
|
|
// Interpolate to the intersection latitude
|
|
const fraction = clamp((antimeridian - (lon1 + 360) % 360) /
|
|
((lon2 + 360) % 360 - (lon1 + 360) % 360), 0, 1), lat = (previousLonLat[1] +
|
|
fraction * (lonLat[1] - previousLonLat[1]));
|
|
intersections.push({
|
|
i,
|
|
lat,
|
|
direction: lon1 < 0 ? 1 : -1,
|
|
previousLonLat,
|
|
lonLat
|
|
});
|
|
}
|
|
});
|
|
let polarIntersection;
|
|
if (intersections.length) {
|
|
if (isPolygon) {
|
|
// Simplified use of the even-odd rule, if there is an odd
|
|
// amount of intersections between the polygon and the
|
|
// antimeridian, the pole is inside the polygon. Applies
|
|
// primarily to Antarctica.
|
|
if (intersections.length % 2 === 1) {
|
|
polarIntersection = intersections.slice().sort((a, b) => Math.abs(b.lat) - Math.abs(a.lat))[0];
|
|
erase(intersections, polarIntersection);
|
|
}
|
|
// Pull out slices of the polygon that is on the opposite side
|
|
// of the antimeridian compared to the starting point
|
|
let i = intersections.length - 2;
|
|
while (i >= 0) {
|
|
const index = intersections[i].i;
|
|
const lonPlus = wrapLon(antimeridian +
|
|
intersections[i].direction * floatCorrection);
|
|
const lonMinus = wrapLon(antimeridian -
|
|
intersections[i].direction * floatCorrection);
|
|
const slice = poly.splice(index, intersections[i + 1].i - index,
|
|
// Add interpolated points close to the cut
|
|
...Projection.greatCircle([lonPlus, intersections[i].lat], [lonPlus, intersections[i + 1].lat], true));
|
|
// Add interpolated points close to the cut
|
|
slice.push(...Projection.greatCircle([lonMinus, intersections[i + 1].lat], [lonMinus, intersections[i].lat], true));
|
|
polygons.push(slice);
|
|
i -= 2;
|
|
}
|
|
// Insert dummy points close to the pole
|
|
if (polarIntersection) {
|
|
for (let i = 0; i < polygons.length; i++) {
|
|
const { direction, lat } = polarIntersection, poly = polygons[i], indexOf = poly.indexOf(polarIntersection.lonLat);
|
|
if (indexOf > -1) {
|
|
const polarLatitude = (lat < 0 ? -1 : 1) *
|
|
this.maxLatitude;
|
|
const lon1 = wrapLon(antimeridian +
|
|
direction * floatCorrection);
|
|
const lon2 = wrapLon(antimeridian -
|
|
direction * floatCorrection);
|
|
const polarSegment = Projection.greatCircle([lon1, lat], [lon1, polarLatitude], true);
|
|
// Circle around the pole point in order to make
|
|
// polygon clipping right. Without this, Antarctica
|
|
// would wrap the wrong way in an LLC projection
|
|
// with parallels [30, 40].
|
|
for (let lon = lon1 + 120 * direction; lon > -180 && lon < 180; lon += 120 * direction) {
|
|
polarSegment.push([lon, polarLatitude]);
|
|
}
|
|
polarSegment.push(...Projection.greatCircle([lon2, polarLatitude], [lon2, polarIntersection.lat], true));
|
|
poly.splice(indexOf, 0, ...polarSegment);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Map lines, not closed
|
|
}
|
|
else {
|
|
let i = intersections.length;
|
|
while (i--) {
|
|
const index = intersections[i].i;
|
|
const slice = poly.splice(index, poly.length,
|
|
// Add interpolated point close to the cut
|
|
[
|
|
wrapLon(antimeridian +
|
|
intersections[i].direction * floatCorrection),
|
|
intersections[i].lat
|
|
]);
|
|
// Add interpolated point close to the cut
|
|
slice.unshift([
|
|
wrapLon(antimeridian -
|
|
intersections[i].direction * floatCorrection),
|
|
intersections[i].lat
|
|
]);
|
|
polygons.push(slice);
|
|
}
|
|
}
|
|
}
|
|
return polygons;
|
|
}
|
|
// Take a GeoJSON geometry and return a translated SVGPath
|
|
path(geometry) {
|
|
const { bounds, def, rotator } = this;
|
|
const antimeridian = 180;
|
|
const path = [];
|
|
const isPolygon = geometry.type === 'Polygon' ||
|
|
geometry.type === 'MultiPolygon';
|
|
// @todo: It doesn't really have to do with whether north is
|
|
// positive. It depends on whether the coordinates are
|
|
// pre-projected.
|
|
const hasGeoProjection = this.hasGeoProjection;
|
|
// Detect whether we need to do antimeridian cutting and clipping to
|
|
// bounds. The alternative (currently for Orthographic) is to apply a
|
|
// clip angle.
|
|
const projectingToPlane = !def || def.antimeridianCutting !== false;
|
|
// We need to rotate in a separate step before applying antimeridian
|
|
// cutting
|
|
const preclip = projectingToPlane ? rotator : void 0;
|
|
const postclip = projectingToPlane ? (def || this) : this;
|
|
let boundsPolygon;
|
|
if (bounds) {
|
|
boundsPolygon = [
|
|
[bounds.x1, bounds.y1],
|
|
[bounds.x2, bounds.y1],
|
|
[bounds.x2, bounds.y2],
|
|
[bounds.x1, bounds.y2]
|
|
];
|
|
}
|
|
const addToPath = (polygon) => {
|
|
// Create a copy of the original coordinates. The copy applies a
|
|
// correction of points close to the antimeridian in order to
|
|
// prevent the points to be projected to the wrong side of the
|
|
// plane. Float errors in topojson or in the projection may cause
|
|
// that.
|
|
const poly = polygon.map((lonLat) => {
|
|
if (projectingToPlane) {
|
|
if (preclip) {
|
|
lonLat = preclip.forward(lonLat);
|
|
}
|
|
let lon = lonLat[0];
|
|
if (Math.abs(lon - antimeridian) < floatCorrection) {
|
|
if (lon < antimeridian) {
|
|
lon = antimeridian - floatCorrection;
|
|
}
|
|
else {
|
|
lon = antimeridian + floatCorrection;
|
|
}
|
|
}
|
|
lonLat = [lon, lonLat[1]];
|
|
}
|
|
return lonLat;
|
|
});
|
|
let polygons = [poly];
|
|
if (hasGeoProjection) {
|
|
// Insert great circles into long straight lines
|
|
Projection.insertGreatCircles(poly);
|
|
if (projectingToPlane) {
|
|
polygons = this.cutOnAntimeridian(poly, isPolygon);
|
|
}
|
|
}
|
|
polygons.forEach((poly) => {
|
|
if (poly.length < 2) {
|
|
return;
|
|
}
|
|
let movedTo = false;
|
|
let firstValidLonLat;
|
|
let lastValidLonLat;
|
|
let gap = false;
|
|
const pushToPath = (point) => {
|
|
if (!movedTo) {
|
|
path.push(['M', point[0], point[1]]);
|
|
movedTo = true;
|
|
}
|
|
else {
|
|
path.push(['L', point[0], point[1]]);
|
|
}
|
|
};
|
|
let someOutside = false, someInside = false;
|
|
let points = poly.map((lonLat) => {
|
|
const xy = postclip.forward(lonLat);
|
|
if (xy.outside) {
|
|
someOutside = true;
|
|
}
|
|
else {
|
|
someInside = true;
|
|
}
|
|
// Mercator projects pole points to Infinity, and
|
|
// clipPolygon is not able to handle it.
|
|
if (xy[1] === Infinity) {
|
|
xy[1] = 10e9;
|
|
}
|
|
else if (xy[1] === -Infinity) {
|
|
xy[1] = -10e9;
|
|
}
|
|
return xy;
|
|
});
|
|
if (projectingToPlane) {
|
|
// Wrap around in order for pointInPolygon to work
|
|
if (isPolygon) {
|
|
points.push(points[0]);
|
|
}
|
|
if (someOutside) {
|
|
// All points are outside
|
|
if (!someInside) {
|
|
return;
|
|
}
|
|
// Some inside, some outside. Clip to the bounds.
|
|
if (boundsPolygon) {
|
|
// Polygons
|
|
if (isPolygon) {
|
|
points = clipPolygon(points, boundsPolygon);
|
|
// Linestrings
|
|
}
|
|
else if (bounds) {
|
|
clipLineString(points, boundsPolygon)
|
|
.forEach((points) => {
|
|
movedTo = false;
|
|
points.forEach(pushToPath);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
points.forEach(pushToPath);
|
|
// For orthographic projection, or when a clipAngle applies
|
|
}
|
|
else {
|
|
for (let i = 0; i < points.length; i++) {
|
|
const lonLat = poly[i], point = points[i];
|
|
if (!point.outside) {
|
|
// In order to be able to interpolate if the first
|
|
// or last point is invalid (on the far side of the
|
|
// globe in an orthographic projection), we need to
|
|
// push the first valid point to the end of the
|
|
// polygon.
|
|
if (isPolygon && !firstValidLonLat) {
|
|
firstValidLonLat = lonLat;
|
|
poly.push(lonLat);
|
|
points.push(point);
|
|
}
|
|
// When entering the first valid point after a gap
|
|
// of invalid points, typically on the far side of
|
|
// the globe in an orthographic projection.
|
|
if (gap && lastValidLonLat) {
|
|
// For areas, in an orthographic projection, the
|
|
// great circle between two visible points will
|
|
// be close to the horizon. A possible exception
|
|
// may be when the two points are on opposite
|
|
// sides of the globe. It that poses a problem,
|
|
// we may have to rewrite this to use the small
|
|
// circle related to the current lon0 and lat0.
|
|
if (isPolygon && hasGeoProjection) {
|
|
const greatCircle = Projection.greatCircle(lastValidLonLat, lonLat);
|
|
greatCircle.forEach((lonLat) => pushToPath(postclip.forward(lonLat)));
|
|
// For lines, just jump over the gap
|
|
}
|
|
else {
|
|
movedTo = false;
|
|
}
|
|
}
|
|
pushToPath(point);
|
|
lastValidLonLat = lonLat;
|
|
gap = false;
|
|
}
|
|
else {
|
|
gap = true;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
if (geometry.type === 'LineString') {
|
|
addToPath(geometry.coordinates);
|
|
}
|
|
else if (geometry.type === 'MultiLineString') {
|
|
geometry.coordinates.forEach((c) => addToPath(c));
|
|
}
|
|
else if (geometry.type === 'Polygon') {
|
|
geometry.coordinates.forEach((c) => addToPath(c));
|
|
if (path.length) {
|
|
path.push(['Z']);
|
|
}
|
|
}
|
|
else if (geometry.type === 'MultiPolygon') {
|
|
geometry.coordinates.forEach((polygons) => {
|
|
polygons.forEach((c) => addToPath(c));
|
|
});
|
|
if (path.length) {
|
|
path.push(['Z']);
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
}
|
|
Projection.registry = registry;
|
|
|
|
return Projection;
|
|
});
|
|
_registerModule(_modules, 'Maps/MapView.js', [_modules['Maps/MapViewOptionsDefault.js'], _modules['Maps/MapViewInsetsOptionsDefault.js'], _modules['Extensions/GeoJSON.js'], _modules['Core/Chart/MapChart.js'], _modules['Maps/MapUtilities.js'], _modules['Maps/Projection.js'], _modules['Core/Utilities.js']], function (defaultOptions, defaultInsetsOptions, GeoJSONModule, MapChart, MU, Projection, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2020 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { topo2geo } = GeoJSONModule;
|
|
const { maps } = MapChart;
|
|
const { boundsFromPath, pointInPolygon } = MU;
|
|
const { addEvent, clamp, fireEvent, isArray, isNumber, isObject, isString, merge, pick, relativeLength } = U;
|
|
/**
|
|
* The world size in terms of 10k meters in the Web Mercator projection, to
|
|
* match a 256 square tile to zoom level 0.
|
|
* @private
|
|
*/
|
|
const worldSize = 400.979322;
|
|
const tileSize = 256;
|
|
// Compute the zoom from given bounds and the size of the playing field. Used in
|
|
// two places, hence the local function.
|
|
const zoomFromBounds = (b, playingField) => {
|
|
const { width, height } = playingField, scaleToField = Math.max((b.x2 - b.x1) / (width / tileSize), (b.y2 - b.y1) / (height / tileSize));
|
|
return Math.log(worldSize / scaleToField) / Math.log(2);
|
|
};
|
|
/*
|
|
const mergeCollections = <
|
|
T extends Array<AnyRecord|undefined>
|
|
>(a: T, b: T): T => {
|
|
b.forEach((newer, i): void => {
|
|
// Only merge by id supported for now. We may consider later to support
|
|
// more complex rules like those of `Chart.update` with `oneToOne`, but
|
|
// it is probably not needed. Existing insets can be disabled by
|
|
// overwriting the `geoBounds` with empty data.
|
|
if (newer && isString(newer.id)) {
|
|
const older = U.find(
|
|
a,
|
|
(aItem): boolean => (aItem && aItem.id) === newer.id
|
|
);
|
|
if (older) {
|
|
const aIndex = a.indexOf(older);
|
|
a[aIndex] = merge(older, newer);
|
|
}
|
|
}
|
|
});
|
|
return a;
|
|
};
|
|
*/
|
|
/**
|
|
* The map view handles zooming and centering on the map, and various
|
|
* client-side projection capabilities.
|
|
*
|
|
* On a chart instance of `MapChart`, the map view is available as `chart.mapView`.
|
|
*
|
|
* @class
|
|
* @name Highcharts.MapView
|
|
*
|
|
* @param {Highcharts.MapChart} chart
|
|
* The MapChart instance
|
|
* @param {Highcharts.MapViewOptions} options
|
|
* MapView options
|
|
*/
|
|
class MapView {
|
|
// Merge two collections of insets by the id
|
|
static mergeInsets(a, b) {
|
|
const toObject = (insets) => {
|
|
const ob = {};
|
|
insets.forEach((inset, i) => {
|
|
ob[inset && inset.id || `i${i}`] = inset;
|
|
});
|
|
return ob;
|
|
};
|
|
const insetsObj = merge(toObject(a), toObject(b)), insets = Object
|
|
.keys(insetsObj)
|
|
.map((key) => insetsObj[key]);
|
|
return insets;
|
|
}
|
|
// Create MapViewInset instances from insets options
|
|
createInsets() {
|
|
const options = this.options, insets = options.insets;
|
|
if (insets) {
|
|
insets.forEach((item) => {
|
|
const inset = new MapViewInset(this, merge(options.insetOptions, item));
|
|
this.insets.push(inset);
|
|
});
|
|
}
|
|
}
|
|
constructor(chart, options) {
|
|
this.allowTransformAnimation = true;
|
|
this.insets = [];
|
|
this.padding = [0, 0, 0, 0];
|
|
this.eventsToUnbind = [];
|
|
let recommendedMapView;
|
|
let recommendedProjection;
|
|
if (!(this instanceof MapViewInset)) {
|
|
// Handle the global map and series-level mapData
|
|
const geoMaps = [
|
|
chart.options.chart.map,
|
|
...(chart.options.series || []).map((s) => s.mapData)
|
|
]
|
|
.map((mapData) => this.getGeoMap(mapData));
|
|
const allGeoBounds = [];
|
|
geoMaps.forEach((geoMap) => {
|
|
if (geoMap) {
|
|
// Use the first geo map as main
|
|
if (!recommendedMapView) {
|
|
recommendedMapView =
|
|
geoMap['hc-recommended-mapview'];
|
|
}
|
|
// Combine the bounding boxes of all loaded maps
|
|
if (geoMap.bbox) {
|
|
const [x1, y1, x2, y2] = geoMap.bbox;
|
|
allGeoBounds.push({ x1, y1, x2, y2 });
|
|
}
|
|
}
|
|
});
|
|
// Get the composite bounds
|
|
const geoBounds = (allGeoBounds.length &&
|
|
MapView.compositeBounds(allGeoBounds));
|
|
// Provide a best-guess recommended projection if not set in
|
|
// the map or in user options
|
|
fireEvent(chart, 'beforeMapViewInit', {
|
|
geoBounds
|
|
}, function () {
|
|
if (geoBounds) {
|
|
const { x1, y1, x2, y2 } = geoBounds;
|
|
recommendedProjection =
|
|
(x2 - x1 > 180 && y2 - y1 > 90) ?
|
|
// Wide angle, go for the world view
|
|
{
|
|
name: 'EqualEarth'
|
|
} :
|
|
// Narrower angle, use a projection better
|
|
// suited for local view
|
|
{
|
|
name: 'LambertConformalConic',
|
|
parallels: [y1, y2],
|
|
rotation: [-(x1 + x2) / 2]
|
|
};
|
|
}
|
|
});
|
|
// Register the main geo map (from options.chart.map) if set
|
|
this.geoMap = geoMaps[0];
|
|
}
|
|
this.userOptions = options || {};
|
|
if (chart.options.mapView &&
|
|
chart.options.mapView.recommendedMapView) {
|
|
recommendedMapView = chart.options.mapView.recommendedMapView;
|
|
}
|
|
const o = merge(defaultOptions, { projection: recommendedProjection }, recommendedMapView, options);
|
|
// Merge the inset collections by id, or index if id missing
|
|
const recInsets = recommendedMapView && recommendedMapView.insets, optInsets = options && options.insets;
|
|
if (recInsets && optInsets) {
|
|
o.insets = MapView.mergeInsets(recInsets, optInsets);
|
|
}
|
|
this.chart = chart;
|
|
/**
|
|
* The current center of the view in terms of `[longitude, latitude]`.
|
|
* @name Highcharts.MapView#center
|
|
* @readonly
|
|
* @type {LonLatArray}
|
|
*/
|
|
this.center = o.center;
|
|
this.options = o;
|
|
this.projection = new Projection(o.projection);
|
|
// Initialize with full plot box so we don't have to check for undefined
|
|
// every time we use it
|
|
this.playingField = chart.plotBox;
|
|
/**
|
|
* The current zoom level of the view.
|
|
* @name Highcharts.MapView#zoom
|
|
* @readonly
|
|
* @type {number}
|
|
*/
|
|
this.zoom = o.zoom || 0;
|
|
this.minZoom = o.minZoom;
|
|
// Create the insets
|
|
this.createInsets();
|
|
// Initialize and respond to chart size changes
|
|
this.eventsToUnbind.push(addEvent(chart, 'afterSetChartSize', () => {
|
|
this.playingField = this.getField();
|
|
if (this.minZoom === void 0 || // When initializing the chart
|
|
this.minZoom === this.zoom // When resizing the chart
|
|
) {
|
|
this.fitToBounds(void 0, void 0, false);
|
|
if (
|
|
// Set zoom only when initializing the chart
|
|
// (do not overwrite when zooming in/out, #17082)
|
|
!this.chart.hasRendered &&
|
|
isNumber(this.userOptions.zoom)) {
|
|
this.zoom = this.userOptions.zoom;
|
|
}
|
|
if (this.userOptions.center) {
|
|
merge(true, this.center, this.userOptions.center);
|
|
}
|
|
}
|
|
}));
|
|
this.setUpEvents();
|
|
}
|
|
/**
|
|
* Fit the view to given bounds
|
|
*
|
|
* @function Highcharts.MapView#fitToBounds
|
|
* @param {Object} bounds
|
|
* Bounds in terms of projected units given as `{ x1, y1, x2, y2 }`.
|
|
* If not set, fit to the bounds of the current data set
|
|
* @param {number|string} [padding=0]
|
|
* Padding inside the bounds. A number signifies pixels, while a
|
|
* percentage string (like `5%`) can be used as a fraction of the
|
|
* plot area size.
|
|
* @param {boolean} [redraw=true]
|
|
* Whether to redraw the chart immediately
|
|
* @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation]
|
|
* What animation to use for redraw
|
|
*/
|
|
fitToBounds(bounds, padding, redraw = true, animation) {
|
|
const b = bounds || this.getProjectedBounds();
|
|
if (b) {
|
|
const pad = pick(padding, bounds ? 0 : this.options.padding), fullField = this.getField(false), padArr = isArray(pad) ? pad : [pad, pad, pad, pad];
|
|
this.padding = [
|
|
relativeLength(padArr[0], fullField.height),
|
|
relativeLength(padArr[1], fullField.width),
|
|
relativeLength(padArr[2], fullField.height),
|
|
relativeLength(padArr[3], fullField.width)
|
|
];
|
|
// Apply the playing field, corrected with padding
|
|
this.playingField = this.getField();
|
|
const zoom = zoomFromBounds(b, this.playingField);
|
|
// Reset minZoom when fitting to natural bounds
|
|
if (!bounds) {
|
|
this.minZoom = zoom;
|
|
}
|
|
const center = this.projection.inverse([
|
|
(b.x2 + b.x1) / 2,
|
|
(b.y2 + b.y1) / 2
|
|
]);
|
|
this.setView(center, zoom, redraw, animation);
|
|
}
|
|
}
|
|
getField(padded = true) {
|
|
const padding = padded ? this.padding : [0, 0, 0, 0];
|
|
return {
|
|
x: padding[3],
|
|
y: padding[0],
|
|
width: this.chart.plotWidth - padding[1] - padding[3],
|
|
height: this.chart.plotHeight - padding[0] - padding[2]
|
|
};
|
|
}
|
|
getGeoMap(map) {
|
|
if (isString(map)) {
|
|
if (maps[map] && maps[map].type === 'Topology') {
|
|
return topo2geo(maps[map]);
|
|
}
|
|
return maps[map];
|
|
}
|
|
if (isObject(map, true)) {
|
|
if (map.type === 'FeatureCollection') {
|
|
return map;
|
|
}
|
|
if (map.type === 'Topology') {
|
|
return topo2geo(map);
|
|
}
|
|
}
|
|
}
|
|
getMapBBox() {
|
|
const bounds = this.getProjectedBounds(), scale = this.getScale();
|
|
if (bounds) {
|
|
const padding = this.padding, p1 = this.projectedUnitsToPixels({
|
|
x: bounds.x1,
|
|
y: bounds.y2
|
|
}), width = ((bounds.x2 - bounds.x1) * scale +
|
|
padding[1] + padding[3]), height = ((bounds.y2 - bounds.y1) * scale +
|
|
padding[0] + padding[2]);
|
|
return {
|
|
width,
|
|
height,
|
|
x: p1.x - padding[3],
|
|
y: p1.y - padding[0]
|
|
};
|
|
}
|
|
}
|
|
getProjectedBounds() {
|
|
const projection = this.projection;
|
|
const allBounds = this.chart.series.reduce((acc, s) => {
|
|
const bounds = s.getProjectedBounds && s.getProjectedBounds();
|
|
if (bounds &&
|
|
s.options.affectsMapView !== false) {
|
|
acc.push(bounds);
|
|
}
|
|
return acc;
|
|
}, []);
|
|
// The bounds option
|
|
const fitToGeometry = this.options.fitToGeometry;
|
|
if (fitToGeometry) {
|
|
if (!this.fitToGeometryCache) {
|
|
if (fitToGeometry.type === 'MultiPoint') {
|
|
const positions = fitToGeometry.coordinates
|
|
.map((lonLat) => projection.forward(lonLat)), xs = positions.map((pos) => pos[0]), ys = positions.map((pos) => pos[1]);
|
|
this.fitToGeometryCache = {
|
|
x1: Math.min.apply(0, xs),
|
|
x2: Math.max.apply(0, xs),
|
|
y1: Math.min.apply(0, ys),
|
|
y2: Math.max.apply(0, ys)
|
|
};
|
|
}
|
|
else {
|
|
this.fitToGeometryCache = boundsFromPath(projection.path(fitToGeometry));
|
|
}
|
|
}
|
|
return this.fitToGeometryCache;
|
|
}
|
|
return this.projection.bounds || MapView.compositeBounds(allBounds);
|
|
}
|
|
getScale() {
|
|
// A zoom of 0 means the world (360x360 degrees) fits in a 256x256 px
|
|
// tile
|
|
return (tileSize / worldSize) * Math.pow(2, this.zoom);
|
|
}
|
|
// Calculate the SVG transform to be applied to series groups
|
|
getSVGTransform() {
|
|
const { x, y, width, height } = this.playingField, projectedCenter = this.projection.forward(this.center), flipFactor = this.projection.hasCoordinates ? -1 : 1, scaleX = this.getScale(), scaleY = scaleX * flipFactor, translateX = x + width / 2 - projectedCenter[0] * scaleX, translateY = y + height / 2 - projectedCenter[1] * scaleY;
|
|
return { scaleX, scaleY, translateX, translateY };
|
|
}
|
|
/**
|
|
* Convert map coordinates in longitude/latitude to pixels
|
|
*
|
|
* @function Highcharts.MapView#lonLatToPixels
|
|
* @since 10.0.0
|
|
* @param {Highcharts.MapLonLatObject} lonLat
|
|
* The map coordinates
|
|
* @return {Highcharts.PositionObject|undefined}
|
|
* The pixel position
|
|
*/
|
|
lonLatToPixels(lonLat) {
|
|
const pos = this.lonLatToProjectedUnits(lonLat);
|
|
if (pos) {
|
|
return this.projectedUnitsToPixels(pos);
|
|
}
|
|
}
|
|
/**
|
|
* Get projected units from longitude/latitude. Insets are accounted for.
|
|
* Returns an object with x and y values corresponding to positions on the
|
|
* projected plane.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @function Highcharts.MapView#lonLatToProjectedUnits
|
|
*
|
|
* @since 10.0.0
|
|
* @sample maps/series/latlon-to-point/ Find a point from lon/lat
|
|
*
|
|
* @param {Highcharts.MapLonLatObject} lonLat Coordinates.
|
|
*
|
|
* @return {Highcharts.ProjectedXY} X and Y coordinates in terms of
|
|
* projected values
|
|
*/
|
|
lonLatToProjectedUnits(lonLat) {
|
|
const chart = this.chart, mapTransforms = chart.mapTransforms;
|
|
// Legacy, built-in transforms
|
|
if (mapTransforms) {
|
|
for (const transform in mapTransforms) {
|
|
if (Object.hasOwnProperty.call(mapTransforms, transform) &&
|
|
mapTransforms[transform].hitZone) {
|
|
const coords = chart.transformFromLatLon(lonLat, mapTransforms[transform]);
|
|
if (coords && pointInPolygon(coords, mapTransforms[transform].hitZone.coordinates[0])) {
|
|
return coords;
|
|
}
|
|
}
|
|
}
|
|
return chart.transformFromLatLon(lonLat, mapTransforms['default'] // eslint-disable-line dot-notation
|
|
);
|
|
}
|
|
// Handle insets
|
|
for (const inset of this.insets) {
|
|
if (inset.options.geoBounds &&
|
|
pointInPolygon({ x: lonLat.lon, y: lonLat.lat }, inset.options.geoBounds.coordinates[0])) {
|
|
const insetProjectedPoint = inset.projection.forward([lonLat.lon, lonLat.lat]), pxPoint = inset.projectedUnitsToPixels({ x: insetProjectedPoint[0], y: insetProjectedPoint[1] });
|
|
return this.pixelsToProjectedUnits(pxPoint);
|
|
}
|
|
}
|
|
const point = this.projection.forward([lonLat.lon, lonLat.lat]);
|
|
if (!point.outside) {
|
|
return { x: point[0], y: point[1] };
|
|
}
|
|
}
|
|
/**
|
|
* Calculate longitude/latitude values for a point or position. Returns an
|
|
* object with the numeric properties `lon` and `lat`.
|
|
*
|
|
* @requires modules/map
|
|
*
|
|
* @function Highcharts.MapView#projectedUnitsToLonLat
|
|
*
|
|
* @since 10.0.0
|
|
*
|
|
* @sample maps/demo/latlon-advanced/ Advanced lat/lon demo
|
|
*
|
|
* @param {Highcharts.Point|Highcharts.ProjectedXY} point
|
|
* A `Point` instance or anything containing `x` and `y` properties
|
|
* with numeric values.
|
|
*
|
|
* @return {Highcharts.MapLonLatObject|undefined} An object with `lat` and
|
|
* `lon` properties.
|
|
*/
|
|
projectedUnitsToLonLat(point) {
|
|
const chart = this.chart, mapTransforms = chart.mapTransforms;
|
|
// Legacy, built-in transforms
|
|
if (mapTransforms) {
|
|
for (const transform in mapTransforms) {
|
|
if (Object.hasOwnProperty.call(mapTransforms, transform) &&
|
|
mapTransforms[transform].hitZone &&
|
|
pointInPolygon(point, mapTransforms[transform].hitZone.coordinates[0])) {
|
|
return chart.transformToLatLon(point, mapTransforms[transform]);
|
|
}
|
|
}
|
|
return chart.transformToLatLon(point, mapTransforms['default'] // eslint-disable-line dot-notation
|
|
);
|
|
}
|
|
const pxPoint = this.projectedUnitsToPixels(point);
|
|
for (const inset of this.insets) {
|
|
if (inset.hitZone &&
|
|
pointInPolygon(pxPoint, inset.hitZone.coordinates[0])) {
|
|
const insetProjectedPoint = inset
|
|
.pixelsToProjectedUnits(pxPoint), coordinates = inset.projection.inverse([insetProjectedPoint.x, insetProjectedPoint.y]);
|
|
return { lon: coordinates[0], lat: coordinates[1] };
|
|
}
|
|
}
|
|
const coordinates = this.projection.inverse([point.x, point.y]);
|
|
return { lon: coordinates[0], lat: coordinates[1] };
|
|
}
|
|
redraw(animation) {
|
|
this.chart.series.forEach((s) => {
|
|
if (s.useMapGeometry) {
|
|
s.isDirty = true;
|
|
}
|
|
});
|
|
this.chart.redraw(animation);
|
|
}
|
|
/**
|
|
* Set the view to given center and zoom values.
|
|
* @function Highcharts.MapView#setView
|
|
* @param {Highcharts.LonLatArray|undefined} center
|
|
* The center point
|
|
* @param {number} zoom
|
|
* The zoom level
|
|
* @param {boolean} [redraw=true]
|
|
* Whether to redraw immediately
|
|
* @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation]
|
|
* Animation options for the redraw
|
|
*
|
|
* @sample maps/mapview/setview
|
|
* Set the view programmatically
|
|
*/
|
|
setView(center, zoom, redraw = true, animation) {
|
|
if (center) {
|
|
this.center = center;
|
|
}
|
|
if (typeof zoom === 'number') {
|
|
if (typeof this.minZoom === 'number') {
|
|
zoom = Math.max(zoom, this.minZoom);
|
|
}
|
|
if (typeof this.options.maxZoom === 'number') {
|
|
zoom = Math.min(zoom, this.options.maxZoom);
|
|
}
|
|
// Use isNumber to prevent Infinity (#17205)
|
|
if (isNumber(zoom)) {
|
|
this.zoom = zoom;
|
|
}
|
|
}
|
|
const bounds = this.getProjectedBounds();
|
|
if (bounds) {
|
|
const projectedCenter = this.projection.forward(this.center), { x, y, width, height } = this.playingField, scale = this.getScale(), bottomLeft = this.projectedUnitsToPixels({
|
|
x: bounds.x1,
|
|
y: bounds.y1
|
|
}), topRight = this.projectedUnitsToPixels({
|
|
x: bounds.x2,
|
|
y: bounds.y2
|
|
}), boundsCenterProjected = [
|
|
(bounds.x1 + bounds.x2) / 2,
|
|
(bounds.y1 + bounds.y2) / 2
|
|
], isDrilling = this.chart.series.some((series) => series.isDrilling);
|
|
if (!isDrilling) {
|
|
// Constrain to data bounds
|
|
// Pixel coordinate system is reversed vs projected
|
|
const x1 = bottomLeft.x, y1 = topRight.y, x2 = topRight.x, y2 = bottomLeft.y;
|
|
// Map smaller than plot area, center it
|
|
if (x2 - x1 < width) {
|
|
projectedCenter[0] = boundsCenterProjected[0];
|
|
// Off west
|
|
}
|
|
else if (x1 < x && x2 < x + width) {
|
|
// Adjust eastwards
|
|
projectedCenter[0] +=
|
|
Math.max(x1 - x, x2 - width - x) / scale;
|
|
// Off east
|
|
}
|
|
else if (x2 > x + width && x1 > x) {
|
|
// Adjust westwards
|
|
projectedCenter[0] +=
|
|
Math.min(x2 - width - x, x1 - x) / scale;
|
|
}
|
|
// Map smaller than plot area, center it
|
|
if (y2 - y1 < height) {
|
|
projectedCenter[1] = boundsCenterProjected[1];
|
|
// Off north
|
|
}
|
|
else if (y1 < y && y2 < y + height) {
|
|
// Adjust southwards
|
|
projectedCenter[1] -=
|
|
Math.max(y1 - y, y2 - height - y) / scale;
|
|
// Off south
|
|
}
|
|
else if (y2 > y + height && y1 > y) {
|
|
// Adjust northwards
|
|
projectedCenter[1] -=
|
|
Math.min(y2 - height - y, y1 - y) / scale;
|
|
}
|
|
this.center = this.projection.inverse(projectedCenter);
|
|
}
|
|
this.insets.forEach((inset) => {
|
|
if (inset.options.field) {
|
|
inset.hitZone = inset.getHitZone();
|
|
inset.playingField = inset.getField();
|
|
}
|
|
});
|
|
this.render();
|
|
}
|
|
fireEvent(this, 'afterSetView');
|
|
if (redraw) {
|
|
this.redraw(animation);
|
|
}
|
|
}
|
|
/**
|
|
* Convert projected units to pixel position
|
|
*
|
|
* @function Highcharts.MapView#projectedUnitsToPixels
|
|
* @param {Highcharts.PositionObject} pos
|
|
* The position in projected units
|
|
* @return {Highcharts.PositionObject} The position in pixels
|
|
*/
|
|
projectedUnitsToPixels(pos) {
|
|
const scale = this.getScale(), projectedCenter = this.projection.forward(this.center), field = this.playingField, centerPxX = field.x + field.width / 2, centerPxY = field.y + field.height / 2;
|
|
const x = centerPxX - scale * (projectedCenter[0] - pos.x);
|
|
const y = centerPxY + scale * (projectedCenter[1] - pos.y);
|
|
return { x, y };
|
|
}
|
|
/**
|
|
* Convert pixel position to longitude and latitude.
|
|
*
|
|
* @function Highcharts.MapView#pixelsToLonLat
|
|
* @since 10.0.0
|
|
* @param {Highcharts.PositionObject} pos
|
|
* The position in pixels
|
|
* @return {Highcharts.MapLonLatObject|undefined}
|
|
* The map coordinates
|
|
*/
|
|
pixelsToLonLat(pos) {
|
|
return this.projectedUnitsToLonLat(this.pixelsToProjectedUnits(pos));
|
|
}
|
|
/**
|
|
* Convert pixel position to projected units
|
|
*
|
|
* @function Highcharts.MapView#pixelsToProjectedUnits
|
|
* @param {Highcharts.PositionObject} pos
|
|
* The position in pixels
|
|
* @return {Highcharts.PositionObject} The position in projected units
|
|
*/
|
|
pixelsToProjectedUnits(pos) {
|
|
const { x, y } = pos, scale = this.getScale(), projectedCenter = this.projection.forward(this.center), field = this.playingField, centerPxX = field.x + field.width / 2, centerPxY = field.y + field.height / 2;
|
|
const projectedX = projectedCenter[0] + (x - centerPxX) / scale;
|
|
const projectedY = projectedCenter[1] - (y - centerPxY) / scale;
|
|
return { x: projectedX, y: projectedY };
|
|
}
|
|
setUpEvents() {
|
|
const { chart } = this;
|
|
// Set up panning for maps. In orthographic projections the globe will
|
|
// rotate, otherwise adjust the map center.
|
|
let mouseDownCenterProjected;
|
|
let mouseDownKey;
|
|
let mouseDownRotation;
|
|
const onPan = (e) => {
|
|
const pinchDown = chart.pointer.pinchDown, projection = this.projection;
|
|
let { mouseDownX, mouseDownY } = chart;
|
|
if (pinchDown.length === 1) {
|
|
mouseDownX = pinchDown[0].chartX;
|
|
mouseDownY = pinchDown[0].chartY;
|
|
}
|
|
if (typeof mouseDownX === 'number' &&
|
|
typeof mouseDownY === 'number') {
|
|
const key = `${mouseDownX},${mouseDownY}`, { chartX, chartY } = e.originalEvent;
|
|
// Reset starting position
|
|
if (key !== mouseDownKey) {
|
|
mouseDownKey = key;
|
|
mouseDownCenterProjected = this.projection
|
|
.forward(this.center);
|
|
mouseDownRotation = (this.projection.options.rotation || [0, 0]).slice();
|
|
}
|
|
// Get the natural zoom level of the projection itself when
|
|
// zoomed to view the full world
|
|
const worldBounds = projection.def && projection.def.bounds, worldZoom = (worldBounds &&
|
|
zoomFromBounds(worldBounds, this.playingField)) || -Infinity;
|
|
// Panning rotates the globe
|
|
if (projection.options.name === 'Orthographic' &&
|
|
// ... but don't rotate if we're loading only a part of the
|
|
// world
|
|
(this.minZoom || Infinity) < worldZoom * 1.3) {
|
|
// Empirical ratio where the globe rotates roughly the same
|
|
// speed as moving the pointer across the center of the
|
|
// projection
|
|
const ratio = 440 / (this.getScale() * Math.min(chart.plotWidth, chart.plotHeight));
|
|
if (mouseDownRotation) {
|
|
const lon = (mouseDownX - chartX) * ratio -
|
|
mouseDownRotation[0], lat = clamp(-mouseDownRotation[1] -
|
|
(mouseDownY - chartY) * ratio, -80, 80), zoom = this.zoom;
|
|
this.update({
|
|
projection: {
|
|
rotation: [-lon, -lat]
|
|
}
|
|
}, false);
|
|
this.fitToBounds(void 0, void 0, false);
|
|
this.zoom = zoom;
|
|
chart.redraw(false);
|
|
}
|
|
// #17925 Skip NaN values
|
|
}
|
|
else if (isNumber(chartX) && isNumber(chartY)) {
|
|
// #17238
|
|
const scale = this.getScale(), flipFactor = this.projection.hasCoordinates ? 1 : -1;
|
|
const newCenter = this.projection.inverse([
|
|
mouseDownCenterProjected[0] +
|
|
(mouseDownX - chartX) / scale,
|
|
mouseDownCenterProjected[1] -
|
|
(mouseDownY - chartY) / scale * flipFactor
|
|
]);
|
|
this.setView(newCenter, void 0, true, false);
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
addEvent(chart, 'pan', onPan);
|
|
addEvent(chart, 'touchpan', onPan);
|
|
// Perform the map zoom by selection
|
|
addEvent(chart, 'selection', (evt) => {
|
|
// Zoom in
|
|
if (!evt.resetSelection) {
|
|
const x = evt.x - chart.plotLeft;
|
|
const y = evt.y - chart.plotTop;
|
|
const { y: y1, x: x1 } = this.pixelsToProjectedUnits({ x, y });
|
|
const { y: y2, x: x2 } = this.pixelsToProjectedUnits({ x: x + evt.width, y: y + evt.height });
|
|
this.fitToBounds({ x1, y1, x2, y2 }, void 0, true, evt.originalEvent.touches ?
|
|
// On touch zoom, don't animate, since we're already in
|
|
// transformed zoom preview
|
|
false :
|
|
// On mouse zoom, obey the chart-level animation
|
|
void 0);
|
|
// Only for mouse. Touch users can pinch out.
|
|
if (!/^touch/.test((evt.originalEvent.type))) {
|
|
chart.showResetZoom();
|
|
}
|
|
evt.preventDefault();
|
|
// Reset zoom
|
|
}
|
|
else {
|
|
this.zoomBy();
|
|
}
|
|
});
|
|
}
|
|
render() {
|
|
// We need a group for the insets
|
|
if (!this.group) {
|
|
this.group = this.chart.renderer.g('map-view')
|
|
.attr({ zIndex: 4 })
|
|
.add();
|
|
}
|
|
}
|
|
/**
|
|
* Update the view with given options
|
|
*
|
|
* @function Highcharts.MapView#update
|
|
*
|
|
* @param {Partial<Highcharts.MapViewOptions>} options
|
|
* The new map view options to apply
|
|
* @param {boolean} [redraw=true]
|
|
* Whether to redraw immediately
|
|
* @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation]
|
|
* The animation to apply to a the redraw
|
|
*/
|
|
update(options, redraw = true, animation) {
|
|
const newProjection = options.projection;
|
|
let isDirtyProjection = newProjection && ((Projection.toString(newProjection) !==
|
|
Projection.toString(this.options.projection))), isDirtyInsets = false;
|
|
merge(true, this.userOptions, options);
|
|
merge(true, this.options, options);
|
|
// If anything changed with the insets, destroy them all and create
|
|
// again below
|
|
if ('insets' in options) {
|
|
this.insets.forEach((inset) => inset.destroy());
|
|
this.insets.length = 0;
|
|
isDirtyInsets = true;
|
|
}
|
|
if (isDirtyProjection || 'fitToGeometry' in options) {
|
|
delete this.fitToGeometryCache;
|
|
}
|
|
if (isDirtyProjection || isDirtyInsets) {
|
|
this.chart.series.forEach((series) => {
|
|
const groups = series.transformGroups;
|
|
if (series.clearBounds) {
|
|
series.clearBounds();
|
|
}
|
|
series.isDirty = true;
|
|
series.isDirtyData = true;
|
|
// Destroy inset transform groups
|
|
if (isDirtyInsets && groups) {
|
|
while (groups.length > 1) {
|
|
const group = groups.pop();
|
|
if (group) {
|
|
group.destroy();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (isDirtyProjection) {
|
|
this.projection = new Projection(this.options.projection);
|
|
}
|
|
// Create new insets
|
|
if (isDirtyInsets) {
|
|
this.createInsets();
|
|
}
|
|
// Fit to natural bounds if center/zoom are not explicitly given
|
|
if (!options.center &&
|
|
// do not fire fitToBounds if user don't want to set zoom
|
|
Object.hasOwnProperty.call(options, 'zoom') &&
|
|
!isNumber(options.zoom)) {
|
|
this.fitToBounds(void 0, void 0, false);
|
|
}
|
|
}
|
|
if (options.center || isNumber(options.zoom)) {
|
|
this.setView(this.options.center, options.zoom, false);
|
|
}
|
|
else if ('fitToGeometry' in options) {
|
|
this.fitToBounds(void 0, void 0, false);
|
|
}
|
|
if (redraw) {
|
|
this.chart.redraw(animation);
|
|
}
|
|
}
|
|
/**
|
|
* Zoom the map view by a given number
|
|
*
|
|
* @function Highcharts.MapView#zoomBy
|
|
*
|
|
* @param {number|undefined} [howMuch]
|
|
* The amount of zoom to apply. 1 zooms in on half the current view,
|
|
* -1 zooms out. Pass `undefined` to zoom to the full bounds of the
|
|
* map.
|
|
* @param {Highcharts.LonLatArray} [coords]
|
|
* Optional map coordinates to keep fixed
|
|
* @param {Array<number>} [chartCoords]
|
|
* Optional chart coordinates to keep fixed, in pixels
|
|
* @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation]
|
|
* The animation to apply to a the redraw
|
|
*/
|
|
zoomBy(howMuch, coords, chartCoords, animation) {
|
|
const chart = this.chart;
|
|
const projectedCenter = this.projection.forward(this.center);
|
|
// let { x, y } = coords || {};
|
|
let [x, y] = coords ? this.projection.forward(coords) : [];
|
|
if (typeof howMuch === 'number') {
|
|
const zoom = this.zoom + howMuch;
|
|
let center;
|
|
// Keep chartX and chartY stationary - convert to lat and lng
|
|
if (chartCoords) {
|
|
const [chartX, chartY] = chartCoords;
|
|
const scale = this.getScale();
|
|
const offsetX = chartX - chart.plotLeft - chart.plotWidth / 2;
|
|
const offsetY = chartY - chart.plotTop - chart.plotHeight / 2;
|
|
x = projectedCenter[0] + offsetX / scale;
|
|
y = projectedCenter[1] + offsetY / scale;
|
|
}
|
|
// Keep lon and lat stationary by adjusting the center
|
|
if (typeof x === 'number' && typeof y === 'number') {
|
|
const scale = 1 - Math.pow(2, this.zoom) / Math.pow(2, zoom);
|
|
// const projectedCenter = this.projection.forward(this.center);
|
|
const offsetX = projectedCenter[0] - x;
|
|
const offsetY = projectedCenter[1] - y;
|
|
projectedCenter[0] -= offsetX * scale;
|
|
projectedCenter[1] += offsetY * scale;
|
|
center = this.projection.inverse(projectedCenter);
|
|
}
|
|
this.setView(center, zoom, void 0, animation);
|
|
// Undefined howMuch => reset zoom
|
|
}
|
|
else {
|
|
this.fitToBounds(void 0, void 0, void 0, animation);
|
|
}
|
|
}
|
|
}
|
|
/* *
|
|
* Return the composite bounding box of a collection of bounding boxes
|
|
*/
|
|
MapView.compositeBounds = (arrayOfBounds) => {
|
|
if (arrayOfBounds.length) {
|
|
return arrayOfBounds
|
|
.slice(1)
|
|
.reduce((acc, cur) => {
|
|
acc.x1 = Math.min(acc.x1, cur.x1);
|
|
acc.y1 = Math.min(acc.y1, cur.y1);
|
|
acc.x2 = Math.max(acc.x2, cur.x2);
|
|
acc.y2 = Math.max(acc.y2, cur.y2);
|
|
return acc;
|
|
}, merge(arrayOfBounds[0]));
|
|
}
|
|
return;
|
|
};
|
|
// Putting this in the same file due to circular dependency with MapView
|
|
class MapViewInset extends MapView {
|
|
constructor(mapView, options) {
|
|
super(mapView.chart, options);
|
|
this.id = options.id;
|
|
this.mapView = mapView;
|
|
this.options = merge(defaultInsetsOptions, options);
|
|
this.allBounds = [];
|
|
if (this.options.geoBounds) {
|
|
// The path in projected units in the map view's main projection.
|
|
// This is used for hit testing where the points should render.
|
|
const path = mapView.projection.path(this.options.geoBounds);
|
|
this.geoBoundsProjectedBox = boundsFromPath(path);
|
|
this.geoBoundsProjectedPolygon = path.map((segment) => [
|
|
segment[1] || 0,
|
|
segment[2] || 0
|
|
]);
|
|
}
|
|
}
|
|
// Get the playing field in pixels
|
|
getField(padded = true) {
|
|
const hitZone = this.hitZone;
|
|
if (hitZone) {
|
|
const padding = padded ? this.padding : [0, 0, 0, 0], polygon = hitZone.coordinates[0], xs = polygon.map((xy) => xy[0]), ys = polygon.map((xy) => xy[1]), x = Math.min.apply(0, xs) + padding[3], x2 = Math.max.apply(0, xs) - padding[1], y = Math.min.apply(0, ys) + padding[0], y2 = Math.max.apply(0, ys) - padding[2];
|
|
if (isNumber(x) && isNumber(y)) {
|
|
return {
|
|
x,
|
|
y,
|
|
width: x2 - x,
|
|
height: y2 - y
|
|
};
|
|
}
|
|
}
|
|
// Fall back to plot area
|
|
return super.getField.call(this, padded);
|
|
}
|
|
// Get the hit zone in pixels
|
|
getHitZone() {
|
|
const { chart, mapView, options } = this, { coordinates } = options.field || {};
|
|
if (coordinates) {
|
|
let polygon = coordinates[0];
|
|
if (options.units === 'percent') {
|
|
const relativeTo = options.relativeTo === 'mapBoundingBox' &&
|
|
mapView.getMapBBox() ||
|
|
merge(chart.plotBox, { x: 0, y: 0 });
|
|
polygon = polygon.map((xy) => [
|
|
relativeLength(`${xy[0]}%`, relativeTo.width, relativeTo.x),
|
|
relativeLength(`${xy[1]}%`, relativeTo.height, relativeTo.y)
|
|
]);
|
|
}
|
|
return {
|
|
type: 'Polygon',
|
|
coordinates: [polygon]
|
|
};
|
|
}
|
|
}
|
|
getProjectedBounds() {
|
|
return MapView.compositeBounds(this.allBounds);
|
|
}
|
|
// Determine whether a point on the main projected plane is inside the
|
|
// geoBounds of the inset.
|
|
isInside(point) {
|
|
const { geoBoundsProjectedBox, geoBoundsProjectedPolygon } = this;
|
|
return Boolean(
|
|
// First we do a pre-pass to check whether the test point is inside
|
|
// the rectangular bounding box of the polygon. This is less
|
|
// expensive and will rule out most cases.
|
|
geoBoundsProjectedBox &&
|
|
point.x >= geoBoundsProjectedBox.x1 &&
|
|
point.x <= geoBoundsProjectedBox.x2 &&
|
|
point.y >= geoBoundsProjectedBox.y1 &&
|
|
point.y <= geoBoundsProjectedBox.y2 &&
|
|
// Next, do the more expensive check whether the point is inside the
|
|
// polygon itself.
|
|
geoBoundsProjectedPolygon &&
|
|
pointInPolygon(point, geoBoundsProjectedPolygon));
|
|
}
|
|
// Render the map view inset with the border path
|
|
render() {
|
|
const { chart, mapView, options } = this, borderPath = options.borderPath || options.field;
|
|
if (borderPath && mapView.group) {
|
|
let animate = true;
|
|
if (!this.border) {
|
|
this.border = chart.renderer
|
|
.path()
|
|
.addClass('highcharts-mapview-inset-border')
|
|
.add(mapView.group);
|
|
animate = false;
|
|
}
|
|
if (!chart.styledMode) {
|
|
this.border.attr({
|
|
stroke: options.borderColor,
|
|
'stroke-width': options.borderWidth
|
|
});
|
|
}
|
|
const crisp = Math.round(this.border.strokeWidth()) % 2 / 2, field = (options.relativeTo === 'mapBoundingBox' &&
|
|
mapView.getMapBBox()) || mapView.playingField;
|
|
const d = (borderPath.coordinates || []).reduce((d, lineString) => lineString.reduce((d, point, i) => {
|
|
let [x, y] = point;
|
|
if (options.units === 'percent') {
|
|
x = chart.plotLeft + relativeLength(`${x}%`, field.width, field.x);
|
|
y = chart.plotTop + relativeLength(`${y}%`, field.height, field.y);
|
|
}
|
|
x = Math.floor(x) + crisp;
|
|
y = Math.floor(y) + crisp;
|
|
d.push(i === 0 ? ['M', x, y] : ['L', x, y]);
|
|
return d;
|
|
}, d), []);
|
|
// Apply the border path
|
|
this.border[animate ? 'animate' : 'attr']({ d });
|
|
}
|
|
}
|
|
destroy() {
|
|
if (this.border) {
|
|
this.border = this.border.destroy();
|
|
}
|
|
this.eventsToUnbind.forEach((f) => f());
|
|
}
|
|
// No chart-level events for insets
|
|
setUpEvents() { }
|
|
}
|
|
// Initialize the MapView after initialization, but before firstRender
|
|
addEvent(MapChart, 'afterInit', function () {
|
|
/**
|
|
* The map view handles zooming and centering on the map, and various
|
|
* client-side projection capabilities.
|
|
*
|
|
* @name Highcharts.MapChart#mapView
|
|
* @type {Highcharts.MapView|undefined}
|
|
*/
|
|
this.mapView = new MapView(this, this.options.mapView);
|
|
});
|
|
|
|
return MapView;
|
|
});
|
|
_registerModule(_modules, 'Series/Map/MapSeries.js', [_modules['Core/Animation/AnimationUtilities.js'], _modules['Series/ColorMapComposition.js'], _modules['Series/CenteredUtilities.js'], _modules['Core/Globals.js'], _modules['Core/Chart/MapChart.js'], _modules['Series/Map/MapPoint.js'], _modules['Maps/MapView.js'], _modules['Core/Series/Series.js'], _modules['Core/Series/SeriesRegistry.js'], _modules['Core/Renderer/SVG/SVGRenderer.js'], _modules['Core/Utilities.js']], function (A, ColorMapComposition, CU, H, MapChart, MapPoint, MapView, Series, SeriesRegistry, SVGRenderer, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2021 Torstein Honsi
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { animObject, stop } = A;
|
|
const { noop } = H;
|
|
const { splitPath } = MapChart;
|
|
const {
|
|
// indirect dependency to keep product size low
|
|
seriesTypes: { column: ColumnSeries, scatter: ScatterSeries } } = SeriesRegistry;
|
|
const { extend, find, fireEvent, getNestedProperty, isArray, defined, isNumber, isObject, merge, objectEach, pick, splat } = U;
|
|
/* *
|
|
*
|
|
* Class
|
|
*
|
|
* */
|
|
/**
|
|
* @private
|
|
* @class
|
|
* @name Highcharts.seriesTypes.map
|
|
*
|
|
* @augments Highcharts.Series
|
|
*/
|
|
class MapSeries extends ScatterSeries {
|
|
constructor() {
|
|
/* *
|
|
*
|
|
* Static Properties
|
|
*
|
|
* */
|
|
super(...arguments);
|
|
this.chart = void 0;
|
|
this.data = void 0;
|
|
this.group = void 0;
|
|
this.joinBy = void 0;
|
|
this.options = void 0;
|
|
this.points = void 0;
|
|
this.processedData = [];
|
|
/* eslint-enable valid-jsdoc */
|
|
}
|
|
/* *
|
|
*
|
|
* Functions
|
|
*
|
|
* */
|
|
/* eslint-disable valid-jsdoc */
|
|
/**
|
|
* The initial animation for the map series. By default, animation is
|
|
* disabled.
|
|
* @private
|
|
*/
|
|
animate(init) {
|
|
const { chart, group } = this, animation = animObject(this.options.animation);
|
|
// Initialize the animation
|
|
if (init) {
|
|
// Scale down the group and place it in the center
|
|
group.attr({
|
|
translateX: chart.plotLeft + chart.plotWidth / 2,
|
|
translateY: chart.plotTop + chart.plotHeight / 2,
|
|
scaleX: 0.001,
|
|
scaleY: 0.001
|
|
});
|
|
// Run the animation
|
|
}
|
|
else {
|
|
group.animate({
|
|
translateX: chart.plotLeft,
|
|
translateY: chart.plotTop,
|
|
scaleX: 1,
|
|
scaleY: 1
|
|
}, animation);
|
|
}
|
|
}
|
|
clearBounds() {
|
|
this.points.forEach((point) => {
|
|
delete point.bounds;
|
|
delete point.insetIndex;
|
|
delete point.projectedPath;
|
|
});
|
|
delete this.bounds;
|
|
}
|
|
/**
|
|
* Allow a quick redraw by just translating the area group. Used for zooming
|
|
* and panning in capable browsers.
|
|
* @private
|
|
*/
|
|
doFullTranslate() {
|
|
return Boolean(this.isDirtyData ||
|
|
this.chart.isResizing ||
|
|
!this.hasRendered);
|
|
}
|
|
/**
|
|
* Draw the data labels. Special for maps is the time that the data labels
|
|
* are drawn (after points), and the clipping of the dataLabelsGroup.
|
|
* @private
|
|
*/
|
|
drawMapDataLabels() {
|
|
Series.prototype.drawDataLabels.call(this);
|
|
if (this.dataLabelsGroup) {
|
|
this.dataLabelsGroup.clip(this.chart.clipRect);
|
|
}
|
|
}
|
|
/**
|
|
* Use the drawPoints method of column, that is able to handle simple
|
|
* shapeArgs. Extend it by assigning the tooltip position.
|
|
* @private
|
|
*/
|
|
drawPoints() {
|
|
const series = this, { chart, group, transformGroups = [] } = this, { mapView, renderer } = chart;
|
|
if (!mapView) {
|
|
return;
|
|
}
|
|
// Set groups that handle transform during zooming and panning in order
|
|
// to preserve clipping on series.group
|
|
this.transformGroups = transformGroups;
|
|
if (!transformGroups[0]) {
|
|
transformGroups[0] = renderer.g().add(group);
|
|
}
|
|
mapView.insets.forEach((inset, i) => {
|
|
if (!transformGroups[i + 1]) {
|
|
transformGroups.push(renderer.g().add(group));
|
|
}
|
|
});
|
|
// Draw the shapes again
|
|
if (this.doFullTranslate()) {
|
|
// Individual point actions.
|
|
this.points.forEach((point) => {
|
|
const { graphic, shapeArgs } = point;
|
|
// Points should be added in the corresponding transform group
|
|
point.group = transformGroups[typeof point.insetIndex === 'number' ?
|
|
point.insetIndex + 1 :
|
|
0];
|
|
// When the point has been moved between insets after
|
|
// MapView.update
|
|
if (graphic && graphic.parentGroup !== point.group) {
|
|
graphic.add(point.group);
|
|
}
|
|
// Restore state color on update/redraw (#3529)
|
|
if (shapeArgs && chart.hasRendered && !chart.styledMode) {
|
|
shapeArgs.fill = this.pointAttribs(point, point.state).fill;
|
|
}
|
|
});
|
|
// Draw the points
|
|
ColumnSeries.prototype.drawPoints.apply(this);
|
|
// Add class names
|
|
this.points.forEach((point) => {
|
|
const graphic = point.graphic;
|
|
if (graphic) {
|
|
const animate = graphic.animate;
|
|
let className = '';
|
|
if (point.name) {
|
|
className +=
|
|
'highcharts-name-' +
|
|
point.name.replace(/ /g, '-').toLowerCase();
|
|
}
|
|
if (point.properties && point.properties['hc-key']) {
|
|
className +=
|
|
' highcharts-key-' +
|
|
point.properties['hc-key'].toString().toLowerCase();
|
|
}
|
|
if (className) {
|
|
graphic.addClass(className);
|
|
}
|
|
// In styled mode, apply point colors by CSS
|
|
if (chart.styledMode) {
|
|
graphic.css(this.pointAttribs(point, point.selected && 'select' || void 0));
|
|
}
|
|
graphic.animate = function (params, options, complete) {
|
|
const animateIn = (isNumber(params['stroke-width']) &&
|
|
!isNumber(graphic['stroke-width'])), animateOut = (isNumber(graphic['stroke-width']) &&
|
|
!isNumber(params['stroke-width']));
|
|
// When strokeWidth is animating
|
|
if (animateIn || animateOut) {
|
|
const strokeWidth = pick(series.getStrokeWidth(series.options), 1 // Styled mode
|
|
), inheritedStrokeWidth = (strokeWidth /
|
|
(chart.mapView &&
|
|
chart.mapView.getScale() ||
|
|
1));
|
|
// For animating from undefined, .attr() reads the
|
|
// property as the starting point
|
|
if (animateIn) {
|
|
graphic['stroke-width'] = inheritedStrokeWidth;
|
|
}
|
|
// For animating to undefined
|
|
if (animateOut) {
|
|
params['stroke-width'] = inheritedStrokeWidth;
|
|
}
|
|
}
|
|
const ret = animate.call(graphic, params, options, animateOut ? function () {
|
|
// Remove the attribute after finished animation
|
|
graphic.element.removeAttribute('stroke-width');
|
|
delete graphic['stroke-width'];
|
|
// Proceed
|
|
if (complete) {
|
|
complete.apply(this, arguments);
|
|
}
|
|
} : complete);
|
|
return ret;
|
|
};
|
|
}
|
|
});
|
|
}
|
|
// Apply the SVG transform
|
|
transformGroups.forEach((transformGroup, i) => {
|
|
const view = i === 0 ? mapView : mapView.insets[i - 1], svgTransform = view.getSVGTransform(), strokeWidth = pick(this.getStrokeWidth(this.options), 1 // Styled mode
|
|
);
|
|
/*
|
|
Animate or move to the new zoom level. In order to prevent
|
|
flickering as the different transform components are set out of sync
|
|
(#5991), we run a fake animator attribute and set scale and
|
|
translation synchronously in the same step.
|
|
|
|
A possible improvement to the API would be to handle this in the
|
|
renderer or animation engine itself, to ensure that when we are
|
|
animating multiple properties, we make sure that each step for each
|
|
property is performed in the same step. Also, for symbols and for
|
|
transform properties, it should induce a single updateTransform and
|
|
symbolAttr call.
|
|
*/
|
|
const scale = svgTransform.scaleX, flipFactor = svgTransform.scaleY > 0 ? 1 : -1;
|
|
const animatePoints = (scale) => {
|
|
(series.points || []).forEach((point) => {
|
|
const graphic = point.graphic;
|
|
let strokeWidth;
|
|
if (graphic &&
|
|
graphic['stroke-width'] &&
|
|
(strokeWidth = this.getStrokeWidth(point.options))) {
|
|
graphic.attr({
|
|
'stroke-width': strokeWidth / scale
|
|
});
|
|
}
|
|
});
|
|
};
|
|
if (renderer.globalAnimation &&
|
|
chart.hasRendered &&
|
|
mapView.allowTransformAnimation) {
|
|
const startTranslateX = Number(transformGroup.attr('translateX'));
|
|
const startTranslateY = Number(transformGroup.attr('translateY'));
|
|
const startScale = Number(transformGroup.attr('scaleX'));
|
|
const step = (now, fx) => {
|
|
const scaleStep = startScale +
|
|
(scale - startScale) * fx.pos;
|
|
transformGroup.attr({
|
|
translateX: (startTranslateX + (svgTransform.translateX - startTranslateX) * fx.pos),
|
|
translateY: (startTranslateY + (svgTransform.translateY - startTranslateY) * fx.pos),
|
|
scaleX: scaleStep,
|
|
scaleY: scaleStep * flipFactor,
|
|
'stroke-width': strokeWidth / scaleStep
|
|
});
|
|
animatePoints(scaleStep); // #18166
|
|
};
|
|
const animOptions = merge(animObject(renderer.globalAnimation)), userStep = animOptions.step;
|
|
animOptions.step =
|
|
function (obj) {
|
|
if (userStep) {
|
|
userStep.apply(this, arguments);
|
|
}
|
|
step.apply(this, arguments);
|
|
};
|
|
transformGroup
|
|
.attr({ animator: 0 })
|
|
.animate({ animator: 1 }, animOptions, function () {
|
|
if (typeof renderer.globalAnimation !== 'boolean' &&
|
|
renderer.globalAnimation.complete) {
|
|
// fire complete only from this place
|
|
renderer.globalAnimation.complete({
|
|
applyDrilldown: true
|
|
});
|
|
}
|
|
});
|
|
// When dragging or first rendering, animation is off
|
|
}
|
|
else {
|
|
stop(transformGroup);
|
|
transformGroup.attr(merge(svgTransform, { 'stroke-width': strokeWidth / scale }));
|
|
animatePoints(scale); // #18166
|
|
}
|
|
});
|
|
if (!this.isDrilling) {
|
|
this.drawMapDataLabels();
|
|
}
|
|
}
|
|
/**
|
|
* Get the bounding box of all paths in the map combined.
|
|
*
|
|
*/
|
|
getProjectedBounds() {
|
|
if (!this.bounds && this.chart.mapView) {
|
|
const { insets, projection } = this.chart.mapView, allBounds = [];
|
|
// Find the bounding box of each point
|
|
(this.points || []).forEach(function (point) {
|
|
if (point.path || point.geometry) {
|
|
// @todo Try to puth these two conversions in
|
|
// MapPoint.applyOptions
|
|
if (typeof point.path === 'string') {
|
|
point.path = splitPath(point.path);
|
|
// Legacy one-dimensional array
|
|
}
|
|
else if (isArray(point.path) &&
|
|
point.path[0] === 'M') {
|
|
point.path = SVGRenderer.prototype.pathToSegments(point.path);
|
|
}
|
|
// The first time a map point is used, analyze its box
|
|
if (!point.bounds) {
|
|
let bounds = point.getProjectedBounds(projection);
|
|
if (bounds) {
|
|
point.labelrank = pick(point.labelrank,
|
|
// Bigger shape, higher rank
|
|
((bounds.x2 - bounds.x1) *
|
|
(bounds.y2 - bounds.y1)));
|
|
const { midX, midY } = bounds;
|
|
if (insets && isNumber(midX) && isNumber(midY)) {
|
|
const inset = find(insets, (inset) => inset.isInside({
|
|
x: midX, y: midY
|
|
}));
|
|
if (inset) {
|
|
// Project again, but with the inset
|
|
// projection
|
|
delete point.projectedPath;
|
|
bounds = point.getProjectedBounds(inset.projection);
|
|
if (bounds) {
|
|
inset.allBounds.push(bounds);
|
|
}
|
|
point.insetIndex = insets.indexOf(inset);
|
|
}
|
|
}
|
|
point.bounds = bounds;
|
|
}
|
|
}
|
|
if (point.bounds && point.insetIndex === void 0) {
|
|
allBounds.push(point.bounds);
|
|
}
|
|
}
|
|
});
|
|
this.bounds = MapView.compositeBounds(allBounds);
|
|
}
|
|
return this.bounds;
|
|
}
|
|
/**
|
|
* Return the stroke-width either from a series options or point options
|
|
* object. This function is used by both the map series where the
|
|
* `borderWidth` sets the stroke-width, and the mapline series where the
|
|
* `lineWidth` sets the stroke-width.
|
|
* @private
|
|
*/
|
|
getStrokeWidth(options) {
|
|
const pointAttrToOptions = this.pointAttrToOptions;
|
|
return options[pointAttrToOptions &&
|
|
pointAttrToOptions['stroke-width'] || 'borderWidth'];
|
|
}
|
|
/**
|
|
* Define hasData function for non-cartesian series. Returns true if the
|
|
* series has points at all.
|
|
* @private
|
|
*/
|
|
hasData() {
|
|
return !!this.processedXData.length; // != 0
|
|
}
|
|
/**
|
|
* Get presentational attributes. In the maps series this runs in both
|
|
* styled and non-styled mode, because colors hold data when a colorAxis is
|
|
* used.
|
|
* @private
|
|
*/
|
|
pointAttribs(point, state) {
|
|
var _a;
|
|
const { mapView, styledMode } = point.series.chart;
|
|
const attr = styledMode ?
|
|
this.colorAttribs(point) :
|
|
ColumnSeries.prototype.pointAttribs.call(this, point, state);
|
|
// Individual stroke width
|
|
let pointStrokeWidth = this.getStrokeWidth(point.options);
|
|
// Handle state specific border or line width
|
|
if (state) {
|
|
const stateOptions = merge(this.options.states[state], point.options.states &&
|
|
point.options.states[state] ||
|
|
{}), stateStrokeWidth = this.getStrokeWidth(stateOptions);
|
|
if (defined(stateStrokeWidth)) {
|
|
pointStrokeWidth = stateStrokeWidth;
|
|
}
|
|
attr.stroke = (_a = stateOptions.borderColor) !== null && _a !== void 0 ? _a : point.color;
|
|
}
|
|
if (pointStrokeWidth && mapView) {
|
|
pointStrokeWidth /= mapView.getScale();
|
|
}
|
|
// In order for dash style to avoid being scaled, set the transformed
|
|
// stroke width on the item
|
|
const seriesStrokeWidth = this.getStrokeWidth(this.options);
|
|
if (attr.dashstyle &&
|
|
mapView &&
|
|
isNumber(seriesStrokeWidth)) {
|
|
pointStrokeWidth = seriesStrokeWidth / mapView.getScale();
|
|
}
|
|
// Invisible map points means that the data value is removed from the
|
|
// map, but not the map area shape itself. Instead it is rendered like a
|
|
// null point. To fully remove a map area, it should be removed from the
|
|
// mapData.
|
|
if (!point.visible) {
|
|
attr.fill = this.options.nullColor;
|
|
}
|
|
if (defined(pointStrokeWidth)) {
|
|
attr['stroke-width'] = pointStrokeWidth;
|
|
}
|
|
else {
|
|
delete attr['stroke-width'];
|
|
}
|
|
attr['stroke-linecap'] = attr['stroke-linejoin'] = this.options.linecap;
|
|
return attr;
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
updateData() {
|
|
// #16782
|
|
if (this.processedData) {
|
|
return false;
|
|
}
|
|
return super.updateData.apply(this, arguments);
|
|
}
|
|
/**
|
|
* Extend setData to call processData and generatePoints immediately.
|
|
* @private
|
|
*/
|
|
setData(data, redraw = true, animation, updatePoints) {
|
|
delete this.bounds;
|
|
super.setData.call(this, data, false, void 0, updatePoints);
|
|
this.processData();
|
|
this.generatePoints();
|
|
if (redraw) {
|
|
this.chart.redraw(animation);
|
|
}
|
|
}
|
|
/**
|
|
* Extend processData to join in mapData. If the allAreas option is true,
|
|
* all areas from the mapData are used, and those that don't correspond to a
|
|
* data value are given null values. The results are stored in
|
|
* `processedData` in order to avoid mutating `data`.
|
|
* @private
|
|
*/
|
|
processData() {
|
|
const options = this.options, data = options.data, chartOptions = this.chart.options.chart, joinBy = this.joinBy, pointArrayMap = options.keys || this.pointArrayMap, dataUsed = [], mapMap = {};
|
|
let mapView = this.chart.mapView, mapDataObject = mapView && (
|
|
// Get map either from series or global
|
|
isObject(options.mapData, true) ?
|
|
mapView.getGeoMap(options.mapData) : mapView.geoMap), mapTransforms = this.chart.mapTransforms, mapPoint, props, i;
|
|
// Pick up transform definitions for chart
|
|
this.chart.mapTransforms = mapTransforms =
|
|
chartOptions.mapTransforms ||
|
|
mapDataObject && mapDataObject['hc-transform'] ||
|
|
mapTransforms;
|
|
// Cache cos/sin of transform rotation angle
|
|
if (mapTransforms) {
|
|
objectEach(mapTransforms, function (transform) {
|
|
if (transform.rotation) {
|
|
transform.cosAngle = Math.cos(transform.rotation);
|
|
transform.sinAngle = Math.sin(transform.rotation);
|
|
}
|
|
});
|
|
}
|
|
let mapData;
|
|
if (isArray(options.mapData)) {
|
|
mapData = options.mapData;
|
|
}
|
|
else if (mapDataObject && mapDataObject.type === 'FeatureCollection') {
|
|
this.mapTitle = mapDataObject.title;
|
|
mapData = H.geojson(mapDataObject, this.type, this);
|
|
}
|
|
// Reset processedData
|
|
this.processedData = [];
|
|
const processedData = this.processedData;
|
|
// Pick up numeric values, add index. Convert Array point definitions to
|
|
// objects using pointArrayMap.
|
|
if (data) {
|
|
data.forEach(function (val, i) {
|
|
let ix = 0;
|
|
if (isNumber(val)) {
|
|
processedData[i] = {
|
|
value: val
|
|
};
|
|
}
|
|
else if (isArray(val)) {
|
|
processedData[i] = {};
|
|
// Automatically copy first item to hc-key if there is
|
|
// an extra leading string
|
|
if (!options.keys &&
|
|
val.length > pointArrayMap.length &&
|
|
typeof val[0] === 'string') {
|
|
processedData[i]['hc-key'] = val[0];
|
|
++ix;
|
|
}
|
|
// Run through pointArrayMap and what's left of the
|
|
// point data array in parallel, copying over the values
|
|
for (let j = 0; j < pointArrayMap.length; ++j, ++ix) {
|
|
if (pointArrayMap[j] &&
|
|
typeof val[ix] !== 'undefined') {
|
|
if (pointArrayMap[j].indexOf('.') > 0) {
|
|
MapPoint.prototype.setNestedProperty(processedData[i], val[ix], pointArrayMap[j]);
|
|
}
|
|
else {
|
|
processedData[i][pointArrayMap[j]] =
|
|
val[ix];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
processedData[i] = data[i];
|
|
}
|
|
if (joinBy && joinBy[0] === '_i') {
|
|
processedData[i]._i = i;
|
|
}
|
|
});
|
|
}
|
|
if (mapData) {
|
|
this.mapData = mapData;
|
|
this.mapMap = {};
|
|
for (i = 0; i < mapData.length; i++) {
|
|
mapPoint = mapData[i];
|
|
props = mapPoint.properties;
|
|
mapPoint._i = i;
|
|
// Copy the property over to root for faster access
|
|
if (joinBy[0] && props && props[joinBy[0]]) {
|
|
mapPoint[joinBy[0]] = props[joinBy[0]];
|
|
}
|
|
mapMap[mapPoint[joinBy[0]]] = mapPoint;
|
|
}
|
|
this.mapMap = mapMap;
|
|
// Registered the point codes that actually hold data
|
|
if (joinBy[1]) {
|
|
const joinKey = joinBy[1];
|
|
processedData.forEach(function (pointOptions) {
|
|
const mapKey = getNestedProperty(joinKey, pointOptions);
|
|
if (mapMap[mapKey]) {
|
|
dataUsed.push(mapMap[mapKey]);
|
|
}
|
|
});
|
|
}
|
|
if (options.allAreas) {
|
|
// Register the point codes that actually hold data
|
|
if (joinBy[1]) {
|
|
const joinKey = joinBy[1];
|
|
processedData.forEach(function (pointOptions) {
|
|
dataUsed.push(getNestedProperty(joinKey, pointOptions));
|
|
});
|
|
}
|
|
// Add those map points that don't correspond to data, which
|
|
// will be drawn as null points. Searching a string is faster
|
|
// than Array.indexOf
|
|
const dataUsedString = ('|' +
|
|
dataUsed
|
|
.map(function (point) {
|
|
return point && point[joinBy[0]];
|
|
})
|
|
.join('|') +
|
|
'|');
|
|
mapData.forEach(function (mapPoint) {
|
|
if (!joinBy[0] ||
|
|
dataUsedString.indexOf('|' + mapPoint[joinBy[0]] + '|') === -1) {
|
|
processedData.push(merge(mapPoint, { value: null }));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// The processedXData array is used by general chart logic for checking
|
|
// data length in various scanarios
|
|
this.processedXData = new Array(processedData.length);
|
|
return void 0;
|
|
}
|
|
/**
|
|
* Extend setOptions by picking up the joinBy option and applying it to a
|
|
* series property.
|
|
* @private
|
|
*/
|
|
setOptions(itemOptions) {
|
|
let options = Series.prototype.setOptions.call(this, itemOptions), joinBy = options.joinBy, joinByNull = joinBy === null;
|
|
if (joinByNull) {
|
|
joinBy = '_i';
|
|
}
|
|
joinBy = this.joinBy = splat(joinBy);
|
|
if (!joinBy[1]) {
|
|
joinBy[1] = joinBy[0];
|
|
}
|
|
return options;
|
|
}
|
|
/**
|
|
* Add the path option for data points. Find the max value for color
|
|
* calculation.
|
|
* @private
|
|
*/
|
|
translate() {
|
|
const series = this, doFullTranslate = series.doFullTranslate(), mapView = this.chart.mapView, projection = mapView && mapView.projection;
|
|
// Recalculate box on updated data
|
|
if (this.chart.hasRendered && (this.isDirtyData || !this.hasRendered)) {
|
|
this.processData();
|
|
this.generatePoints();
|
|
delete this.bounds;
|
|
if (mapView &&
|
|
!mapView.userOptions.center &&
|
|
!isNumber(mapView.userOptions.zoom) &&
|
|
mapView.zoom === mapView.minZoom // #18542 don't zoom out if
|
|
// map is zoomed
|
|
) {
|
|
// Not only recalculate bounds but also fit view
|
|
mapView.fitToBounds(void 0, void 0, false); // #17012
|
|
}
|
|
else {
|
|
// If center and zoom is defined in user options, get bounds but
|
|
// don't change view
|
|
this.getProjectedBounds();
|
|
}
|
|
}
|
|
if (mapView) {
|
|
const mainSvgTransform = mapView.getSVGTransform();
|
|
series.points.forEach(function (point) {
|
|
const svgTransform = (isNumber(point.insetIndex) &&
|
|
mapView.insets[point.insetIndex].getSVGTransform()) || mainSvgTransform;
|
|
// Record the middle point (loosely based on centroid),
|
|
// determined by the middleX and middleY options.
|
|
if (svgTransform &&
|
|
point.bounds &&
|
|
isNumber(point.bounds.midX) &&
|
|
isNumber(point.bounds.midY)) {
|
|
point.plotX = point.bounds.midX * svgTransform.scaleX +
|
|
svgTransform.translateX;
|
|
point.plotY = point.bounds.midY * svgTransform.scaleY +
|
|
svgTransform.translateY;
|
|
}
|
|
if (doFullTranslate) {
|
|
point.shapeType = 'path';
|
|
point.shapeArgs = {
|
|
d: MapPoint.getProjectedPath(point, projection)
|
|
};
|
|
}
|
|
if (point.projectedPath && !point.projectedPath.length) {
|
|
point.setVisible(false);
|
|
}
|
|
else {
|
|
point.setVisible(true);
|
|
}
|
|
});
|
|
}
|
|
fireEvent(series, 'afterTranslate');
|
|
}
|
|
}
|
|
/**
|
|
* The map series is used for basic choropleth maps, where each map area has
|
|
* a color based on its value.
|
|
*
|
|
* @sample maps/demo/all-maps/
|
|
* Choropleth map
|
|
*
|
|
* @extends plotOptions.scatter
|
|
* @excluding boostBlending, boostThreshold, dragDrop, cluster, marker
|
|
* @product highmaps
|
|
* @optionparent plotOptions.map
|
|
*
|
|
* @private
|
|
*/
|
|
MapSeries.defaultOptions = merge(ScatterSeries.defaultOptions, {
|
|
/**
|
|
* Whether the MapView takes this series into account when computing the
|
|
* default zoom and center of the map.
|
|
*
|
|
* @sample maps/series/affectsmapview/
|
|
* US map with world map backdrop
|
|
*
|
|
* @since 10.0.0
|
|
*
|
|
* @private
|
|
*/
|
|
affectsMapView: true,
|
|
animation: false,
|
|
dataLabels: {
|
|
crop: false,
|
|
formatter: function () {
|
|
const { numberFormatter } = this.series.chart;
|
|
const { value } = this.point;
|
|
return isNumber(value) ? numberFormatter(value, -1) : '';
|
|
},
|
|
inside: true,
|
|
overflow: false,
|
|
padding: 0,
|
|
verticalAlign: 'middle'
|
|
},
|
|
/**
|
|
* The SVG value used for the `stroke-linecap` and `stroke-linejoin` of
|
|
* the map borders. Round means that borders are rounded in the ends and
|
|
* bends.
|
|
*
|
|
* @sample maps/demo/mappoint-mapmarker/
|
|
* Backdrop coastline with round linecap
|
|
*
|
|
* @type {Highcharts.SeriesLinecapValue}
|
|
* @since 10.3.3
|
|
*/
|
|
linecap: 'round',
|
|
/**
|
|
* @ignore-option
|
|
*
|
|
* @private
|
|
*/
|
|
marker: null,
|
|
/**
|
|
* The color to apply to null points.
|
|
*
|
|
* In styled mode, the null point fill is set in the
|
|
* `.highcharts-null-point` class.
|
|
*
|
|
* @sample maps/demo/all-areas-as-null/
|
|
* Null color
|
|
*
|
|
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
|
|
*
|
|
* @private
|
|
*/
|
|
nullColor: "#f7f7f7" /* Palette.neutralColor3 */,
|
|
/**
|
|
* Whether to allow pointer interaction like tooltips and mouse events
|
|
* on null points.
|
|
*
|
|
* @type {boolean}
|
|
* @since 4.2.7
|
|
* @apioption plotOptions.map.nullInteraction
|
|
*
|
|
* @private
|
|
*/
|
|
stickyTracking: false,
|
|
tooltip: {
|
|
followPointer: true,
|
|
pointFormat: '{point.name}: {point.value}<br/>'
|
|
},
|
|
/**
|
|
* @ignore-option
|
|
*
|
|
* @private
|
|
*/
|
|
turboThreshold: 0,
|
|
/**
|
|
* Whether all areas of the map defined in `mapData` should be rendered.
|
|
* If `true`, areas which don't correspond to a data point, are rendered
|
|
* as `null` points. If `false`, those areas are skipped.
|
|
*
|
|
* @sample maps/plotoptions/series-allareas-false/
|
|
* All areas set to false
|
|
*
|
|
* @type {boolean}
|
|
* @default true
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.allAreas
|
|
*
|
|
* @private
|
|
*/
|
|
allAreas: true,
|
|
/**
|
|
* The border color of the map areas.
|
|
*
|
|
* In styled mode, the border stroke is given in the `.highcharts-point`
|
|
* class.
|
|
*
|
|
* @sample {highmaps} maps/plotoptions/series-border/
|
|
* Borders demo
|
|
*
|
|
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
|
|
* @default #cccccc
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.borderColor
|
|
*
|
|
* @private
|
|
*/
|
|
borderColor: "#e6e6e6" /* Palette.neutralColor10 */,
|
|
/**
|
|
* The border width of each map area.
|
|
*
|
|
* In styled mode, the border stroke width is given in the
|
|
* `.highcharts-point` class.
|
|
*
|
|
* @sample maps/plotoptions/series-border/
|
|
* Borders demo
|
|
*
|
|
* @type {number}
|
|
* @default 1
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.borderWidth
|
|
*
|
|
* @private
|
|
*/
|
|
borderWidth: 1,
|
|
/**
|
|
* @type {string}
|
|
* @default value
|
|
* @apioption plotOptions.map.colorKey
|
|
*/
|
|
/**
|
|
* What property to join the `mapData` to the value data. For example,
|
|
* if joinBy is "code", the mapData items with a specific code is merged
|
|
* into the data with the same code. For maps loaded from GeoJSON, the
|
|
* keys may be held in each point's `properties` object.
|
|
*
|
|
* The joinBy option can also be an array of two values, where the first
|
|
* points to a key in the `mapData`, and the second points to another
|
|
* key in the `data`.
|
|
*
|
|
* When joinBy is `null`, the map items are joined by their position in
|
|
* the array, which performs much better in maps with many data points.
|
|
* This is the recommended option if you are printing more than a
|
|
* thousand data points and have a backend that can preprocess the data
|
|
* into a parallel array of the mapData.
|
|
*
|
|
* @sample maps/plotoptions/series-border/
|
|
* Joined by "code"
|
|
* @sample maps/demo/geojson/
|
|
* GeoJSON joined by an array
|
|
* @sample maps/series/joinby-null/
|
|
* Simple data joined by null
|
|
*
|
|
* @type {string|Array<string>}
|
|
* @default hc-key
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.joinBy
|
|
*
|
|
* @private
|
|
*/
|
|
joinBy: 'hc-key',
|
|
/**
|
|
* Define the z index of the series.
|
|
*
|
|
* @type {number}
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.zIndex
|
|
*/
|
|
/**
|
|
* @apioption plotOptions.series.states
|
|
*
|
|
* @private
|
|
*/
|
|
states: {
|
|
/**
|
|
* @apioption plotOptions.series.states.hover
|
|
*/
|
|
hover: {
|
|
/** @ignore-option */
|
|
halo: void 0,
|
|
/**
|
|
* The color of the shape in this state.
|
|
*
|
|
* @sample maps/plotoptions/series-states-hover/
|
|
* Hover options
|
|
*
|
|
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.states.hover.color
|
|
*/
|
|
/**
|
|
* The border color of the point in this state.
|
|
*
|
|
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.states.hover.borderColor
|
|
*/
|
|
borderColor: "#666666" /* Palette.neutralColor60 */,
|
|
/**
|
|
* The border width of the point in this state
|
|
*
|
|
* @type {number}
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.states.hover.borderWidth
|
|
*/
|
|
borderWidth: 2
|
|
/**
|
|
* The relative brightness of the point when hovered, relative
|
|
* to the normal point color.
|
|
*
|
|
* @type {number}
|
|
* @product highmaps
|
|
* @default 0
|
|
* @apioption plotOptions.series.states.hover.brightness
|
|
*/
|
|
},
|
|
/**
|
|
* @apioption plotOptions.series.states.normal
|
|
*/
|
|
normal: {
|
|
/**
|
|
* @productdesc {highmaps}
|
|
* The animation adds some latency in order to reduce the effect
|
|
* of flickering when hovering in and out of for example an
|
|
* uneven coastline.
|
|
*
|
|
* @sample {highmaps} maps/plotoptions/series-states-animation-false/
|
|
* No animation of fill color
|
|
*
|
|
* @apioption plotOptions.series.states.normal.animation
|
|
*/
|
|
animation: true
|
|
},
|
|
/**
|
|
* @apioption plotOptions.series.states.select
|
|
*/
|
|
select: {
|
|
/**
|
|
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
|
|
* @default #cccccc
|
|
* @product highmaps
|
|
* @apioption plotOptions.series.states.select.color
|
|
*/
|
|
color: "#cccccc" /* Palette.neutralColor20 */
|
|
}
|
|
},
|
|
legendSymbol: 'rectangle'
|
|
});
|
|
extend(MapSeries.prototype, {
|
|
type: 'map',
|
|
axisTypes: ColorMapComposition.seriesMembers.axisTypes,
|
|
colorAttribs: ColorMapComposition.seriesMembers.colorAttribs,
|
|
colorKey: ColorMapComposition.seriesMembers.colorKey,
|
|
// When tooltip is not shared, this series (and derivatives) requires
|
|
// direct touch/hover. KD-tree does not apply.
|
|
directTouch: true,
|
|
// We need the points' bounding boxes in order to draw the data labels,
|
|
// so we skip it now and call it from drawPoints instead.
|
|
drawDataLabels: noop,
|
|
// No graph for the map series
|
|
drawGraph: noop,
|
|
forceDL: true,
|
|
getCenter: CU.getCenter,
|
|
getExtremesFromAll: true,
|
|
getSymbol: noop,
|
|
isCartesian: false,
|
|
parallelArrays: ColorMapComposition.seriesMembers.parallelArrays,
|
|
pointArrayMap: ColorMapComposition.seriesMembers.pointArrayMap,
|
|
pointClass: MapPoint,
|
|
// X axis and Y axis must have same translation slope
|
|
preserveAspectRatio: true,
|
|
searchPoint: noop,
|
|
trackerGroups: ColorMapComposition.seriesMembers.trackerGroups,
|
|
// Get axis extremes from paths, not values
|
|
useMapGeometry: true
|
|
});
|
|
ColorMapComposition.compose(MapSeries);
|
|
SeriesRegistry.registerSeriesType('map', MapSeries);
|
|
/* *
|
|
*
|
|
* Default Export
|
|
*
|
|
* */
|
|
/* *
|
|
*
|
|
* API Options
|
|
*
|
|
* */
|
|
/**
|
|
* An array of objects containing a `geometry` or `path` definition and
|
|
* optionally additional properties to join in the `data` as per the `joinBy`
|
|
* option. GeoJSON and TopoJSON structures can also be passed directly into
|
|
* `mapData`.
|
|
*
|
|
* @sample maps/demo/category-map/
|
|
* Map data and joinBy
|
|
* @sample maps/series/mapdata-multiple/
|
|
* Multiple map sources
|
|
*
|
|
* @type {Array<Highcharts.SeriesMapDataOptions>|Highcharts.GeoJSON|Highcharts.TopoJSON}
|
|
* @product highmaps
|
|
* @apioption series.mapData
|
|
*/
|
|
/**
|
|
* A `map` series. If the [type](#series.map.type) option is not specified, it
|
|
* is inherited from [chart.type](#chart.type).
|
|
*
|
|
* @extends series,plotOptions.map
|
|
* @excluding dataParser, dataURL, dragDrop, marker
|
|
* @product highmaps
|
|
* @apioption series.map
|
|
*/
|
|
/**
|
|
* An array of data points for the series. For the `map` series type, points can
|
|
* be given in the following ways:
|
|
*
|
|
* 1. An array of numerical values. In this case, the numerical values will be
|
|
* interpreted as `value` options. Example:
|
|
* ```js
|
|
* data: [0, 5, 3, 5]
|
|
* ```
|
|
*
|
|
* 2. An array of arrays with 2 values. In this case, the values correspond to
|
|
* `[hc-key, value]`. Example:
|
|
* ```js
|
|
* data: [
|
|
* ['us-ny', 0],
|
|
* ['us-mi', 5],
|
|
* ['us-tx', 3],
|
|
* ['us-ak', 5]
|
|
* ]
|
|
* ```
|
|
*
|
|
* 3. An array of objects with named values. The following snippet shows only a
|
|
* few settings, see the complete options set below. If the total number of
|
|
* data points exceeds the series'
|
|
* [turboThreshold](#series.map.turboThreshold),
|
|
* this option is not available.
|
|
* ```js
|
|
* data: [{
|
|
* value: 6,
|
|
* name: "Point2",
|
|
* color: "#00FF00"
|
|
* }, {
|
|
* value: 6,
|
|
* name: "Point1",
|
|
* color: "#FF00FF"
|
|
* }]
|
|
* ```
|
|
*
|
|
* @type {Array<number|Array<string,(number|null)>|null|*>}
|
|
* @product highmaps
|
|
* @apioption series.map.data
|
|
*/
|
|
/**
|
|
* When using automatic point colors pulled from the global
|
|
* [colors](colors) or series-specific
|
|
* [plotOptions.map.colors](series.colors) collections, this option
|
|
* determines whether the chart should receive one color per series or
|
|
* one color per point.
|
|
*
|
|
* In styled mode, the `colors` or `series.colors` arrays are not
|
|
* supported, and instead this option gives the points individual color
|
|
* class names on the form `highcharts-color-{n}`.
|
|
*
|
|
* @see [series colors](#plotOptions.map.colors)
|
|
*
|
|
* @sample {highmaps} maps/plotoptions/mapline-colorbypoint-false/
|
|
* Mapline colorByPoint set to false by default
|
|
* @sample {highmaps} maps/plotoptions/mapline-colorbypoint-true/
|
|
* Mapline colorByPoint set to true
|
|
*
|
|
* @type {boolean}
|
|
* @default false
|
|
* @since 2.0
|
|
* @product highmaps
|
|
* @apioption plotOptions.map.colorByPoint
|
|
*/
|
|
/**
|
|
* A series specific or series type specific color set to apply instead
|
|
* of the global [colors](#colors) when [colorByPoint](
|
|
* #plotOptions.map.colorByPoint) is true.
|
|
*
|
|
* @type {Array<Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject>}
|
|
* @since 3.0
|
|
* @product highmaps
|
|
* @apioption plotOptions.map.colors
|
|
*/
|
|
/**
|
|
* Individual color for the point. By default the color is either used
|
|
* to denote the value, or pulled from the global `colors` array.
|
|
*
|
|
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
|
|
* @product highmaps
|
|
* @apioption series.map.data.color
|
|
*/
|
|
/**
|
|
* Individual data label for each point. The options are the same as
|
|
* the ones for [plotOptions.series.dataLabels](
|
|
* #plotOptions.series.dataLabels).
|
|
*
|
|
* @sample maps/series/data-datalabels/
|
|
* Disable data labels for individual areas
|
|
*
|
|
* @type {Highcharts.DataLabelsOptions}
|
|
* @product highmaps
|
|
* @apioption series.map.data.dataLabels
|
|
*/
|
|
/**
|
|
* The `id` of a series in the [drilldown.series](#drilldown.series)
|
|
* array to use for a drilldown for this point.
|
|
*
|
|
* @sample maps/demo/map-drilldown/
|
|
* Basic drilldown
|
|
*
|
|
* @type {string}
|
|
* @product highmaps
|
|
* @apioption series.map.data.drilldown
|
|
*/
|
|
/**
|
|
* For map and mapline series types, the geometry of a point.
|
|
*
|
|
* To achieve a better separation between the structure and the data,
|
|
* it is recommended to use `mapData` to define the geometry instead
|
|
* of defining it on the data points themselves.
|
|
*
|
|
* The geometry object is compatible to that of a `feature` in GeoJSON, so
|
|
* features of GeoJSON can be passed directly into the `data`, optionally
|
|
* after first filtering and processing it.
|
|
*
|
|
* For pre-projected maps (like GeoJSON maps from our
|
|
* [map collection](https://code.highcharts.com/mapdata/)), user has to specify
|
|
* coordinates in `projectedUnits` for geometry type other than `Point`,
|
|
* instead of `[longitude, latitude]`.
|
|
*
|
|
* @sample maps/series/mappoint-line-geometry/
|
|
* Map point and line geometry
|
|
* @sample maps/series/geometry-types/
|
|
* Geometry types
|
|
*
|
|
* @type {Object}
|
|
* @since 9.3.0
|
|
* @product highmaps
|
|
* @apioption series.map.data.geometry
|
|
*/
|
|
/**
|
|
* The geometry type. Can be one of `LineString`, `Polygon`, `MultiLineString`
|
|
* or `MultiPolygon`.
|
|
*
|
|
* @sample maps/series/geometry-types/
|
|
* Geometry types
|
|
*
|
|
* @declare Highcharts.MapGeometryTypeValue
|
|
* @type {string}
|
|
* @since 9.3.0
|
|
* @product highmaps
|
|
* @validvalue ["LineString", "Polygon", "MultiLineString", "MultiPolygon"]
|
|
* @apioption series.map.data.geometry.type
|
|
*/
|
|
/**
|
|
* The geometry coordinates in terms of arrays of `[longitude, latitude]`, or
|
|
* a two dimensional array of the same. The dimensionality must comply with the
|
|
* `type`.
|
|
*
|
|
* @type {Array<LonLatArray>|Array<Array<LonLatArray>>}
|
|
* @since 9.3.0
|
|
* @product highmaps
|
|
* @apioption series.map.data.geometry.coordinates
|
|
*/
|
|
/**
|
|
* An id for the point. This can be used after render time to get a
|
|
* pointer to the point object through `chart.get()`.
|
|
*
|
|
* @sample maps/series/data-id/
|
|
* Highlight a point by id
|
|
*
|
|
* @type {string}
|
|
* @product highmaps
|
|
* @apioption series.map.data.id
|
|
*/
|
|
/**
|
|
* When data labels are laid out on a map, Highmaps runs a simplified
|
|
* algorithm to detect collision. When two labels collide, the one with
|
|
* the lowest rank is hidden. By default the rank is computed from the
|
|
* area.
|
|
*
|
|
* @type {number}
|
|
* @product highmaps
|
|
* @apioption series.map.data.labelrank
|
|
*/
|
|
/**
|
|
* The relative mid point of an area, used to place the data label.
|
|
* Ranges from 0 to 1\. When `mapData` is used, middleX can be defined
|
|
* there.
|
|
*
|
|
* @type {number}
|
|
* @default 0.5
|
|
* @product highmaps
|
|
* @apioption series.map.data.middleX
|
|
*/
|
|
/**
|
|
* The relative mid point of an area, used to place the data label.
|
|
* Ranges from 0 to 1\. When `mapData` is used, middleY can be defined
|
|
* there.
|
|
*
|
|
* @type {number}
|
|
* @default 0.5
|
|
* @product highmaps
|
|
* @apioption series.map.data.middleY
|
|
*/
|
|
/**
|
|
* The name of the point as shown in the legend, tooltip, dataLabel
|
|
* etc.
|
|
*
|
|
* @sample maps/series/data-datalabels/
|
|
* Point names
|
|
*
|
|
* @type {string}
|
|
* @product highmaps
|
|
* @apioption series.map.data.name
|
|
*/
|
|
/**
|
|
* For map and mapline series types, the SVG path for the shape. For
|
|
* compatibily with old IE, not all SVG path definitions are supported,
|
|
* but M, L and C operators are safe.
|
|
*
|
|
* To achieve a better separation between the structure and the data,
|
|
* it is recommended to use `mapData` to define that paths instead
|
|
* of defining them on the data points themselves.
|
|
*
|
|
* For providing true geographical shapes based on longitude and latitude, use
|
|
* the `geometry` option instead.
|
|
*
|
|
* @sample maps/series/data-path/
|
|
* Paths defined in data
|
|
*
|
|
* @type {string}
|
|
* @product highmaps
|
|
* @apioption series.map.data.path
|
|
*/
|
|
/**
|
|
* The numeric value of the data point.
|
|
*
|
|
* @type {number|null}
|
|
* @product highmaps
|
|
* @apioption series.map.data.value
|
|
*/
|
|
/**
|
|
* Individual point events
|
|
*
|
|
* @extends plotOptions.series.point.events
|
|
* @product highmaps
|
|
* @apioption series.map.data.events
|
|
*/
|
|
''; // adds doclets above to the transpiled file
|
|
|
|
return MapSeries;
|
|
});
|
|
_registerModule(_modules, 'Series/FlowMap/FlowMapSeries.js', [_modules['Series/FlowMap/FlowMapPoint.js'], _modules['Series/Map/MapSeries.js'], _modules['Core/Series/SeriesRegistry.js'], _modules['Core/Utilities.js']], function (FlowMapPoint, MapSeries, SeriesRegistry, U) {
|
|
/* *
|
|
*
|
|
* (c) 2010-2022 Askel Eirik Johansson, Piotr Madej
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
|
*
|
|
* */
|
|
const { series: { prototype: { pointClass: Point } }, seriesTypes: { column: ColumnSeries, mapline: MapLineSeries } } = SeriesRegistry;
|
|
const { addEvent, arrayMax, arrayMin, defined, extend, isArray, merge, pick, relativeLength } = U;
|
|
/**
|
|
* The flowmap series type
|
|
*
|
|
* @private
|
|
* @class
|
|
* @name Highcharts.seriesTypes.flowmap
|
|
*
|
|
* @augments Highcharts.Series
|
|
*/
|
|
class FlowMapSeries extends MapLineSeries {
|
|
constructor() {
|
|
/* *
|
|
*
|
|
* Static properties
|
|
*
|
|
* */
|
|
super(...arguments);
|
|
/* *
|
|
*
|
|
* Properties
|
|
*
|
|
* */
|
|
this.data = void 0;
|
|
this.options = void 0;
|
|
this.points = void 0;
|
|
this.smallestWeight = void 0;
|
|
this.greatestWeight = void 0;
|
|
this.centerOfPoints = void 0;
|
|
}
|
|
/* *
|
|
*
|
|
* Static Function
|
|
*
|
|
* */
|
|
/**
|
|
* Get vector length.
|
|
* @private
|
|
*/
|
|
static getLength(x, y) {
|
|
return Math.sqrt(x * x + y * y);
|
|
}
|
|
/**
|
|
* Return a normalized vector.
|
|
* @private
|
|
*/
|
|
static normalize(x, y) {
|
|
const length = this.getLength(x, y);
|
|
return [x / length, y / length];
|
|
}
|
|
/**
|
|
* Return an SVGPath for markerEnd.
|
|
* @private
|
|
*/
|
|
static markerEndPath(lCorner, rCorner, topCorner, options) {
|
|
const width = relativeLength(options.width || 0, this.getLength(rCorner[0] - lCorner[0], rCorner[1] - lCorner[1]));
|
|
const type = options.markerType || 'arrow', [edgeX, edgeY] = this.normalize(rCorner[0] - lCorner[0], rCorner[1] - lCorner[1]);
|
|
const path = [];
|
|
// For arrow head calculation.
|
|
if (type === 'arrow') {
|
|
// Left side of arrow head.
|
|
let [x, y] = lCorner;
|
|
x -= edgeX * width;
|
|
y -= edgeY * width;
|
|
path.push(['L', x, y]);
|
|
// Tip of arrow head.
|
|
path.push(['L', topCorner[0], topCorner[1]]);
|
|
// Right side of arrow head.
|
|
[x, y] = rCorner;
|
|
x += edgeX * width;
|
|
y += edgeY * width;
|
|
path.push(['L', x, y]);
|
|
}
|
|
// For mushroom head calculation.
|
|
if (type === 'mushroom') {
|
|
let [xLeft, yLeft] = lCorner, [xRight, yRight] = rCorner;
|
|
const [xTop, yTop] = topCorner, xMid = (xRight - xLeft) / 2 + xLeft, yMid = (yRight - yLeft) / 2 + yLeft,
|
|
// Control point for curve.
|
|
xControl = (xTop - xMid) * 2 + xMid, yControl = (yTop - yMid) * 2 + yMid;
|
|
// Left side of arrow head.
|
|
xLeft -= edgeX * width;
|
|
yLeft -= edgeY * width;
|
|
path.push(['L', xLeft, yLeft]);
|
|
// Right side of arrow head.
|
|
xRight += edgeX * width;
|
|
yRight += edgeY * width;
|
|
// Curve from left to right.
|
|
path.push(['Q', xControl, yControl, xRight, yRight]);
|
|
}
|
|
return path;
|
|
}
|
|
/**
|
|
*
|
|
* Functions
|
|
*
|
|
*/
|
|
/**
|
|
* Animate the flowmap point one by one from 'fromPoint'.
|
|
*
|
|
* @private
|
|
* @function Highcharts.seriesTypes.flowmap#animate
|
|
*
|
|
* @param {boolean} init
|
|
* Whether to initialize the animation or run it
|
|
*/
|
|
animate(init) {
|
|
const series = this, points = series.points;
|
|
if (!init) { // run the animation
|
|
points.forEach((point) => {
|
|
if (point.shapeArgs &&
|
|
isArray(point.shapeArgs.d) &&
|
|
point.shapeArgs.d.length) {
|
|
const path = point.shapeArgs.d, x = path[0][1], y = path[0][2];
|
|
// to animate SVG path the initial path array needs to be
|
|
// same as target, but element should be visible, so we
|
|
// insert array elements with start (M) values
|
|
if (x && y) {
|
|
const start = [];
|
|
for (let i = 0; i < path.length; i++) {
|
|
// Added any when merging master into another branch
|
|
// :((. The spread looks correct, but TS complains
|
|
// about possible number in the first position,
|
|
// which is the segment type.
|
|
start.push([...path[i]]);
|
|
for (let j = 1; j < path[i].length; j++) {
|
|
start[i][j] = j % 2 ? x : y;
|
|
}
|
|
}
|
|
if (point.graphic) {
|
|
point.graphic.attr({ d: start });
|
|
point.graphic.animate({ d: path });
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Get the actual width of a link either as a mapped weight between
|
|
* `minWidth` and `maxWidth` or a specified width.
|
|
* @private
|
|
*/
|
|
getLinkWidth(point) {
|
|
const width = this.options.width, weight = point.options.weight || this.options.weight;
|
|
point.options.weight = weight;
|
|
if (width && !weight) {
|
|
return width;
|
|
}
|
|
const smallestWeight = this.smallestWeight, greatestWeight = this.greatestWeight;
|
|
if (!defined(weight) || !smallestWeight || !greatestWeight) {
|
|
return 0;
|
|
}
|
|
const minWidthLimit = this.options.minWidth, maxWidthLimit = this.options.maxWidth;
|
|
return (weight - smallestWeight) * (maxWidthLimit - minWidthLimit) /
|
|
((greatestWeight - smallestWeight) || 1) + minWidthLimit;
|
|
}
|
|
/**
|
|
* Automatically calculate the optimal curve based on a reference point.
|
|
* @private
|
|
*/
|
|
autoCurve(fromX, fromY, toX, toY, centerX, centerY) {
|
|
const linkV = {
|
|
x: (toX - fromX),
|
|
y: (toY - fromY)
|
|
}, half = {
|
|
x: (toX - fromX) / 2 + fromX,
|
|
y: (toY - fromY) / 2 + fromY
|
|
}, centerV = {
|
|
x: half.x - centerX,
|
|
y: half.y - centerY
|
|
};
|
|
// Dot product and determinant
|
|
const dot = linkV.x * centerV.x + linkV.y * centerV.y, det = linkV.x * centerV.y - linkV.y * centerV.x;
|
|
// Calculate the angle and base the curveFactor on it.
|
|
let angle = Math.atan2(det, dot), angleDeg = angle * 180 / Math.PI;
|
|
if (angleDeg < 0) {
|
|
angleDeg = 360 + angleDeg;
|
|
}
|
|
angle = angleDeg * Math.PI / 180;
|
|
// A more subtle result.
|
|
return -Math.sin(angle) * 0.7;
|
|
}
|
|
/**
|
|
* Get point attributes.
|
|
* @private
|
|
*/
|
|
pointAttribs(point, state) {
|
|
const attrs = MapSeries.prototype.pointAttribs.call(this, point, state);
|
|
attrs.fill = pick(point.options.fillColor, point.options.color, this.options.fillColor === 'none' ? null : this.options.fillColor, this.color);
|
|
attrs['fill-opacity'] = pick(point.options.fillOpacity, this.options.fillOpacity);
|
|
attrs['stroke-width'] = pick(point.options.lineWidth, this.options.lineWidth, 1);
|
|
if (point.options.opacity) {
|
|
attrs.opacity = point.options.opacity;
|
|
}
|
|
return attrs;
|
|
}
|
|
/**
|
|
* Draw shapeArgs based on from/to options. Run translation operations. We
|
|
* need two loops: first loop to calculate data, like smallest/greatest
|
|
* weights and centerOfPoints, which needs the calculated positions, second
|
|
* loop for calculating shapes of points based on previous calculations.
|
|
* @private
|
|
*/
|
|
translate() {
|
|
if (this.chart.hasRendered && (this.isDirtyData || !this.hasRendered)) {
|
|
this.processData();
|
|
this.generatePoints();
|
|
}
|
|
const weights = [];
|
|
let averageX = 0, averageY = 0;
|
|
this.points.forEach((point) => {
|
|
const chart = this.chart, mapView = chart.mapView, options = point.options, dirtySeries = () => {
|
|
point.series.isDirty = true;
|
|
}, getPointXY = (pointId) => {
|
|
const foundPoint = chart.get(pointId);
|
|
// Connect to the linked parent point (in mappoint) to
|
|
// trigger series redraw for the linked point (in flow).
|
|
if ((foundPoint instanceof Point) &&
|
|
foundPoint.plotX &&
|
|
foundPoint.plotY) {
|
|
// after linked point update flowmap point should
|
|
// be also updated
|
|
addEvent(foundPoint, 'update', dirtySeries);
|
|
return {
|
|
x: foundPoint.plotX,
|
|
y: foundPoint.plotY
|
|
};
|
|
}
|
|
}, getLonLatXY = (lonLat) => {
|
|
if (isArray(lonLat)) {
|
|
return {
|
|
lon: lonLat[0],
|
|
lat: lonLat[1]
|
|
};
|
|
}
|
|
return lonLat;
|
|
};
|
|
let fromPos, toPos;
|
|
if (typeof options.from === 'string') {
|
|
fromPos = getPointXY(options.from);
|
|
}
|
|
else if (typeof options.from === 'object' && mapView) {
|
|
fromPos = mapView.lonLatToPixels(getLonLatXY(options.from));
|
|
}
|
|
if (typeof options.to === 'string') {
|
|
toPos = getPointXY(options.to);
|
|
}
|
|
else if (typeof options.to === 'object' && mapView) {
|
|
toPos = mapView.lonLatToPixels(getLonLatXY(options.to));
|
|
}
|
|
// Save original point location.
|
|
point.fromPos = fromPos;
|
|
point.toPos = toPos;
|
|
if (fromPos && toPos) {
|
|
averageX += (fromPos.x + toPos.x) / 2;
|
|
averageY += (fromPos.y + toPos.y) / 2;
|
|
}
|
|
if (pick(point.options.weight, this.options.weight)) {
|
|
weights.push(pick(point.options.weight, this.options.weight));
|
|
}
|
|
});
|
|
this.smallestWeight = arrayMin(weights);
|
|
this.greatestWeight = arrayMax(weights);
|
|
this.centerOfPoints = {
|
|
x: averageX / this.points.length,
|
|
y: averageY / this.points.length
|
|
};
|
|
this.points.forEach((point) => {
|
|
// Don't draw point if weight is not valid.
|
|
if (!this.getLinkWidth(point)) {
|
|
point.shapeArgs = {
|
|
d: []
|
|
};
|
|
return;
|
|
}
|
|
if (point.fromPos) {
|
|
point.plotX = point.fromPos.x;
|
|
point.plotY = point.fromPos.y;
|
|
}
|
|
// Calculate point shape
|
|
point.shapeType = 'path';
|
|
point.shapeArgs = this.getPointShapeArgs(point);
|
|
// When updating point from null to normal value, set a real color
|
|
// (don't keep nullColor).
|
|
point.color = pick(point.options.color, point.series.color);
|
|
});
|
|
}
|
|
getPointShapeArgs(point) {
|
|
const { fromPos, toPos } = point;
|
|
if (!fromPos || !toPos) {
|
|
return {};
|
|
}
|
|
const finalWidth = this.getLinkWidth(point) / 2, pointOptions = point.options, markerEndOptions = merge(this.options.markerEnd, pointOptions.markerEnd), growTowards = pick(pointOptions.growTowards, this.options.growTowards), fromX = fromPos.x || 0, fromY = fromPos.y || 0;
|
|
let toX = toPos.x || 0, toY = toPos.y || 0, curveFactor = pick(pointOptions.curveFactor, this.options.curveFactor), offset = markerEndOptions && markerEndOptions.enabled &&
|
|
markerEndOptions.height || 0;
|
|
if (!defined(curveFactor)) { // Automate the curveFactor value.
|
|
curveFactor = this.autoCurve(fromX, fromY, toX, toY, this.centerOfPoints.x, this.centerOfPoints.y);
|
|
}
|
|
// An offset makes room for arrows if they are specified.
|
|
if (offset) {
|
|
// Prepare offset if it's a percentage by converting to number.
|
|
offset = relativeLength(offset, finalWidth * 4);
|
|
// Vector between the points.
|
|
let dX = toX - fromX, dY = toY - fromY;
|
|
// Vector is halved.
|
|
dX *= 0.5;
|
|
dY *= 0.5;
|
|
// Vector points exactly between the points.
|
|
const mX = fromX + dX, mY = fromY + dY;
|
|
// Rotating the halfway distance by 90 anti-clockwise.
|
|
// We can then use this to create an arc.
|
|
const tmp = dX;
|
|
dX = dY;
|
|
dY = -tmp;
|
|
// Calculate the arc strength.
|
|
const arcPointX = (mX + dX * curveFactor), arcPointY = (mY + dY * curveFactor);
|
|
let [offsetX, offsetY] = FlowMapSeries.normalize(arcPointX - toX, arcPointY - toY);
|
|
offsetX *= offset;
|
|
offsetY *= offset;
|
|
toX += offsetX;
|
|
toY += offsetY;
|
|
}
|
|
// Vector between the points.
|
|
let dX = toX - fromX, dY = toY - fromY;
|
|
// Vector is halved.
|
|
dX *= 0.5;
|
|
dY *= 0.5;
|
|
// Vector points exactly between the points.
|
|
const mX = fromX + dX, mY = fromY + dY;
|
|
// Rotating the halfway distance by 90 anti-clockwise.
|
|
// We can then use this to create an arc.
|
|
let tmp = dX;
|
|
dX = dY;
|
|
dY = -tmp;
|
|
// Weight vector calculation for the middle of the curve.
|
|
let [wX, wY] = FlowMapSeries.normalize(dX, dY);
|
|
// The `fineTune` prevents an obvious mismatch along the curve.
|
|
const fineTune = 1 + Math.sqrt(curveFactor * curveFactor) * 0.25;
|
|
wX *= finalWidth * fineTune;
|
|
wY *= finalWidth * fineTune;
|
|
// Calculate the arc strength.
|
|
const arcPointX = (mX + dX * curveFactor), arcPointY = (mY + dY * curveFactor);
|
|
// Calculate edge vectors in the from-point.
|
|
let [fromXToArc, fromYToArc] = FlowMapSeries.normalize(arcPointX - fromX, arcPointY - fromY);
|
|
tmp = fromXToArc;
|
|
fromXToArc = fromYToArc;
|
|
fromYToArc = -tmp;
|
|
fromXToArc *= finalWidth;
|
|
fromYToArc *= finalWidth;
|
|
// Calculate edge vectors in the to-point.
|
|
let [toXToArc, toYToArc] = FlowMapSeries.normalize(arcPointX - toX, arcPointY - toY);
|
|
tmp = toXToArc;
|
|
toXToArc = -toYToArc;
|
|
toYToArc = tmp;
|
|
toXToArc *= finalWidth;
|
|
toYToArc *= finalWidth;
|
|
// Shrink the starting edge and middle thickness to make it grow
|
|
// towards the end.
|
|
if (growTowards) {
|
|
fromXToArc /= finalWidth;
|
|
fromYToArc /= finalWidth;
|
|
wX /= 4;
|
|
wY /= 4;
|
|
}
|
|
const shapeArgs = {
|
|
d: [[
|
|
'M',
|
|
fromX - fromXToArc,
|
|
fromY - fromYToArc
|
|
], [
|
|
'Q',
|
|
arcPointX - wX,
|
|
arcPointY - wY,
|
|
toX - toXToArc,
|
|
toY - toYToArc
|
|
], [
|
|
'L',
|
|
toX + toXToArc,
|
|
toY + toYToArc
|
|
], [
|
|
'Q',
|
|
arcPointX + wX,
|
|
arcPointY + wY,
|
|
fromX + fromXToArc,
|
|
fromY + fromYToArc
|
|
], [
|
|
'Z'
|
|
]]
|
|
};
|
|
if (markerEndOptions && markerEndOptions.enabled && shapeArgs.d) {
|
|
const marker = FlowMapSeries.markerEndPath([toX - toXToArc, toY - toYToArc], [toX + toXToArc, toY + toYToArc], [toPos.x, toPos.y], markerEndOptions);
|
|
shapeArgs.d.splice(2, 0, ...marker);
|
|
}
|
|
// Objects converted to string to be used in tooltip.
|
|
const fromPoint = point.options.from, toPoint = point.options.to, fromLat = fromPoint.lat, fromLon = fromPoint.lon, toLat = toPoint.lat, toLon = toPoint.lon;
|
|
if (fromLat && fromLon) {
|
|
point.options.from = `${+fromLat}, ${+fromLon}`;
|
|
}
|
|
if (toLat && toLon) {
|
|
point.options.to = `${+toLat}, ${+toLon}`;
|
|
}
|
|
return shapeArgs;
|
|
}
|
|
}
|
|
/**
|
|
* A flowmap series is a series laid out on top of a map series allowing to
|
|
* display route paths (e.g. flight or ship routes) or flows on a map. It
|
|
* creates a link between two points on a map chart.
|
|
*
|
|
* @since 11.0.0
|
|
* @extends plotOptions.mapline
|
|
* @excluding affectsMapView, allAreas, allowPointSelect, boostBlending,
|
|
* boostThreshold, borderColor, borderWidth, dashStyle, dataLabels,
|
|
* dragDrop, joinBy, mapData, negativeColor, onPoint, shadow, showCheckbox
|
|
* @product highmaps
|
|
* @requires modules/flowmap
|
|
* @optionparent plotOptions.flowmap
|
|
*/
|
|
FlowMapSeries.defaultOptions = merge(MapLineSeries.defaultOptions, {
|
|
animation: true,
|
|
/**
|
|
* The `curveFactor` option for all links. Value higher than 0 will
|
|
* curve the link clockwise. A negative value will curve it counter
|
|
* clockwise. If the value is 0 the link will be a straight line. By
|
|
* default undefined curveFactor get an automatic curve.
|
|
*
|
|
* @sample {highmaps} maps/series-flowmap/curve-factor Setting different
|
|
* values for curveFactor
|
|
*
|
|
* @type {number}
|
|
* @default undefined
|
|
* @apioption plotOptions.flowmap.curveFactor
|
|
*/
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
/**
|
|
* The fill color of all the links. If not set, the series color will be
|
|
* used with the opacity set in
|
|
* [fillOpacity](#plotOptions.flowmap.fillOpacity).
|
|
*
|
|
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
|
|
* @apioption plotOptions.flowmap.fillColor
|
|
*/
|
|
/**
|
|
* The opacity of the color fill for all links.
|
|
*
|
|
* @type {number}
|
|
* @sample {highmaps} maps/series-flowmap/fill-opacity
|
|
* Setting different values for fillOpacity
|
|
*/
|
|
fillOpacity: 0.5,
|
|
/**
|
|
* The [id](#series.id) of another series to link to. Additionally, the
|
|
* value can be ":previous" to link to the previous series. When two
|
|
* series are linked, only the first one appears in the legend. Toggling
|
|
* the visibility of this also toggles the linked series, which is
|
|
* necessary for operations such as zoom or updates on the flowmap
|
|
* series.
|
|
*
|
|
* @type {string}
|
|
* @apioption plotOptions.flowmap.linkedTo
|
|
*/
|
|
/**
|
|
* A `markerEnd` creates an arrow symbol indicating the direction of
|
|
* flow at the destination. Specifying a `markerEnd` here will create
|
|
* one for each link.
|
|
*
|
|
* @declare Highcharts.SeriesFlowMapSeriesOptionsObject
|
|
*/
|
|
markerEnd: {
|
|
/**
|
|
* Enable or disable the `markerEnd`.
|
|
*
|
|
* @type {boolean}
|
|
* @sample {highmaps} maps/series-flowmap/marker-end
|
|
* Setting different markerType for markerEnd
|
|
*/
|
|
enabled: true,
|
|
/**
|
|
* Height of the `markerEnd`. Can be a number in pixels or a
|
|
* percentage based on the weight of the link.
|
|
*
|
|
* @type {number|string}
|
|
*/
|
|
height: '40%',
|
|
/**
|
|
* Width of the `markerEnd`. Can be a number in pixels or a
|
|
* percentage based on the weight of the link.
|
|
*
|
|
* @type {number|string}
|
|
*/
|
|
width: '40%',
|
|
/**
|
|
* Change the shape of the `markerEnd`.
|
|
* Can be `arrow` or `mushroom`.
|
|
*
|
|
* @type {string}
|
|
*/
|
|
markerType: 'arrow'
|
|
},
|
|
/**
|
|
* If no weight has previously been specified, this will set the width
|
|
* of all the links without being compared to and scaled according to
|
|
* other weights.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
width: 1,
|
|
/**
|
|
* Maximum width of a link expressed in pixels. The weight of a link is
|
|
* mapped between `maxWidth` and `minWidth`.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
maxWidth: 25,
|
|
/**
|
|
* Minimum width of a link expressed in pixels. The weight of a link is
|
|
* mapped between `maxWidth` and `minWidth`.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
minWidth: 5,
|
|
/**
|
|
* Specify the `lineWidth` of the links if they are not specified.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
lineWidth: void 0,
|
|
/**
|
|
* The opacity of all the links. Affects the opacity for the entire
|
|
* link, including stroke. See also
|
|
* [fillOpacity](#plotOptions.flowmap.fillOpacity), that affects the
|
|
* opacity of only the fill color.
|
|
*
|
|
* @apioption plotOptions.flowmap.opacity
|
|
*/
|
|
/**
|
|
* The weight for all links with unspecified weights. The weight of a
|
|
* link determines its thickness compared to other links.
|
|
*
|
|
* @sample {highmaps} maps/series-flowmap/ship-route/ Example ship route
|
|
*
|
|
* @type {number}
|
|
* @product highmaps
|
|
* @apioption plotOptions.flowmap.weight
|
|
*/
|
|
tooltip: {
|
|
/**
|
|
* The HTML for the flowmaps' route description in the tooltip. It
|
|
* consists of the `headerFormat` and `pointFormat`, which can be
|
|
* edited. Variables are enclosed by curly brackets. Available
|
|
* variables are `series.name`, `point.options.from`,
|
|
* `point.options.to`, `point.options.weight` and other properties in the
|
|
* same form.
|
|
*
|
|
* @product highmaps
|
|
*/
|
|
headerFormat: '<span style="font-size: 0.8em">{series.name}</span><br/>',
|
|
pointFormat: '{point.options.from} \u2192 {point.options.to}: <b>{point.options.weight}</b>'
|
|
}
|
|
});
|
|
extend(FlowMapSeries.prototype, {
|
|
pointClass: FlowMapPoint,
|
|
pointArrayMap: ['from', 'to', 'weight'],
|
|
drawPoints: ColumnSeries.prototype.drawPoints,
|
|
// Make it work on zoom or pan.
|
|
useMapGeometry: true
|
|
});
|
|
SeriesRegistry.registerSeriesType('flowmap', FlowMapSeries);
|
|
/* *
|
|
*
|
|
* Default export
|
|
*
|
|
* */
|
|
/* *
|
|
*
|
|
* API options
|
|
*
|
|
* */
|
|
/**
|
|
* A `flowmap` series. If the [type](#series.flowmap.type) option
|
|
* is not specified, it is inherited from [chart.type](#chart.type).
|
|
*
|
|
* @extends series,plotOptions.flowmap
|
|
* @excluding affectsMapView, allAreas, allowPointSelect, boostBlending,
|
|
* boostThreshold, borderColor, borderWidth, dashStyle, dataLabels, dragDrop,
|
|
* joinBy, mapData, negativeColor, onPoint, shadow, showCheckbox
|
|
* @product highmaps
|
|
* @apioption series.flowmap
|
|
*/
|
|
/**
|
|
* An array of data points for the series. For the `flowmap` series
|
|
* type, points can be given in the following ways:
|
|
*
|
|
* 1. An array of arrays with options as values. In this case,
|
|
* the values correspond to `from, to, weight`. Example:
|
|
* ```js
|
|
* data: [
|
|
* ['Point 1', 'Point 2', 4]
|
|
* ]
|
|
* ```
|
|
*
|
|
* 2. An array of objects with named values. The following snippet shows only a
|
|
* few settings, see the complete options set below.
|
|
*
|
|
* ```js
|
|
* data: [{
|
|
* from: 'Point 1',
|
|
* to: 'Point 2',
|
|
* curveFactor: 0.4,
|
|
* weight: 5,
|
|
* growTowards: true,
|
|
* markerEnd: {
|
|
* enabled: true,
|
|
* height: 15,
|
|
* width: 8
|
|
* }
|
|
* }]
|
|
* ```
|
|
*
|
|
* 3. For objects with named values, instead of using the `mappoint` `id`,
|
|
* you can use `[longitude, latitude]` arrays.
|
|
*
|
|
* ```js
|
|
* data: [{
|
|
* from: [longitude, latitude],
|
|
* to: [longitude, latitude]
|
|
* }]
|
|
* ```
|
|
*
|
|
* @type {Array<number|null|*>}
|
|
* @apioption series.flowmap.data
|
|
*/
|
|
/**
|
|
* A `curveFactor` with a higher value than 0 will curve the link clockwise.
|
|
* A negative value will curve the link counter clockwise.
|
|
* If the value is 0 the link will be straight.
|
|
*
|
|
* @sample {highmaps} maps/series-flowmap/ship-route/
|
|
* Example ship route
|
|
*
|
|
* @type {number}
|
|
* @apioption series.flowmap.data.curveFactor
|
|
*/
|
|
/**
|
|
* The fill color of an individual link.
|
|
*
|
|
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
|
|
* @apioption series.flowmap.data.fillColor
|
|
*/
|
|
/**
|
|
* ID referencing a map point holding coordinates of the link origin or
|
|
* coordinates in terms of array of `[longitude, latitude]` or object with `lon`
|
|
* and `lat` properties.
|
|
*
|
|
* @sample {highmaps} maps/series-flowmap/from-to-lon-lat
|
|
* Flowmap point using lonlat coordinates
|
|
* @sample {highmaps} maps/series-flowmap/flight-routes
|
|
* Highmaps basic flight routes demo
|
|
*
|
|
* @type {string|Highcharts.LonLatArray|Highcharts.MapLonLatObject}
|
|
* @apioption series.flowmap.data.from
|
|
*/
|
|
/**
|
|
* ID referencing a map point holding coordinates of the link origin or
|
|
* coordinates in terms of array of `[longitude, latitude]` or object with `lon`
|
|
* and `lat` properties.
|
|
*
|
|
* @sample {highmaps} maps/series-flowmap/from-to-lon-lat
|
|
* Flowmap point using lonlat coordinates
|
|
* @sample {highmaps} maps/series-flowmap/flight-routes
|
|
* Highmaps basic flight routes demo
|
|
*
|
|
* @type {string|Highcharts.LonLatArray|Highcharts.MapLonLatObject}
|
|
* @apioption series.flowmap.data.to
|
|
*/
|
|
/**
|
|
* The opacity of the link color fill.
|
|
*
|
|
* @type {number}
|
|
* @apioption series.flowmap.data.fillOpacity
|
|
*/
|
|
/**
|
|
* If set to `true`, the line will grow towards its end.
|
|
*
|
|
* @sample {highmaps} maps/series-flowmap/ship-route/
|
|
* Example ship route
|
|
*
|
|
* @type {boolean}
|
|
* @apioption series.flowmap.data.growTowards
|
|
*/
|
|
/**
|
|
* Specifying a `markerEnd` here will create an arrow symbol
|
|
* indicating the direction of flow at the destination of one individual link.
|
|
* If one has been previously specified at the higher level option it will be
|
|
* overridden for the current link.
|
|
*
|
|
* @sample {highmaps} maps/series-flowmap/ship-route/
|
|
* Example ship route
|
|
*
|
|
* @type {*|null}
|
|
* @apioption series.flowmap.data.markerEnd
|
|
*/
|
|
/**
|
|
* Enable or disable the `markerEnd`.
|
|
*
|
|
* @type {boolean}
|
|
* @apioption series.flowmap.data.markerEnd.enabled
|
|
*/
|
|
/**
|
|
* Height of the `markerEnd`. Can be a number in pixels
|
|
* or a percentage based on the weight of the link.
|
|
*
|
|
* @type {number|string}
|
|
* @apioption series.flowmap.data.markerEnd.height
|
|
*/
|
|
/**
|
|
* Width of the `markerEnd`. Can be a number in pixels
|
|
* or a percentage based on the weight of the link.
|
|
*
|
|
* @type {number|string}
|
|
* @apioption series.flowmap.data.markerEnd.width
|
|
*/
|
|
/**
|
|
* Change the shape of the `markerEnd`. Can be `arrow` or `mushroom`.
|
|
*
|
|
* @type {string}
|
|
* @apioption series.flowmap.data.markerEnd.markerType
|
|
*/
|
|
/**
|
|
* The opacity of an individual link.
|
|
*
|
|
* @type {number}
|
|
* @apioption series.flowmap.data.opacity
|
|
*/
|
|
/**
|
|
* The weight of a link determines its thickness compared to
|
|
* other links.
|
|
*
|
|
* @sample {highmaps} maps/series-flowmap/ship-route/
|
|
* Example ship route
|
|
*
|
|
* @type {number}
|
|
* @apioption series.flowmap.data.weight
|
|
*/
|
|
/**
|
|
* Specify the `lineWidth` of the link.
|
|
*
|
|
* @type {number}
|
|
* @apioption series.flowmap.data.lineWidth
|
|
*/
|
|
''; // adds doclets above to transpiled file
|
|
|
|
return FlowMapSeries;
|
|
});
|
|
_registerModule(_modules, 'masters/modules/flowmap.src.js', [], function () {
|
|
|
|
|
|
});
|
|
})); |