avances en plantillas

This commit is contained in:
JACS 2026-05-01 18:15:40 -05:00
parent 0f84beacf1
commit da0530d79b
2062 changed files with 598814 additions and 22 deletions

View file

@ -0,0 +1,18 @@
import { removeElement } from '../util'
class BaseComponent {
dispose() {
if (this._tooltip) {
removeElement(this._tooltip)
} else {
// @todo: move shape in base component in v2
this.shape.remove()
}
for (const propertyName of Object.getOwnPropertyNames(this)) {
this[propertyName] = null
}
}
}
export default BaseComponent

View file

@ -0,0 +1,68 @@
const Interactable = {
getLabelText(key, label) {
if (!label) {
return
}
if (typeof label.render === 'function') {
let params = []
// Pass additional paramater (Marker config object) in case it's a Marker.
if (this.constructor.Name === 'marker') {
params.push(this.getConfig())
}
// Becuase we need to add the key always at the end
params.push(key)
return label.render.apply(this, params)
}
return key
},
getLabelOffsets(key, label) {
if (typeof label.offsets === 'function') {
return label.offsets(key)
}
// If offsets are an array of offsets e.g offsets: [ [0, 25], [10, 15] ]
if (Array.isArray(label.offsets)) {
return label.offsets[key]
}
return [0, 0]
},
setStyle(property, value) {
this.shape.setStyle(property, value)
},
remove() {
this.shape.remove()
if (this.label) this.label.remove()
},
hover(state) {
this._setStatus('isHovered', state)
},
select(state) {
this._setStatus('isSelected', state)
},
// Private
_setStatus(property, state) {
this.shape[property] = state
this.shape.updateStyle()
this[property] = state
if (this.label) {
this.label[property] = state
this.label.updateStyle()
}
}
}
export default Interactable

View file

@ -0,0 +1,50 @@
import BaseComponent from './base'
const LINE_CLASS = 'jvm-line'
class Line extends BaseComponent {
constructor(options, style) {
super()
this._options = options
this._style = { initial: style }
this._draw()
}
setStyle(property, value) {
this.shape.setStyle(property, value)
}
getConfig() {
return this._options.config
}
_draw() {
const { index, group, map } = this._options
const config = {
d: this._getDAttribute(),
fill: 'none',
dataIndex: index,
}
this.shape = map.canvas.createPath(config, this._style, group)
this.shape.addClass(LINE_CLASS)
}
_getDAttribute() {
const { x1, y1, x2, y2, curvature } = this._options
return `M${x1},${y1}${this._getQCommand(x1, y1, x2, y2, curvature)}${x2},${y2}`
}
_getQCommand(x1, y1, x2, y2, curvature) {
if (!curvature) {
return ' '
}
const curveX = (x1 + x2) / 2 + curvature * (y2 - y1)
const curveY = (y1 + y2) / 2 - curvature * (x2 - x1)
return ` Q${curveX},${curveY} `
}
}
export default Line

View file

@ -0,0 +1,109 @@
import { inherit } from '../util'
import BaseComponent from './base'
import Interactable from './concerns/interactable'
const NAME = 'marker'
const JVM_PREFIX = 'jvm-'
const MARKER_CLASS = `${JVM_PREFIX}element ${JVM_PREFIX}marker`
const MARKER_LABEL_CLASS = `${JVM_PREFIX}element ${JVM_PREFIX}label`
class Marker extends BaseComponent {
static get Name() {
return NAME
}
constructor(options, style) {
super()
this._options = options
this._style = style
this._labelX = null
this._labelY = null
this._offsets = null
this._isImage = !!style.initial.image
this._draw()
if (this._options.label) {
this._drawLabel()
}
if (this._isImage) {
this.updateLabelPosition()
}
}
getConfig() {
return this._options.config
}
updateLabelPosition() {
const map = this._options.map
if (this.label) {
this.label.set({
x:
this._labelX * map.scale +
this._offsets[0] +
map.transX * map.scale +
5 +
(this._isImage
? (this.shape.width || 0) / 2
: this.shape.node.r.baseVal.value),
y:
this._labelY * map.scale +
map.transY * this._options.map.scale +
this._offsets[1],
})
}
}
_draw() {
const { index, map, group, cx, cy } = this._options
const shapeType = this._isImage ? 'createImage' : 'createCircle'
this.shape = map.canvas[shapeType](
{ dataIndex: index, cx, cy },
this._style,
group
)
this.shape.addClass(MARKER_CLASS)
}
_drawLabel() {
const {
index,
map,
label,
labelsGroup,
cx,
cy,
config,
isRecentlyCreated,
} = this._options
const labelText = this.getLabelText(index, label)
this._labelX = cx / map.scale - map.transX
this._labelY = cy / map.scale - map.transY
this._offsets = isRecentlyCreated && config.offsets ? config.offsets : this.getLabelOffsets(index, label)
this.label = map.canvas.createText(
{
text: labelText,
dataIndex: index,
x: this._labelX,
y: this._labelY,
dy: '0.6ex',
},
map.params.markerLabelStyle,
labelsGroup
)
this.label.addClass(MARKER_LABEL_CLASS)
if (isRecentlyCreated) {
this.updateLabelPosition()
}
}
}
inherit(Marker, Interactable)
export default Marker

View file

@ -0,0 +1,53 @@
import { inherit } from '../util'
import BaseComponent from './base'
import Interactable from './concerns/interactable'
class Region extends BaseComponent {
constructor({ map, code, path, style, label, labelStyle, labelsGroup }) {
super()
this._map = map
this.shape = this._createRegion(path, code, style)
const text = this.getLabelText(code, label)
// If label is passed and render function returns something
if (label && text) {
const bbox = this.shape.getBBox()
const offsets = this.getLabelOffsets(code, label)
this.labelX = bbox.x + bbox.width / 2 + offsets[0]
this.labelY = bbox.y + bbox.height / 2 + offsets[1]
this.label = this._map.canvas.createText({
text,
textAnchor: 'middle',
alignmentBaseline: 'central',
dataCode: code,
x: this.labelX,
y: this.labelY,
}, labelStyle, labelsGroup)
this.label.addClass('jvm-region jvm-element')
}
}
_createRegion(path, code, style) {
path = this._map.canvas.createPath({ d: path, dataCode: code }, style)
path.addClass('jvm-region jvm-element')
return path
}
updateLabelPosition() {
if (this.label) {
this.label.set({
x: this.labelX * this._map.scale + this._map.transX * this._map.scale,
y: this.labelY * this._map.scale + this._map.transY * this._map.scale
})
}
}
}
inherit(Region, Interactable)
export default Region

View file

@ -0,0 +1,76 @@
import Component from './base';
export class Route extends Component {
constructor(options, style) {
super();
this._options = options;
this._style = style;
this._draw();
}
_draw() {
const { node: path } = this._options.map.canvas.createPath(
{
d: this._getDAttribute(),
fill: 'none',
stroke: '#666',
strokeWidth: 1,
dataIndex: this._options.index,
'stroke-dasharray': '2 4 2',
},
this.style,
this._options.group
);
// Get the total length of the path
const totalLength = path.getTotalLength();
// Initialize the stroke dash array and offset to create the drawing effect
path.style.strokeDasharray = totalLength;
path.style.strokeDashoffset = totalLength;
// Animate the stroke dash offset to draw the line
path.animate([{ strokeDashoffset: totalLength }, { strokeDashoffset: 0 }], {
duration: 4000, // Duration of the animation
easing: 'linear', // Easing function
fill: 'forwards', // Keep the line visible after animation ends
});
}
_getDAttribute() {
const curvature = -0.2;
const points = this._getPoints();
let d = `M${points[0].x},${points[0].y}`;
for (let i = 0; i < points.length - 1; i++) {
const nextPoint = points[i + 1];
const cpX =
(points[i].x + nextPoint.x) / 2 +
curvature * (nextPoint.y - points[i].y);
const cpY =
(points[i].y + nextPoint.y) / 2 -
curvature * (nextPoint.x - points[i].x);
d += ` Q${cpX},${cpY} ${nextPoint.x},${nextPoint.y}`;
}
// let d = `M${points[0].x},${points[0].y}`
// for (let i = 1; i < points.length; i++) {
// d += ` L${points[i].x},${points[i].y}`
// }
return d
}
_getPoints() {
const map = this._options.map;
const points = [];
for (let i = 0; i < this._options.waypoints.length; i++) {
const p = this._options.map.getMarkerPosition({
coords: [
this._options.waypoints[i].lat,
this._options.waypoints[i].lng,
],
});
points.push(p);
}
return points;
}
}

View file

