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,21 @@
MIT License
Copyright (c) 2020 Mustafa Omar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,147 @@
:root {
--jvm-border-color: #E5E6E7;
--jvm-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--jvm-tooltip-font-size: 0.9rem;
--jvm-tooltip-bg-color: #337FFA;
--jvm-tooltip-color: #FFF;
--jvm-tooltip-padding: 3px 5px;
--jvm-tooltip-shadow: var(--jvm-box-shadow);
--jvm-tooltip-radius: 3px;
--jvm-zoom-btn-bg-color: #292929;
--jvm-zoom-btn-color: #FFF;
--jvm-zoom-btn-size: 15px;
--jvm-zoom-btn-radius: 3px;
--jvm-series-container-right: 15px;
--jvm-legend-bg-color: #FFF;
--jvm-legend-radius: 0.15rem;
--jvm-legend-margin-left: 0.75rem;
--jvm-legend-padding: 0.6rem;
--jvm-legend-title-padding-bottom: 0.5rem;
--jvm-legend-title-margin-bottom: 0.575rem;
--jvm-legend-tick-margin-top: 0.575rem;
--jvm-legend-tick-sample-radius: 0;
--jvm-legend-tick-sample-height: 12px;
--jvm-legend-tick-sample-width: 30px;
--jvm-legend-tick-text-font-size: 12px;
--jvm-legend-tick-text-margin-top: 3px;
}
image, text, .jvm-zoom-btn {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.jvm-container {
position: relative;
height: 100%;
width: 100%;
}
.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;
}
.jvm-tooltip.active {
display: block;
}
.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-zoom-btn.jvm-zoomin {
top: var(--jvm-zoom-btn-size);
}
.jvm-zoom-btn.jvm-zoomout {
top: calc(var(--jvm-zoom-btn-size) * 2 + var(--jvm-zoom-btn-size) / 3);
}
.jvm-series-container {
position: absolute;
right: var(--jvm-series-container-right);
}
.jvm-series-container.jvm-series-h {
bottom: 15px;
}
.jvm-series-container.jvm-series-v {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
gap: 0.75rem;
top: 15px;
}
.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: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
min-width: 40px;
}
.jvm-legend-tick: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;
}
.jvm-line[animation=true] {
-webkit-animation: jvm-line-animation 10s linear forwards infinite;
animation: jvm-line-animation 10s linear forwards infinite;
}
@-webkit-keyframes jvm-line-animation {
from {
stroke-dashoffset: 250;
}
}
@keyframes jvm-line-animation {
from {
stroke-dashoffset: 250;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
:root{--jvm-border-color: #E5E6E7;--jvm-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);--jvm-tooltip-font-size: 0.9rem;--jvm-tooltip-bg-color: #337FFA;--jvm-tooltip-color: #FFF;--jvm-tooltip-padding: 3px 5px;--jvm-tooltip-shadow: var(--jvm-box-shadow);--jvm-tooltip-radius: 3px;--jvm-zoom-btn-bg-color: #292929;--jvm-zoom-btn-color: #FFF;--jvm-zoom-btn-size: 15px;--jvm-zoom-btn-radius: 3px;--jvm-series-container-right: 15px;--jvm-legend-bg-color: #FFF;--jvm-legend-radius: 0.15rem;--jvm-legend-margin-left: 0.75rem;--jvm-legend-padding: 0.6rem;--jvm-legend-title-padding-bottom: 0.5rem;--jvm-legend-title-margin-bottom: 0.575rem;--jvm-legend-tick-margin-top: 0.575rem;--jvm-legend-tick-sample-radius: 0;--jvm-legend-tick-sample-height: 12px;--jvm-legend-tick-sample-width: 30px;--jvm-legend-tick-text-font-size: 12px;--jvm-legend-tick-text-margin-top: 3px}image,text,.jvm-zoom-btn{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.jvm-container{position:relative;height:100%;width:100%}.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}.jvm-tooltip.active{display:block}.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-zoom-btn.jvm-zoomin{top:var(--jvm-zoom-btn-size)}.jvm-zoom-btn.jvm-zoomout{top:calc(var(--jvm-zoom-btn-size)*2 + var(--jvm-zoom-btn-size)/3)}.jvm-series-container{position:absolute;right:var(--jvm-series-container-right)}.jvm-series-container.jvm-series-h{bottom:15px}.jvm-series-container.jvm-series-v{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:.75rem;top:15px}.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:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-width:40px}.jvm-legend-tick: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:.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}.jvm-line[animation=true]{-webkit-animation:jvm-line-animation 10s linear forwards infinite;animation:jvm-line-animation 10s linear forwards infinite}@-webkit-keyframes jvm-line-animation{from{stroke-dashoffset:250}}@keyframes jvm-line-animation{from{stroke-dashoffset:250}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,48 @@
{
"name": "jsvectormap",
"version": "1.7.0",
"description": "A lightweight Javascript library for creating interactive maps",
"main": "dist/jsvectormap.min.js",
"jsdelivr": "dist/jsvectormap.min.js",
"exports": {
".": {
"import": "./dist/jsvectormap.esm.js",
"default": "./dist/jsvectormap.min.js",
"require": "./dist/jsvectormap.cjs",
"style": "./dist/jsvectormap.css",
"sass": "./src/scss/jsvectormap.scss"
},
"./*": "./*"
},
"style": "dist/jsvectormap.min.css",
"sass": "src/scss/jsvectormap.scss",
"author": {
"name": "Mustafa Omar",
"email": "themustafaomar@gmail.com"
},
"scripts": {
"dev": "rollup --config --watch",
"build": "rollup --config ./build/package.js"
},
"keywords": [
"javascript",
"interactive",
"vector",
"map",
"svg"
],
"files": [
"dist",
"src",
"LICENSE"
],
"repository": {
"type": "git",
"url": "git+https://github.com/themustafaomar/jsvectormap.git"
},
"bugs": {
"url": "https://github.com/themustafaomar/jsvectormap/issues"
},
"homepage": "https://jvm-docs.vercel.app",
"license": "MIT"
}

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; }
}
}