@ -0,0 +1,88 @@
import {
createElement,
findElement,
} from '../util'
import EventHandler from '../eventHandler'
import BaseComponent from './base'
class Tooltip extends BaseComponent {
constructor(map) {
super()
const tooltip = createElement('div', 'jvm-tooltip')
this._map = map
this._tooltip = document.body.appendChild(tooltip)
this._bindEventListeners()
return this
}
_bindEventListeners() {
EventHandler.on(this._map.container, 'mousemove', event => {
if (!this._tooltip.classList.contains('active')) {
return
}
const container = findElement(this._map.container, '#jvm-regions-group').getBoundingClientRect()
const space = 5 // Space between the cursor and tooltip element
// Tooltip
const { height, width } = this._tooltip.getBoundingClientRect()
const topIsPassed = event.clientY <= (container.top + height + space)
let top = event.pageY - height - space
let left = event.pageX - width - space
// Ensure the tooltip will never cross outside the canvas area(map)
if (topIsPassed) { // Top:
top += height + space
// The cursor is a bit larger from left side
left -= space * 2
}
if (event.clientX < (container.left + width + space)) { // Left:
left = event.pageX + space + 2
if (topIsPassed) {
left += space * 2
}
}
this.css({ top: `${top}px`, left: `${left}px` })
})
}
getElement() {
return this._tooltip
}
show() {
this._tooltip.classList.add('active')
}
hide() {
this._tooltip.classList.remove('active')
}
text(string, html = false) {
const property = html ? 'innerHTML' : 'textContent'
if (!string) {
return this._tooltip[property]
}
this._tooltip[property] = string
}
css(css) {
for (let style in css) {
this._tooltip.style[style] = css[style]
}
return this
}
}
export default Tooltip

View file

@ -0,0 +1,43 @@
export default function applyTransform() {
let maxTransX, maxTransY, minTransX, minTransY
if (this._defaultWidth * this.scale <= this._width) {
maxTransX = (this._width - this._defaultWidth * this.scale) / (2 * this.scale)
minTransX = (this._width - this._defaultWidth * this.scale) / (2 * this.scale)
} else {
maxTransX = 0
minTransX = (this._width - this._defaultWidth * this.scale) / this.scale
}
if (this._defaultHeight * this.scale <= this._height) {
maxTransY = (this._height - this._defaultHeight * this.scale) / (2 * this.scale)
minTransY = (this._height - this._defaultHeight * this.scale) / (2 * this.scale)
} else {
maxTransY = 0
minTransY = (this._height - this._defaultHeight * this.scale) / this.scale
}
if (this.transY > maxTransY) {
this.transY = maxTransY
} else if (this.transY < minTransY) {
this.transY = minTransY
}
if (this.transX > maxTransX) {
this.transX = maxTransX
} else if (this.transX < minTransX) {
this.transX = minTransX
}
this.canvas.applyTransformParams(this.scale, this.transX, this.transY)
if (this._markers) {
this._repositionMarkers()
}
if (this._lines) {
this._repositionLines()
}
this._repositionLabels()
}

View file

@ -0,0 +1,22 @@
import Map from '../map'
import Proj from '../projection'
export default function coordsToPoint(lat, lng) {
const projection = Map.maps[this.params.map].projection
let { x, y } = Proj[projection.type](lat, lng, projection.centralMeridian)
let inset = this.getInsetForPoint(x, y)
if (!inset) {
return false
}
let bbox = inset.bbox
x = (x - bbox[0].x) / (bbox[1].x - bbox[0].x) * inset.width * this.scale
y = (y - bbox[0].y) / (bbox[1].y - bbox[0].y) * inset.height * this.scale
return {
x: x + this.transX * this.scale + inset.left * this.scale,
y: y + this.transY * this.scale + inset.top * this.scale
}
}

View file

@ -0,0 +1,44 @@
import { merge, getLineUid } from '../util'
import Line from '../components/line'
export default function createLines(lines) {
let point1 = false, point2 = false
const { curvature, ...lineStyle } = this.params.lineStyle
for (let index in lines) {
const lineConfig = lines[index]
for (let { config: markerConfig } of Object.values(this._markers)) {
if (markerConfig.name === lineConfig.from) {
point1 = this.getMarkerPosition(markerConfig)
}
if (markerConfig.name === lineConfig.to) {
point2 = this.getMarkerPosition(markerConfig)
}
}
if (point1 !== false && point2 !== false) {
const {
curvature: curvatureOption,
...style
} = lineConfig.style || {}
// Register lines with unique keys
this._lines[getLineUid(lineConfig.from, lineConfig.to)] = new Line(
{
index,
map: this,
group: this._linesGroup,
config: lineConfig,
x1: point1.x,
y1: point1.y,
x2: point2.x,
y2: point2.y,
curvature: curvatureOption == 0 ? 0 : (curvatureOption || curvature),
},
merge(lineStyle, style, true)
)
}
}
}

View file

@ -0,0 +1,55 @@
import { merge } from '../util'
import Marker from '../components/marker'
export default function createMarkers(markers = {}, isRecentlyCreated = false) {
for (let index in markers) {
const config = markers[index]
const point = this.getMarkerPosition(config)
const uid = config.coords.join(':')
if (!point) {
continue
}
// We're checking if recently created marker does already exist
// If it does we don't need to create it again, so we'll continue
// Becuase we may have more than one marker submitted via `addMarkers` method.
if (isRecentlyCreated) {
if (
Object.keys(this._markers).filter(i => this._markers[i]._uid === uid).length
) {
continue
}
index = Object.keys(this._markers).length
}
const marker = new Marker(
{
index,
map: this,
label: this.params.labels && this.params.labels.markers,
labelsGroup: this._markerLabelsGroup,
cx: point.x,
cy: point.y,
group: this._markersGroup,
config,
isRecentlyCreated,
},
merge(this.params.markerStyle, { ...(config.style || {}) }, true)
)
// Check for marker duplication
// this is useful when for example: a user clicks a button for creating marker two times
// so it will remove the old one and the new one will take its place.
if (this._markers[index]) {
this.removeMarkers([index])
}
this._markers[index] = {
_uid: uid,
config: config,
element: marker,
}
}
}

View file

@ -0,0 +1,23 @@
import { merge } from '../util'
import Region from '../components/region'
export default function createRegions() {
this._regionLabelsGroup = this._regionLabelsGroup || this.canvas.createGroup('jvm-regions-labels-group')
for (const code in this._mapData.paths) {
const region = new Region({
map: this,
code: code,
path: this._mapData.paths[code].path,
style: merge({}, this.params.regionStyle),
labelStyle: this.params.regionLabelStyle,
labelsGroup: this._regionLabelsGroup,
label: this.params.labels && this.params.labels.regions,
})
this.regions[code] = {
config: this._mapData.paths[code],
element: region,
}
}
}

View file

@ -0,0 +1,16 @@
import waypoints from "../../../../playground/data/waypoints"
import { Route } from "../components/route"
export default function createRoutes(routes) {
this._routes = {}
let index = 0
for (let route of routes) {
this._routes[route.name] = new Route({
map: this,
group: this._routesGroup,
waypoints: route.data,
index: index++,
}, {})
}
}

View file

@ -0,0 +1,13 @@
import Series from '../series'
export default function createSeries() {
this.series = { markers: [], regions: [] }
for (const key in this.params.series) {
for (let i = 0; i < this.params.series[key].length; i++) {
this.series[key][i] = new Series(
this.params.series[key][i], key === 'markers' ? this._markers : this.regions, this
)
}
}
}

View file

@ -0,0 +1,13 @@
import Map from '../map'
export default function getInsetForPoint(x, y) {
const insets = Map.maps[this.params.map].insets
for (let index = 0; index < insets.length; index++) {
const [start, end] = insets[index].bbox
if (x > start.x && x < end.x && y > start.y && y < end.y) {
return insets[index]
}
}
}

View file

@ -0,0 +1,12 @@
import Map from '../map'
export default function getMarkerPosition({ coords }) {
if (Map.maps[this.params.map].projection) {
return this.coordsToPoint(...coords)
}
return {
x: coords[0] * this.scale + this.transX * this.scale,
y: coords[1] * this.scale + this.transY * this.scale
}
}

View file

@ -0,0 +1,41 @@
import _setupContainerEvents from './setupContainerEvents'
import _setupElementEvents from './setupElementEvents'
import _setupZoomButtons from './setupZoomButtons'
import _setupContainerTouchEvents from './setupContainerTouchEvents'
import _createRegions from './createRegions'
import _createLines from './createLines'
import _createMarkers from './createMarkers'
import _createSeries from './createSeries'
import _applyTransform from './applyTransform'
import _resize from './resize'
import _setScale from './setScale'
import setFocus from './setFocus'
import updateSize from './updateSize'
import coordsToPoint from './coordsToPoint'
import getInsetForPoint from './getInsetForPoint'
import getMarkerPosition from './getMarkerPosition'
import _repositionLines from './repositionLines'
import _repositionMarkers from './repositionMarkers'
import _repositionLabels from './repositionLabels'
export default {
_setupContainerEvents,
_setupElementEvents,
_setupZoomButtons,
_setupContainerTouchEvents,
_createRegions,
_createLines,
_createMarkers,
_createSeries,
_applyTransform,
_resize,
_setScale,
setFocus,
updateSize,
coordsToPoint,
getInsetForPoint,
getMarkerPosition,
_repositionLines,
_repositionMarkers,
_repositionLabels,
}

View file

@ -0,0 +1,21 @@
export default function repositionLabels() {
const labels = this.params.labels
if (!labels) {
return
}
// Regions labels
if (labels.regions) {
for (const key in this.regions) {
this.regions[key].element.updateLabelPosition()
}
}
// Markers labels
if (labels.markers) {
for (const key in this._markers) {
this._markers[key].element.updateLabelPosition()
}
}
}

View file

@ -0,0 +1,30 @@
export default function repositionLines() {
const curvature = this.params.lineStyle.curvature
Object.values(this._lines).forEach((line) => {
const startMarker = Object.values(this._markers).find(
({ config }) => config.name === line.getConfig().from
)
const endMarker = Object.values(this._markers).find(
({ config }) => config.name === line.getConfig().to
)
if (startMarker && endMarker) {
const { x: x1, y: y1 } = this.getMarkerPosition(startMarker.config)
const { x: x2, y: y2 } = this.getMarkerPosition(endMarker.config)
const curvatureOption = line._options.curvature == 0
? 0
: line._options.curvature || curvature
const midX = (x1 + x2) / 2
const midY = (y1 + y2) / 2
const curveX = midX + curvatureOption * (y2 - y1)
const curveY = midY - curvatureOption * (x2 - x1)
line.setStyle({
d: `M${x1},${y1} Q${curveX},${curveY} ${x2},${y2}`,
})
}
})
}

View file

@ -0,0 +1,11 @@
export default function repositionMarkers() {
for (const index in this._markers) {
const point = this.getMarkerPosition(this._markers[index].config)
if (point !== false) {
this._markers[index].element.setStyle({
cx: point.x, cy: point.y
})
}
}
}

View file

@ -0,0 +1,15 @@
export default function resize() {
const curBaseScale = this._baseScale
if (this._width / this._height > this._defaultWidth / this._defaultHeight) {
this._baseScale = this._height / this._defaultHeight
this._baseTransX = Math.abs(this._width - this._defaultWidth * this._baseScale) / (2 * this._baseScale)
} else {
this._baseScale = this._width / this._defaultWidth
this._baseTransY = Math.abs(this._height - this._defaultHeight * this._baseScale) / (2 * this._baseScale)
}
this.scale *= this._baseScale / curBaseScale
this.transX *= this._baseScale / curBaseScale
this.transY *= this._baseScale / curBaseScale
}

View file

@ -0,0 +1,46 @@
export default function setFocus(config = {}) {
let bbox, codes = []
if (config.region) {
codes.push(config.region)
} else if (config.regions) {
codes = config.regions
}
if (codes.length) {
codes.forEach((code) => {
if (this.regions[code]) {
let itemBbox = this.regions[code].element.shape.getBBox()
if (itemBbox) {
// Handle the first loop
if (typeof bbox == 'undefined') {
bbox = itemBbox
} else {
// get the old bbox properties plus the current
// this kinda incrementing the old values and the new values
bbox = {
x: Math.min(bbox.x, itemBbox.x),
y: Math.min(bbox.y, itemBbox.y),
width: Math.max(bbox.x + bbox.width, itemBbox.x + itemBbox.width) - Math.min(bbox.x, itemBbox.x),
height: Math.max(bbox.y + bbox.height, itemBbox.y + itemBbox.height) - Math.min(bbox.y, itemBbox.y),
}
}
}
}
})
return this._setScale(
Math.min(this._width / bbox.width, this._height / bbox.height),
-(bbox.x + bbox.width / 2),
-(bbox.y + bbox.height / 2),
true,
config.animate,
)
} else if (config.coords) {
const point = this.coordsToPoint(config.coords[0], config.coords[1])
const x = this.transX - point.x / this.scale
const y = this.transY - point.y / this.scale
return this._setScale(config.scale * this._baseScale, x, y, true, config.animate)
}
}

View file

@ -0,0 +1,65 @@
import Events from '../defaults/events'
export default function setScale(scale, anchorX, anchorY, isCentered, animate) {
let zoomStep,
interval,
i = 0,
count = Math.abs(Math.round((scale - this.scale) * 60 / Math.max(scale, this.scale))),
scaleStart,
scaleDiff,
transXStart,
transXDiff,
transYStart,
transYDiff,
transX,
transY
if (scale > this.params.zoomMax * this._baseScale) {
scale = this.params.zoomMax * this._baseScale
} else if (scale < this.params.zoomMin * this._baseScale) {
scale = this.params.zoomMin * this._baseScale
}
if (typeof anchorX != 'undefined' && typeof anchorY != 'undefined') {
zoomStep = scale / this.scale
if (isCentered) {
transX = anchorX + this._defaultWidth * (this._width / (this._defaultWidth * scale)) / 2
transY = anchorY + this._defaultHeight * (this._height / (this._defaultHeight * scale)) / 2
} else {
transX = this.transX - (zoomStep - 1) / scale * anchorX
transY = this.transY - (zoomStep - 1) / scale * anchorY
}
}
if (animate && count > 0) {
scaleStart = this.scale
scaleDiff = (scale - scaleStart) / count
transXStart = this.transX * this.scale
transYStart = this.transY * this.scale
transXDiff = (transX * scale - transXStart) / count
transYDiff = (transY * scale - transYStart) / count
interval = setInterval(() => {
i += 1
this.scale = scaleStart + scaleDiff * i
this.transX = (transXStart + transXDiff * i) / this.scale
this.transY = (transYStart + transYDiff * i) / this.scale
this._applyTransform()
if (i == count) {
clearInterval(interval)
this._emit(Events.onViewportChange, [
this.scale, this.transX, this.transY
])
}
}, 10)
} else {
this.transX = transX
this.transY = transY
this.scale = scale
this._applyTransform()
this._emit(Events.onViewportChange, [
this.scale, this.transX, this.transY
])
}
}

View file

@ -0,0 +1,50 @@
import EventHandler from '../eventHandler'
export default function setupContainerEvents() {
const map = this
let mouseDown = false
let oldPageX
let oldPageY
if (this.params.draggable) {
EventHandler.on(this.container, 'mousemove', (e) => {
if (!mouseDown) {
return false
}
map.transX -= (oldPageX - e.pageX) / map.scale
map.transY -= (oldPageY - e.pageY) / map.scale
map._applyTransform()
oldPageX = e.pageX
oldPageY = e.pageY
})
EventHandler.on(this.container, 'mousedown', (e) => {
mouseDown = true
oldPageX = e.pageX
oldPageY = e.pageY
return false
})
EventHandler.on(document.body, 'mouseup', () => {
mouseDown = false
})
}
if (this.params.zoomOnScroll) {
EventHandler.on(this.container, 'wheel', event => {
const deltaY = (((event.deltaY || -event.wheelDelta || event.detail) >> 10) || 1) * 75
const rect = this.container.getBoundingClientRect()
const offsetX = event.pageX - rect.left - window.scrollX
const offsetY = event.pageY - rect.top - window.scrollY
const zoomStep = Math.pow(1 + (map.params.zoomOnScrollSpeed / 1000), -1.5 * deltaY)
if (map.tooltip) {
map._tooltip.hide()
}
map._setScale(map.scale * zoomStep, offsetX, offsetY)
event.preventDefault()
})
}
}

View file

@ -0,0 +1,85 @@
import EventHandler from '../eventHandler'
export default function setupContainerTouchEvents() {
let map = this,
touchStartScale,
touchStartDistance,
touchX,
touchY,
centerTouchX,
centerTouchY,
lastTouchesLength
let handleTouchEvent = e => {
const touches = e.touches
let offset, scale, transXOld, transYOld
if (e.type == 'touchstart') {
lastTouchesLength = 0
}
if (touches.length == 1) {
if (lastTouchesLength == 1) {
transXOld = map.transX
transYOld = map.transY
map.transX -= (touchX - touches[0].pageX) / map.scale
map.transY -= (touchY - touches[0].pageY) / map.scale
map._tooltip?.hide()
map._applyTransform()
if (transXOld != map.transX || transYOld != map.transY) {
e.preventDefault()
}
}
touchX = touches[0].pageX
touchY = touches[0].pageY
} else if (touches.length == 2) {
if (lastTouchesLength == 2) {
scale = Math.sqrt(
Math.pow(touches[0].pageX - touches[1].pageX, 2) +
Math.pow(touches[0].pageY - touches[1].pageY, 2)
) / touchStartDistance
map._setScale(touchStartScale * scale, centerTouchX, centerTouchY)
map._tooltip?.hide()
e.preventDefault()
} else {
let rect = map.container.getBoundingClientRect()
offset = {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
}
if (touches[0].pageX > touches[1].pageX) {
centerTouchX = touches[1].pageX + (touches[0].pageX - touches[1].pageX) / 2
} else {
centerTouchX = touches[0].pageX + (touches[1].pageX - touches[0].pageX) / 2
}
if (touches[0].pageY > touches[1].pageY) {
centerTouchY = touches[1].pageY + (touches[0].pageY - touches[1].pageY) / 2
} else {
centerTouchY = touches[0].pageY + (touches[1].pageY - touches[0].pageY) / 2
}
centerTouchX -= offset.left
centerTouchY -= offset.top
touchStartScale = map.scale
touchStartDistance = Math.sqrt(
Math.pow(touches[0].pageX - touches[1].pageX, 2) +
Math.pow(touches[0].pageY - touches[1].pageY, 2)
)
}
}
lastTouchesLength = touches.length
}
EventHandler.on(map.container, 'touchstart', handleTouchEvent)
EventHandler.on(map.container, 'touchmove', handleTouchEvent)
}

View file

@ -0,0 +1,112 @@
import { getElement } from '../util'
import EventHandler from '../eventHandler'
import Events from '../defaults/events'
const parseEvent = (map, selector, isTooltip) => {
const element = getElement(selector)
const type = element.getAttribute('class').indexOf('jvm-region') === -1 ? 'marker' : 'region'
const isRegion = type === 'region'
const code = isRegion ? element.getAttribute('data-code') : element.getAttribute('data-index')
let event = isRegion ? Events.onRegionSelected : Events.onMarkerSelected
// Init tooltip event
if (isTooltip) {
event = isRegion ? Events.onRegionTooltipShow : Events.onMarkerTooltipShow
}
return {
type,
code,
event,
element: isRegion ? map.regions[code].element : map._markers[code].element,
tooltipText: isRegion ? map._mapData.paths[code].name || '' : (map._markers[code].config.name || '')
}
}
export default function setupElementEvents() {
const map = this
const container = this.container
let pageX, pageY, mouseMoved
EventHandler.on(container, 'mousemove', (event) => {
if (Math.abs(pageX - event.pageX) + Math.abs(pageY - event.pageY) > 2) {
mouseMoved = true
}
})
// When the mouse is pressed
EventHandler.delegate(container, 'mousedown', '.jvm-element', (event) => {
pageX = event.pageX
pageY = event.pageY
mouseMoved = false
})
// When the mouse is over the region/marker | When the mouse is out the region/marker
EventHandler.delegate(container, 'mouseover mouseout', '.jvm-element', function (event) {
const data = parseEvent(map, this, true)
const { showTooltip } = map.params
if (event.type === 'mouseover') {
data.element.hover(true)
if (showTooltip) {
map._tooltip.text(data.tooltipText)
map._emit(data.event, [event, map._tooltip, data.code])
if (!event.defaultPrevented) {
map._tooltip.show()
}
}
} else {
data.element.hover(false)
if (showTooltip) {
map._tooltip.hide()
}
}
})
// When the click is released
EventHandler.delegate(container, 'mouseup', '.jvm-element', function (event) {
const data = parseEvent(map, this)
if (mouseMoved) {
return
}
if (
(data.type === 'region' && map.params.regionsSelectable) ||
(data.type === 'marker' && map.params.markersSelectable)
) {
const element = data.element
// We're checking if regions/markers|SelectableOne option is presented
if (map.params[`${data.type}sSelectableOne`]) {
data.type === 'region' ? map.clearSelectedRegions() : map.clearSelectedMarkers()
}
if (data.element.isSelected) {
element.select(false)
} else {
element.select(true)
}
map._emit(data.event, [
data.code,
element.isSelected,
data.type === 'region'
? map.getSelectedRegions()
: map.getSelectedMarkers()
])
}
})
// When region/marker is clicked
EventHandler.delegate(container, 'click', '.jvm-element', function (event) {
const { type, code } = parseEvent(map, this)
map._emit(
type === 'region' ? Events.onRegionClick : Events.onMarkerClick,
[event, code]
)
})
}

View file

@ -0,0 +1,40 @@
import { createElement } from '../util'
import EventHandler from '../eventHandler'
export default function setupZoomButtons() {
const zoomInOption = this.params.zoomInButton
const zoomOutOption = this.params.zoomOutButton
const getZoomButton = (zoomOption) => typeof zoomOption === 'string'
? document.querySelector(zoomOption)
: zoomOption
const zoomIn = zoomInOption
? getZoomButton(zoomInOption)
: createElement('div', 'jvm-zoom-btn jvm-zoomin', '&#43;', true)
const zoomOut = zoomOutOption
? getZoomButton(zoomOutOption)
: createElement('div', 'jvm-zoom-btn jvm-zoomout', '&#x2212', true)
if (!zoomInOption) {
this.container.appendChild(zoomIn)
}
if (!zoomOutOption) {
this.container.appendChild(zoomOut)
}
const handler = (zoomin = true) => {
return () => this._setScale(
zoomin ? this.scale * this.params.zoomStep : this.scale / this.params.zoomStep,
this._width / 2,
this._height / 2,
false,
this.params.zoomAnimate
)
}
EventHandler.on(zoomIn, 'click', handler())
EventHandler.on(zoomOut, 'click', handler(false))
}

View file

@ -0,0 +1,7 @@
export default function updateSize() {
this._width = this.container.offsetWidth
this._height = this.container.offsetHeight
this._resize()
this.canvas.setSize(this._width, this._height)
this._applyTransform()
}

View file

@ -0,0 +1,87 @@
class DataVisualization {
constructor({ scale, values }, map) {
this._scale = scale
this._values = values
this._fromColor = this.hexToRgb(scale[0])
this._toColor = this.hexToRgb(scale[1])
this._map = map
this.setMinMaxValues(values)
this.visualize()
}
setMinMaxValues(values) {
this.min = Number.MAX_VALUE
this.max = 0
for (let value in values) {
value = parseFloat(values[value])
if (value > this.max) {
this.max = value
}
if (value < this.min) {
this.min = value
}
}
}
visualize() {
let attrs = {}, value
for (let regionCode in this._values) {
value = parseFloat(this._values[regionCode])
if (!isNaN(value)) {
attrs[regionCode] = this.getValue(value)
}
}
this.setAttributes(attrs)
}
setAttributes(attrs) {
for (let code in attrs) {
if (this._map.regions[code]) {
this._map.regions[code].element.setStyle('fill', attrs[code])
}
}
}
getValue(value) {
if (this.min === this.max) {
return `#${this._toColor.join('')}`
}
let hex, color = '#'
for (var i = 0; i < 3; i++) {
hex = Math.round(
this._fromColor[i] + (this._toColor[i] - this._fromColor[i]) * ((value - this.min) / (this.max - this.min))
).toString(16)
color += (hex.length === 1 ? '0' : '') + hex
}
return color
}
hexToRgb(h) {
let r = 0, g = 0, b = 0
if (h.length == 4) {
r = '0x' + h[1] + h[1]
g = '0x' + h[2] + h[2]
b = '0x' + h[3] + h[3]
} else if (h.length == 7) {
r = '0x' + h[1] + h[2]
g = '0x' + h[3] + h[4]
b = '0x' + h[5] + h[6]
}
return [parseInt(r), parseInt(g), parseInt(b)]
}
}
export default DataVisualization

View file

@ -0,0 +1,11 @@
export default {
onLoaded: 'map:loaded',
onViewportChange: 'viewport:changed',
onRegionClick: 'region:clicked',
onMarkerClick: 'marker:clicked',
onRegionSelected: 'region:selected',
onMarkerSelected: 'marker:selected',
onRegionTooltipShow: 'region.tooltip:show',
onMarkerTooltipShow: 'marker.tooltip:show',
onDestroyed: 'map:destroyed'
}

View file

@ -0,0 +1,90 @@
export default {
map: 'world',
backgroundColor: 'transparent',
draggable: true,
zoomButtons: true,
zoomOnScroll: true,
zoomOnScrollSpeed: 3,
zoomMax: 12,
zoomMin: 1,
zoomAnimate: true,
showTooltip: true,
zoomStep: 1.5,
bindTouchEvents: true,
// Line options
lineStyle: {
curvature: 0,
stroke: '#808080',
strokeWidth: 1,
strokeLinecap: 'round',
},
// Marker options
markersSelectable: false,
markersSelectableOne: false,
markerStyle: {
initial: {
r: 7,
fill: '#374151',
fillOpacity: 1,
stroke: '#FFF',
strokeWidth: 5,
strokeOpacity: .5,
},
hover: {
fill: '#3cc0ff',
cursor: 'pointer',
},
selected: {
fill: 'blue'
},
selectedHover: {}
},
markerLabelStyle: {
initial: {
fontFamily: 'Verdana',
fontSize: 12,
fontWeight: 500,
cursor: 'default',
fill: '#374151'
},
hover: {
cursor: 'pointer'
},
selected: {},
selectedHover: {}
},
// Region options
regionsSelectable: false,
regionsSelectableOne: false,
regionStyle: {
initial: {
fill: '#dee2e8',
fillOpacity: 1,
stroke: 'none',
strokeWidth: 0,
},
hover: {
fillOpacity: .7,
cursor: 'pointer'
},
selected: {
fill: '#9ca3af'
},
selectedHover: {}
},
regionLabelStyle: {
initial: {
fontFamily: 'Verdana',
fontSize: '12',
fontWeight: 'bold',
cursor: 'default',
fill: '#35373e'
},
hover: {
cursor: 'pointer'
}
},
}

View file

@ -0,0 +1,47 @@
let eventRegistry = {}
let eventUid = 1
const EventHandler = {
on(element, event, handler, options = {}) {
const uid = `jvm:${event}::${eventUid++}`
eventRegistry[uid] = {
selector: element,
handler,
}
handler._uid = uid
element.addEventListener(event, handler, options)
},
delegate(element, event, selector, handler) {
event = event.split(' ')
event.forEach(eventName => {
EventHandler.on(element, eventName, (e) => {
const target = e.target
if (target.matches(selector)) {
handler.call(target, e)
}
})
})
},
off(element, event, handler) {
const eventType = event.split(':')[1]
element.removeEventListener(eventType, handler)
delete eventRegistry[handler._uid]
},
flush() {
Object.keys(eventRegistry).forEach(event => {
EventHandler.off(eventRegistry[event].selector, event, eventRegistry[event].handler)
})
},
getEventRegistry() {
return eventRegistry
},
}
export default EventHandler

View file

@ -0,0 +1,25 @@
/**
* jsVectorMap
* Copyrights (c) Mustafa Omar https://github.com/themustafaomar
* Released under the MIT License.
*/
import Map from './map'
import '../scss/jsvectormap.scss'
class jsVectorMap {
constructor(options = {}) {
if (!options.selector) {
throw new Error('Selector is not given.')
}
return new Map(options)
}
// Public
static addMap(name, map) {
Map.maps[name] = map
}
}
export default window.jsVectorMap = jsVectorMap

View file

@ -0,0 +1,69 @@
import { createElement, isImageUrl } from './util'
class Legend {
constructor(options = {}) {
this._options = options
this._map = this._options.map
this._series = this._options.series
this._body = createElement('div', 'jvm-legend')
if (this._options.cssClass) {
this._body.setAttribute('class', this._options.cssClass)
}
if (options.vertical) {
this._map.legendVertical.appendChild(this._body)
} else {
this._map.legendHorizontal.appendChild(this._body)
}
this.render()
}
render() {
let ticks = this._series.scale.getTicks()
this._body.innderHTML = ''
if (this._options.title) {
let legendTitle = createElement('div', 'jvm-legend-title', this._options.title)
this._body.appendChild(legendTitle)
}
for (let i = 0; i < ticks.length; i++) {
let tick = createElement('div', 'jvm-legend-tick',)
let sample = createElement('div', 'jvm-legend-tick-sample')
switch (this._series.config.attribute) {
case 'fill':
if (isImageUrl(ticks[i].value)) {
sample.style.background = `url(${ticks[i].value})`
} else {
sample.style.background = ticks[i].value
}
break
case 'stroke':
sample.style.background = ticks[i].value
break
case 'image':
sample.style.background = `url(${typeof ticks[i].value === 'object' ? ticks[i].value.url : ticks[i].value}) no-repeat center center`
sample.style.backgroundSize = 'cover'
break
}
tick.appendChild(sample)
let label = ticks[i].label
if (this._options.labelRender) {
label = this._options.labelRender(label)
}
const tickText = createElement('div', 'jvm-legend-tick-text', label)
tick.appendChild(tickText)
this._body.appendChild(tick)
}
}
}
export default Legend

View file

@ -0,0 +1,367 @@
import {
merge,
getLineUid,
getElement,
createElement,
removeElement,
} from './util'
import core from './core'
import Defaults from './defaults/options'
import SVGCanvasElement from './svg/canvasElement'
import Events from './defaults/events'
import EventHandler from './eventHandler'
import Tooltip from './components/tooltip'
import DataVisualization from './dataVisualization'
const JVM_PREFIX = 'jvm-'
const CONTAINER_CLASS = `${JVM_PREFIX}container`
const MARKERS_GROUP_ID = `${JVM_PREFIX}markers-group`
const MARKERS_LABELS_GROUP_ID = `${JVM_PREFIX}markers-labels-group`
const LINES_GROUP_ID = `${JVM_PREFIX}lines-group`
const SERIES_CONTAINER_CLASS = `${JVM_PREFIX}series-container`
const SERIES_CONTAINER_H_CLASS = `${SERIES_CONTAINER_CLASS} ${JVM_PREFIX}series-h`
const SERIES_CONTAINER_V_CLASS = `${SERIES_CONTAINER_CLASS} ${JVM_PREFIX}series-v`
class Map {
static maps = {}
static defaults = Defaults
constructor(options = {}) {
// Merge the given options with the default options
this.params = merge(Map.defaults, options, true)
// Throw an error if the given map name doesn't match
// the map that was set in map file
if (!Map.maps[this.params.map]) {
throw new Error(`Attempt to use map which was not loaded: ${options.map}`)
}
this.regions = {}
this.scale = 1
this.transX = 0
this.transY = 0
this._mapData = Map.maps[this.params.map]
this._markers = {}
this._lines = {}
this._defaultWidth = this._mapData.width
this._defaultHeight = this._mapData.height
this._height = 0
this._width = 0
this._baseScale = 1
this._baseTransX = 0
this._baseTransY = 0
// `document` is already ready, just initialise now
if (document.readyState !== 'loading') {
this._init()
} else {
// Wait until `document` is ready
window.addEventListener('DOMContentLoaded', () => this._init())
}
}
_init() {
const options = this.params
this.container = getElement(options.selector)
this.container.classList.add(CONTAINER_CLASS)
// The map canvas element
this.canvas = new SVGCanvasElement(this.container)
// Set the map's background color
this.setBackgroundColor(options.backgroundColor)
// Create regions
this._createRegions()
// Update size
this.updateSize()
// Lines group must be created before markers
// Otherwise the lines will be drawn on top of the markers.
if (options.lines) {
this._linesGroup = this.canvas.createGroup(LINES_GROUP_ID)
}
if (options.markers) {
this._markersGroup = this.canvas.createGroup(MARKERS_GROUP_ID)
this._markerLabelsGroup = this.canvas.createGroup(MARKERS_LABELS_GROUP_ID)
}
// Create markers
this._createMarkers(options.markers)
// Create lines
this._createLines(options.lines || {})
// Position labels
this._repositionLabels()
// Setup the container events
this._setupContainerEvents()
// Setup regions/markers events
this._setupElementEvents()
// Create zoom buttons if `zoomButtons` is presented
if (options.zoomButtons) {
this._setupZoomButtons()
}
// Create toolip
if (options.showTooltip) {
this._tooltip = new Tooltip(this)
}
// Set selected regions if any
if (options.selectedRegions) {
this._setSelected('regions', options.selectedRegions)
}
// Set selected regions if any
if (options.selectedMarkers) {
this._setSelected('_markers', options.selectedMarkers)
}
// Set focus on a spcific region
if (options.focusOn) {
this.setFocus(options.focusOn)
}
// Data visualization
if (options.visualizeData) {
this.dataVisualization = new DataVisualization(options.visualizeData, this)
}
// Bind touch events if true
if (options.bindTouchEvents) {
if (
('ontouchstart' in window) || (window.DocumentTouch && document instanceof DocumentTouch)
) {
this._setupContainerTouchEvents()
}
}
// Create series if any
if (options.series) {
this.container.appendChild(this.legendHorizontal = createElement(
'div', SERIES_CONTAINER_H_CLASS
))
this.container.appendChild(this.legendVertical = createElement(
'div', SERIES_CONTAINER_V_CLASS
))
this._createSeries()
}
// Fire loaded event
this._emit(Events.onLoaded, [this])
}
// Public
setBackgroundColor(color) {
this.container.style.backgroundColor = color
}
// Regions
getSelectedRegions() {
return this._getSelected('regions')
}
clearSelectedRegions(regions = undefined) {
regions = this._normalizeRegions(regions) || this._getSelected('regions')
regions.forEach((key) => {
this.regions[key].element.select(false)
})
}
setSelectedRegions(regions) {
this.clearSelectedRegions()
this._setSelected('regions', this._normalizeRegions(regions))
}
// Markers
getSelectedMarkers() {
return this._getSelected('_markers')
}
clearSelectedMarkers() {
this._clearSelected('_markers')
}
setSelectedMarkers(markers) {
this._setSelected('_markers', markers)
}
addMarkers(config) {
config = Array.isArray(config) ? config : [config]
this._createMarkers(config, true)
}
removeMarkers(markers) {
if (!markers) {
markers = Object.keys(this._markers)
}
markers.forEach(index => {
// Remove the element from the DOM
this._markers[index].element.remove()
// Remove the element from markers object
delete this._markers[index]
})
}
// Lines
addLine(from, to, style = {}) {
console.warn('`addLine` method is deprecated, please use `addLines` instead.')
this._createLines([{ from, to, style }], this._markers, true)
}
addLines(config) {
const uids = this._getLinesAsUids()
if (!Array.isArray(config)) {
config = [config]
}
this._createLines(config.filter(line => {
return !(uids.indexOf(getLineUid(line.from, line.to)) > -1)
}), true)
}
removeLines(lines) {
if (Array.isArray(lines)) {
lines = lines.map(line => getLineUid(line.from, line.to))
} else {
lines = this._getLinesAsUids()
}
lines.forEach(uid => {
this._lines[uid].dispose()
delete this._lines[uid]
})
}
removeLine(from, to) {
console.warn('`removeLine` method is deprecated, please use `removeLines` instead.')
const uid = getLineUid(from, to)
if (this._lines.hasOwnProperty(uid)) {
this._lines[uid].element.remove()
delete this._lines[uid]
}
}
// Reset map
reset() {
for (let key in this.series) {
for (let i = 0; i < this.series[key].length; i++) {
this.series[key][i].clear()
}
}
if (this.legendHorizontal) {
removeElement(this.legendHorizontal)
this.legendHorizontal = null
}
if (this.legendVertical) {
removeElement(this.legendVertical)
this.legendVertical = null
}
this.scale = this._baseScale
this.transX = this._baseTransX
this.transY = this._baseTransY
this._applyTransform()
this.clearSelectedMarkers()
this.clearSelectedRegions()
this.removeMarkers()
}
// Destroy the map
destroy(destroyInstance = true) {
// Remove event registry
EventHandler.flush()
// Remove tooltip from DOM and memory
this._tooltip.dispose()
// Fire destroyed event
this._emit(Events.onDestroyed)
// Remove references
if (destroyInstance) {
Object.keys(this).forEach(key => {
try {
delete this[key]
} catch (e) {}
})
}
}
extend(name, callback) {
if (typeof this[name] === 'function') {
throw new Error(`The method [${name}] does already exist, please use another name.`)
}
Map.prototype[name] = callback
}
// Private
_emit(eventName, args) {
for (const event in Events) {
if (Events[event] === eventName && typeof this.params[event] === 'function') {
this.params[event].apply(this, args)
}
}
}
// Get selected markers/regions
_getSelected(type) {
const selected = []
for (const key in this[type]) {
if (this[type][key].element.isSelected) {
selected.push(key)
}
}
return selected
}
_setSelected(type, keys) {
keys.forEach(key => {
if (this[type][key]) {
this[type][key].element.select(true)
}
})
}
_clearSelected(type) {
this._getSelected(type).forEach(key => {
this[type][key].element.select(false)
})
}
_getLinesAsUids() {
return Object.keys(this._lines)
}
_normalizeRegions(regions) {
return typeof regions === 'string' ? [regions] : regions
}
}
Object.assign(Map.prototype, core)
export default Map

View file

@ -0,0 +1,127 @@
/**
* ------------------------------------------------------------------------
* Object
* ------------------------------------------------------------------------
*/
const Proj = {
/* sgn(n){
if (n > 0) {
return 1;
} else if (n < 0) {
return -1;
} else {
return n;
}
}, */
mill(lat, lng, c) {
return {
x: this.radius * (lng - c) * this.radDeg,
y: - this.radius * Math.log(Math.tan((45 + 0.4 * lat) * this.radDeg)) / 0.8
};
},
/* mill_inv(x, y, c) {
return {
lat: (2.5 * Math.atan(Math.exp(0.8 * y / this.radius)) - 5 * Math.PI / 8) * this.degRad,
lng: (c * this.radDeg + x / this.radius) * this.degRad
};
}, */
merc(lat, lng, c) {
return {
x: this.radius * (lng - c) * this.radDeg,
y: - this.radius * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360))
};
},
/* merc_inv(x, y, c) {
return {
lat: (2 * Math.atan(Math.exp(y / this.radius)) - Math.PI / 2) * this.degRad,
lng: (c * this.radDeg + x / this.radius) * this.degRad
};
}, */
aea(lat, lng, c) {
var fi0 = 0,
lambda0 = c * this.radDeg,
fi1 = 29.5 * this.radDeg,
fi2 = 45.5 * this.radDeg,
fi = lat * this.radDeg,
lambda = lng * this.radDeg,
n = (Math.sin(fi1)+Math.sin(fi2)) / 2,
C = Math.cos(fi1)*Math.cos(fi1)+2*n*Math.sin(fi1),
theta = n*(lambda-lambda0),
ro = Math.sqrt(C-2*n*Math.sin(fi))/n,
ro0 = Math.sqrt(C-2*n*Math.sin(fi0))/n;
return {
x: ro * Math.sin(theta) * this.radius,
y: - (ro0 - ro * Math.cos(theta)) * this.radius
};
},
/* aea_inv(xCoord, yCoord, c) {
var x = xCoord / this.radius,
y = yCoord / this.radius,
fi0 = 0,
lambda0 = c * this.radDeg,
fi1 = 29.5 * this.radDeg,
fi2 = 45.5 * this.radDeg,
n = (Math.sin(fi1)+Math.sin(fi2)) / 2,
C = Math.cos(fi1)*Math.cos(fi1)+2*n*Math.sin(fi1),
ro0 = Math.sqrt(C-2*n*Math.sin(fi0))/n,
ro = Math.sqrt(x*x+(ro0-y)*(ro0-y)),
theta = Math.atan( x / (ro0 - y) );
return {
lat: (Math.asin((C - ro * ro * n * n) / (2 * n))) * this.degRad,
lng: (lambda0 + theta / n) * this.degRad
};
}, */
lcc(lat, lng, c) {
var fi0 = 0,
lambda0 = c * this.radDeg,
lambda = lng * this.radDeg,
fi1 = 33 * this.radDeg,
fi2 = 45 * this.radDeg,
fi = lat * this.radDeg,
n = Math.log(Math.cos(fi1) * (1 / Math.cos(fi2)) ) / Math.log(Math.tan(Math.PI / 4 + fi2 / 2) * (1 / Math.tan(Math.PI / 4 + fi1 / 2) )),
F = (Math.cos(fi1) * Math.pow(Math.tan(Math.PI / 4 + fi1 / 2 ), n )) / n,
ro = F * Math.pow(1 / Math.tan(Math.PI / 4 + fi / 2), n),
ro0 = F * Math.pow(1 / Math.tan(Math.PI / 4 + fi0 / 2), n);
return {
x: ro * Math.sin(n * (lambda - lambda0)) * this.radius,
y: - (ro0 - ro * Math.cos(n * (lambda - lambda0))) * this.radius
};
},
/* lcc_inv(xCoord, yCoord, c) {
var x = xCoord / this.radius,
y = yCoord / this.radius,
fi0 = 0,
lambda0 = c * this.radDeg,
fi1 = 33 * this.radDeg,
fi2 = 45 * this.radDeg,
n = Math.log( Math.cos(fi1) * (1 / Math.cos(fi2)) ) / Math.log( Math.tan( Math.PI / 4 + fi2 / 2) * (1 / Math.tan( Math.PI / 4 + fi1 / 2) ) ),
F = ( Math.cos(fi1) * Math.pow( Math.tan( Math.PI / 4 + fi1 / 2 ), n ) ) / n,
ro0 = F * Math.pow( 1 / Math.tan( Math.PI / 4 + fi0 / 2 ), n ),
ro = this.sgn(n) * Math.sqrt(x*x+(ro0-y)*(ro0-y)),
theta = Math.atan( x / (ro0 - y) );
return {
lat: (2 * Math.atan(Math.pow(F/ro, 1/n)) - Math.PI / 2) * this.degRad,
lng: (lambda0 + theta / n) * this.degRad
};
} */
}
Proj.degRad = 180 / Math.PI
Proj.radDeg = Math.PI / 180
Proj.radius = 6381372
export default Proj

View file

@ -0,0 +1,21 @@
class OrdinalScale {
constructor(scale) {
this._scale = scale
}
getValue(value){
return this._scale[value]
}
getTicks() {
const ticks = []
for (let key in this._scale) {
ticks.push({ label: key, value: this._scale[key] })
}
return ticks
}
}
export default OrdinalScale

View file

@ -0,0 +1,68 @@
import { merge } from './util/index'
import Legend from './legend'
import OrdinalScale from './scales/ordinalScale'
class Series {
constructor(config = {}, elements, map) {
// Private
this._map = map
this._elements = elements // Could be markers or regions
this._values = config.values || {}
// Protected
this.config = config
this.config.attribute = config.attribute || 'fill'
// Set initial attributes
if (config.attributes) {
this.setAttributes(config.attributes)
}
if (typeof config.scale === 'object') {
this.scale = new OrdinalScale(config.scale)
}
if (this.config.legend) {
this.legend = new Legend(
merge({ map: this._map, series: this }, this.config.legend)
)
}
this.setValues(this._values)
}
setValues(values) {
let attrs = {}
for (let key in values) {
if (values[key]) {
attrs[key] = this.scale.getValue(values[key])
}
}
this.setAttributes(attrs)
}
setAttributes(attrs) {
for (let code in attrs) {
if (this._elements[code]) {
this._elements[code].element.setStyle(this.config.attribute, attrs[code])
}
}
}
clear() {
let key, attrs = {}
for (key in this._values) {
if (this._elements[key]) {
attrs[key] = this._elements[key].element.shape.style.initial[this.config.attribute]
}
}
this.setAttributes(attrs)
this._values = {}
}
}
export default Series

View file

@ -0,0 +1,53 @@
import {
hyphenate,
removeElement
} from '../util/index'
class SVGElement {
constructor(name, config) {
this.node = this._createElement(name)
if (config) {
this.set(config)
}
}
// Create new SVG element `svg`, `g`, `path`, `line`, `circle`, `image`, etc.
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#important_namespace_uris
_createElement(tagName) {
return document.createElementNS('http://www.w3.org/2000/svg', tagName)
}
addClass(className) {
this.node.setAttribute('class', className)
}
getBBox() {
return this.node.getBBox()
}
// Apply attributes on the current node element
set(property, value) {
if (typeof property === 'object') {
for (let attr in property) {
this.applyAttr(attr, property[attr])
}
} else {
this.applyAttr(property, value)
}
}
get(property) {
return this.style.initial[property]
}
applyAttr(property, value) {
this.node.setAttribute(hyphenate(property), value)
}
remove() {
removeElement(this.node)
}
}
export default SVGElement

View file

@ -0,0 +1,99 @@
import SVGElement from './baseElement'
import SVGShapeElement from './shapeElement'
import SVGTextElement from './textElement'
import SVGImageElement from './imageElement'
class SVGCanvasElement extends SVGElement {
constructor(container) {
super('svg') // Create svg element for holding the whole map
this._container = container
// Create the defs element
this._defsElement = new SVGElement('defs')
// Create group element which will hold the paths (regions)
this._rootElement = new SVGElement('g', { id: 'jvm-regions-group' })
// Append the defs element to the this.node (SVG tag)
this.node.appendChild(this._defsElement.node)
// Append the group to this.node (SVG tag)
this.node.appendChild(this._rootElement.node)
// Append this.node (SVG tag) to the container
this._container.appendChild(this.node)
}
setSize(width, height) {
this.node.setAttribute('width', width)
this.node.setAttribute('height', height)
}
applyTransformParams(scale, transX, transY) {
this._rootElement.node.setAttribute('transform', `scale(${scale}) translate(${transX}, ${transY})`)
}
// Create `path` element
createPath(config, style, group) {
const path = new SVGShapeElement('path', config, style)
path.node.setAttribute('fill-rule', 'evenodd')
return this._add(path, group)
}
// Create `circle` element
createCircle(config, style, group) {
const circle = new SVGShapeElement('circle', config, style)
return this._add(circle, group)
}
// Create `line` element
createLine(config, style, group) {
const line = new SVGShapeElement('line', config, style)
return this._add(line, group)
}
// Create `text` element
createText(config, style, group) {
const text = new SVGTextElement(config, style)
return this._add(text, group)
}
// Create `image` element
createImage(config, style, group) {
const image = new SVGImageElement(config, style)
return this._add(image, group)
}
// Create `g` element
createGroup(id) {
const group = new SVGElement('g')
this.node.appendChild(group.node)
if (id) {
group.node.id = id
}
group.canvas = this
return group
}
// Add some element to a spcific group or the root element if the group isn't given
_add(element, group) {
group = group || this._rootElement
group.node.appendChild(element.node)
return element
}
}
export default SVGCanvasElement

View file

@ -0,0 +1,49 @@
import SVGShapeElement from './shapeElement'
class SVGImageElement extends SVGShapeElement {
constructor(config, style) {
super('image', config, style)
}
applyAttr(attr, value) {
let imageUrl
if (attr === 'image') {
// This get executed when we have url in series.markers[0].scale.someScale.url
if (typeof value === 'object') {
imageUrl = value.url
this.offset = value.offset || [0, 0]
} else {
imageUrl = value
this.offset = [0, 0]
}
this.node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', imageUrl)
// Set width and height then call this `applyAttr` again
this.width = 23
this.height = 23
this.applyAttr('width', this.width)
this.applyAttr('height', this.height)
this.applyAttr('x', this.cx - this.width / 2 + this.offset[0])
this.applyAttr('y', this.cy - this.height / 2 + this.offset[1])
} else if (attr == 'cx') {
this.cx = value
if (this.width) {
this.applyAttr('x', value - this.width / 2 + this.offset[0])
}
} else if (attr == 'cy') {
this.cy = value
if (this.height) {
this.applyAttr('y', value - this.height / 2 + this.offset[1])
}
} else {
// This time Call SVGElement
super.applyAttr.apply(this, arguments)
}
}
}
export default SVGImageElement

View file

@ -0,0 +1,48 @@
import { merge } from '../util/index'
import SVGElement from './baseElement'
class SVGShapeElement extends SVGElement {
constructor(name, config, style = {}) {
super(name, config)
this.isHovered = false
this.isSelected = false
this.style = style
this.style.current = {}
this.updateStyle()
}
setStyle(property, value) {
if (typeof property === 'object') {
merge(this.style.current, property)
} else {
merge(this.style.current, { [property]: value })
}
this.updateStyle()
}
updateStyle() {
const attrs = {}
merge(attrs, this.style.initial)
merge(attrs, this.style.current)
if (this.isHovered) {
merge(attrs, this.style.hover)
}
if (this.isSelected) {
merge(attrs, this.style.selected)
if (this.isHovered) {
merge(attrs, this.style.selectedHover)
}
}
this.set(attrs)
}
}
export default SVGShapeElement

View file

@ -0,0 +1,13 @@
import SVGShapeElement from './shapeElement'
class SVGTextElement extends SVGShapeElement {
constructor(config, style) {
super('text', config, style)
}
applyAttr(attr, value) {
attr === 'text' ? this.node.textContent = value : super.applyAttr(attr, value)
}
}
export default SVGTextElement

View file

@ -0,0 +1,129 @@
/**
* By https://github.com/TehShrike/deepmerge
*/
'use strict'
var isMergeableObject = function isMergeableObject(value) {
return isNonNullObject(value)
&& !isSpecial(value)
}
function isNonNullObject(value) {
return !!value && typeof value === 'object'
}
function isSpecial(value) {
var stringValue = Object.prototype.toString.call(value)
return stringValue === '[object RegExp]'
|| stringValue === '[object Date]'
|| isNode(value)
|| isReactElement(value)
}
// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
var canUseSymbol = typeof Symbol === 'function' && Symbol.for
var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7
function isReactElement(value) {
return value.$$typeof === REACT_ELEMENT_TYPE
}
function isNode(value) {
return value instanceof Node
}
function emptyTarget(val) {
return Array.isArray(val) ? [] : {}
}
function cloneUnlessOtherwiseSpecified(value, options) {
return (options.clone !== false && options.isMergeableObject(value))
? deepmerge(emptyTarget(value), value, options)
: value
}
function defaultArrayMerge(target, source, options) {
return target.concat(source).map(function(element) {
return cloneUnlessOtherwiseSpecified(element, options)
})
}
function getMergeFunction(key, options) {
if (!options.customMerge) {
return deepmerge
}
var customMerge = options.customMerge(key)
return typeof customMerge === 'function' ? customMerge : deepmerge
}
function getEnumerableOwnPropertySymbols(target) {
return Object.getOwnPropertySymbols
? Object.getOwnPropertySymbols(target).filter(function(symbol) {
return target.propertyIsEnumerable(symbol)
})
: []
}
function getKeys(target) {
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
}
function propertyIsOnObject(object, property) {
try {
return property in object
} catch(_) {
return false
}
}
// Protects from prototype poisoning and unexpected merging up the prototype chain.
function propertyIsUnsafe(target, key) {
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
}
function mergeObject(target, source, options) {
var destination = {}
if (options.isMergeableObject(target)) {
getKeys(target).forEach(function(key) {
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options)
})
}
getKeys(source).forEach(function(key) {
if (propertyIsUnsafe(target, key)) {
return
}
if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
destination[key] = getMergeFunction(key, options)(target[key], source[key], options)
} else {
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options)
}
})
return destination
}
var deepmerge = function (target, source, options) {
options = options || {}
options.arrayMerge = options.arrayMerge || defaultArrayMerge
options.isMergeableObject = options.isMergeableObject || isMergeableObject
// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
// implementations can use it. The caller may not replace it.
options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified
var sourceIsArray = Array.isArray(source)
var targetIsArray = Array.isArray(target)
var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray
if (!sourceAndTargetTypesMatch) {
return cloneUnlessOtherwiseSpecified(source, options)
} else if (sourceIsArray) {
return options.arrayMerge(target, source, options)
} else {
return mergeObject(target, source, options)
}
}
export default deepmerge

View file

@ -0,0 +1,81 @@
import DeepMerge from './deepMerge'
/**
* --------------------------------------------------------------------------
* Public Util Api
* --------------------------------------------------------------------------
*/
const getElement = selector => {
if (typeof selector === 'object' && typeof selector.nodeType !== 'undefined') {
return selector
}
if (typeof selector === 'string') {
return document.querySelector(selector)
}
return null
}
const createElement = (type, classes, content, html = false) => {
let el = document.createElement(type)
if (content) {
el[!html ? 'textContent' : 'innerHTML'] = content
}
if (classes) {
el.className = classes
}
return el
}
const findElement = (parentElement, selector) => {
return Element.prototype.querySelector.call(parentElement, selector)
}
const removeElement = target => {
target.parentNode.removeChild(target)
}
const isImageUrl = url => {
return /\.(jpg|gif|png)$/.test(url)
}
const hyphenate = string => {
return string.replace(/[\w]([A-Z])/g, m => `${m[0]}-${m[1]}`).toLowerCase()
}
const merge = (target, source, deep = false) => {
if (deep) {
return DeepMerge(target, source)
}
return Object.assign(target, source)
}
const keys = object => {
return Object.keys(object)
}
const getLineUid = (from, to) => {
return `${from.toLowerCase()}:to:${to.toLowerCase()}`
}
const inherit = (target, source) => {
Object.assign(target.prototype, source)
}
export {
getElement,
createElement,
findElement,
removeElement,
isImageUrl,
hyphenate,
merge,
keys,
getLineUid,
inherit,
}

View file

@ -0,0 +1,37 @@
$border-color: #E5E6E7 !default;
$box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05) !default;
// Tooltip
$tooltip-font-size: 0.9rem !default;
$tooltip-bg-color: #337FFA !default;
$tooltip-color: #FFF !default;
$tooltip-padding: 3px 5px !default;
$tooltip-shadow: 1px 2px 12px rgba(0, 0, 0, .2) !default;
$tooltip-radius: 3px !default;
// Zoom buttons
$zoom-btn-bg-color: #292929 !default;
$zoom-btn-color: #FFF !default;
$zoom-btn-size: 15px !default;
$zoom-btn-radius: 3px !default;
// Series
$series-container-right: 15px !default;
// Legends
$legend-bg-color: #FFF !default;
$legend-radius: .15rem !default;
$legend-margin-left: .75rem !default;
$legend-padding: .6rem !default;
// Legend title
$legend-title-padding-bottom: .5rem !default;
$legend-title-margin-bottom: .575rem !default;
// Legend ticks
$legend-tick-margin-top: .575rem !default;
$legend-tick-sample-radius: 0 !default;
$legend-tick-sample-height: 12px !default;
$legend-tick-sample-width: 30px !default;
$legend-tick-text-font-size: 12px !default;
$legend-tick-text-margin-top: 3px !default;

View file

@ -0,0 +1,153 @@
@import './variables';
:root {
--jvm-border-color: #{$border-color};
--jvm-box-shadow: #{$box-shadow};
// Tooltip
--jvm-tooltip-font-size: #{$tooltip-font-size};
--jvm-tooltip-bg-color: #{$tooltip-bg-color};
--jvm-tooltip-color: #{$tooltip-color};
--jvm-tooltip-padding: #{$tooltip-padding};
--jvm-tooltip-shadow: var(--jvm-box-shadow);
--jvm-tooltip-radius: #{$tooltip-radius};
// Zoom buttons
--jvm-zoom-btn-bg-color: #{$zoom-btn-bg-color};
--jvm-zoom-btn-color: #{$zoom-btn-color};
--jvm-zoom-btn-size: #{$zoom-btn-size};
--jvm-zoom-btn-radius: #{$zoom-btn-radius};
// Series
--jvm-series-container-right: #{$series-container-right};
// Legends
--jvm-legend-bg-color: #{$legend-bg-color};
--jvm-legend-radius: #{$legend-radius};
--jvm-legend-margin-left: #{$legend-margin-left};
--jvm-legend-padding: #{$legend-padding};
// Legend title
--jvm-legend-title-padding-bottom: #{$legend-title-padding-bottom};
--jvm-legend-title-margin-bottom: #{$legend-title-margin-bottom};
// Legend ticks
--jvm-legend-tick-margin-top: #{$legend-tick-margin-top};
--jvm-legend-tick-sample-radius: #{$legend-tick-sample-radius};
--jvm-legend-tick-sample-height: #{$legend-tick-sample-height};
--jvm-legend-tick-sample-width: #{$legend-tick-sample-width};
--jvm-legend-tick-text-font-size: #{$legend-tick-text-font-size};
--jvm-legend-tick-text-margin-top: #{$legend-tick-text-margin-top};
}
// Global resets
image, text, .jvm-zoom-btn { user-select: none }
// jsVectorMap container
.jvm-container {
position: relative;
height: 100%;
width: 100%;
}
// Tooltip
.jvm-tooltip {
border-radius: var(--jvm-tooltip-radius);
background-color: var(--jvm-tooltip-bg-color);
color: var(--jvm-tooltip-color);
font-size: var(--jvm-tooltip-font-size);
box-shadow: var(--jvm-tooltip-shadow);
padding: var(--jvm-tooltip-padding);
white-space: nowrap;
position: absolute;
display: none;
&.active {
display: block;
}
}
// Zoom buttons
.jvm-zoom-btn {
background-color: var(--jvm-zoom-btn-bg-color);
color: var(--jvm-zoom-btn-color);
border-radius: var(--jvm-zoom-btn-radius);
height: var(--jvm-zoom-btn-size);
width: var(--jvm-zoom-btn-size);
box-sizing: border-box;
position: absolute;
left: 10px;
line-height: var(--jvm-zoom-btn-size);
text-align: center;
cursor: pointer;
&.jvm-zoomin {
top: var(--jvm-zoom-btn-size)
}
&.jvm-zoomout {
top: calc(var(--jvm-zoom-btn-size) * 2 + (var(--jvm-zoom-btn-size) / 3));
}
}
// Series
.jvm-series-container {
position: absolute;
right: var(--jvm-series-container-right);
&.jvm-series-h { bottom: 15px }
&.jvm-series-v {
display: flex;
flex-direction: column;
gap: 0.75rem;
top: 15px;
}
}
// Legends
.jvm-legend {
background-color: var(--jvm-legend-bg-color);
border: 1px solid var(--jvm-border-color);
margin-left: var(--jvm-legend-margin-left);
border-radius: var(--jvm-legend-radius);
padding: var(--jvm-legend-padding);
box-shadow: var(--jvm-box-shadow);
}
.jvm-legend-title {
line-height: 1;
border-bottom: 1px solid var(--jvm-border-color);
padding-bottom: var(--jvm-legend-title-padding-bottom);
margin-bottom: var(--jvm-legend-title-margin-bottom);
text-align: left;
}
.jvm-legend-tick {
display: flex;
align-items: center;
min-width: 40px;
&:not(:first-child) {
margin-top: var(--jvm-legend-tick-margin-top);
}
}
.jvm-legend-tick-sample {
border-radius: var(--jvm-legend-tick-sample-radius);
margin-right: 0.45rem;
height: var(--jvm-legend-tick-sample-height);
width: var(--jvm-legend-tick-sample-width);
}
.jvm-legend-tick-text {
font-size: var(--jvm-legend-tick-text-font-size);
text-align: center;
line-height: 1;
}
// Line animation
.jvm-line[animation="true"] {
-webkit-animation: jvm-line-animation 10s linear forwards infinite;
animation: jvm-line-animation 10s linear forwards infinite;
@keyframes jvm-line-animation {
from { stroke-dashoffset: 250; }
}
}