Subtle hint appears in top-right corner (desktop only) when the detail panel is open. Fades in when panel shows, fades out on ESC. Hidden on mobile where the X button is more appropriate.
500 lines
19 KiB
JavaScript
500 lines
19 KiB
JavaScript
// assets/js/weather_map_hook.js
|
|
|
|
function isMobile() {
|
|
return window.innerWidth < 768
|
|
}
|
|
|
|
const COLOR_SCALES = {
|
|
refractivity_gradient: {
|
|
breakpoints: [
|
|
{ value: -500, r: 0, g: 255, b: 163 },
|
|
{ value: -300, r: 125, g: 255, b: 212 },
|
|
{ value: -200, r: 255, g: 229, b: 102 },
|
|
{ value: -100, r: 255, g: 144, b: 68 },
|
|
{ value: 0, r: 255, g: 79, b: 79 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(0)} N/km` : "N/A",
|
|
label: "Refractivity Gradient",
|
|
unit: "N/km"
|
|
},
|
|
bl_height: {
|
|
breakpoints: [
|
|
{ value: 0, r: 0, g: 255, b: 163 },
|
|
{ value: 500, r: 125, g: 255, b: 212 },
|
|
{ value: 1000, r: 255, g: 229, b: 102 },
|
|
{ value: 2000, r: 255, g: 144, b: 68 },
|
|
{ value: 3000, r: 255, g: 79, b: 79 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(0)} m` : "N/A",
|
|
label: "Boundary Layer Height",
|
|
unit: "m"
|
|
},
|
|
dewpoint_depression: {
|
|
breakpoints: [
|
|
{ value: 0, r: 0, g: 255, b: 163 },
|
|
{ value: 5, r: 125, g: 255, b: 212 },
|
|
{ value: 10, r: 255, g: 229, b: 102 },
|
|
{ value: 20, r: 255, g: 144, b: 68 },
|
|
{ value: 30, r: 255, g: 79, b: 79 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(1)} \u00b0C` : "N/A",
|
|
label: "Dewpoint Depression (T - Td)",
|
|
unit: "\u00b0C"
|
|
},
|
|
pwat: {
|
|
breakpoints: [
|
|
{ value: 0, r: 139, g: 90, b: 43 },
|
|
{ value: 15, r: 194, g: 165, b: 116 },
|
|
{ value: 30, r: 173, g: 216, b: 230 },
|
|
{ value: 45, r: 65, g: 105, b: 225 },
|
|
{ value: 60, r: 0, g: 0, b: 180 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(1)} mm` : "N/A",
|
|
label: "Precipitable Water",
|
|
unit: "mm"
|
|
},
|
|
temperature: {
|
|
breakpoints: [
|
|
{ value: -20, r: 0, g: 0, b: 200 },
|
|
{ value: 0, r: 100, g: 149, b: 237 },
|
|
{ value: 15, r: 255, g: 255, b: 150 },
|
|
{ value: 30, r: 255, g: 140, b: 0 },
|
|
{ value: 45, r: 200, g: 0, b: 0 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(1)} \u00b0C (${(v * 9/5 + 32).toFixed(0)} \u00b0F)` : "N/A",
|
|
label: "Surface Temperature",
|
|
unit: "\u00b0C"
|
|
},
|
|
ducting: {
|
|
breakpoints: null,
|
|
trueColor: { r: 0, g: 255, b: 163 },
|
|
format: v => v ? "Yes" : "No",
|
|
label: "Ducting Detected",
|
|
unit: ""
|
|
},
|
|
surface_rh: {
|
|
breakpoints: [
|
|
{ value: 0, r: 255, g: 144, b: 68 },
|
|
{ value: 30, r: 255, g: 229, b: 102 },
|
|
{ value: 60, r: 125, g: 255, b: 212 },
|
|
{ value: 80, r: 0, g: 255, b: 163 },
|
|
{ value: 100, r: 0, g: 200, b: 130 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(0)}%` : "N/A",
|
|
label: "Relative Humidity",
|
|
unit: "%"
|
|
},
|
|
surface_refractivity: {
|
|
breakpoints: [
|
|
{ value: 250, r: 255, g: 79, b: 79 },
|
|
{ value: 300, r: 255, g: 144, b: 68 },
|
|
{ value: 330, r: 255, g: 229, b: 102 },
|
|
{ value: 360, r: 125, g: 255, b: 212 },
|
|
{ value: 400, r: 0, g: 255, b: 163 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(1)} N` : "N/A",
|
|
label: "Surface Refractivity",
|
|
unit: "N"
|
|
},
|
|
temp_850mb: {
|
|
breakpoints: [
|
|
{ value: -20, r: 0, g: 0, b: 200 },
|
|
{ value: -5, r: 100, g: 149, b: 237 },
|
|
{ value: 5, r: 255, g: 255, b: 150 },
|
|
{ value: 15, r: 255, g: 140, b: 0 },
|
|
{ value: 30, r: 200, g: 0, b: 0 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(1)} °C` : "N/A",
|
|
label: "Temperature at 850mb",
|
|
unit: "°C"
|
|
},
|
|
dewpoint_850mb: {
|
|
breakpoints: [
|
|
{ value: -20, r: 139, g: 90, b: 43 },
|
|
{ value: -5, r: 194, g: 165, b: 116 },
|
|
{ value: 5, r: 173, g: 216, b: 230 },
|
|
{ value: 15, r: 65, g: 105, b: 225 },
|
|
{ value: 25, r: 0, g: 0, b: 180 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(1)} °C` : "N/A",
|
|
label: "Dewpoint at 850mb",
|
|
unit: "°C"
|
|
},
|
|
lapse_rate: {
|
|
breakpoints: [
|
|
{ value: 0, r: 0, g: 255, b: 163 },
|
|
{ value: 4, r: 125, g: 255, b: 212 },
|
|
{ value: 6.5, r: 255, g: 229, b: 102 },
|
|
{ value: 8, r: 255, g: 144, b: 68 },
|
|
{ value: 10, r: 255, g: 79, b: 79 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(1)} °C/km` : "N/A",
|
|
label: "Lapse Rate",
|
|
unit: "°C/km"
|
|
},
|
|
inversion_strength: {
|
|
breakpoints: [
|
|
{ value: 0, r: 80, g: 80, b: 80 },
|
|
{ value: 1, r: 255, g: 229, b: 102 },
|
|
{ value: 3, r: 125, g: 255, b: 212 },
|
|
{ value: 5, r: 0, g: 255, b: 163 },
|
|
{ value: 10, r: 0, g: 200, b: 130 }
|
|
],
|
|
format: v => v != null ? (v > 0 ? `+${v.toFixed(1)} °C` : "None") : "N/A",
|
|
label: "Inversion Strength",
|
|
unit: "°C"
|
|
},
|
|
inversion_base_m: {
|
|
breakpoints: [
|
|
{ value: 0, r: 0, g: 255, b: 163 },
|
|
{ value: 500, r: 125, g: 255, b: 212 },
|
|
{ value: 1000, r: 255, g: 229, b: 102 },
|
|
{ value: 2000, r: 255, g: 144, b: 68 },
|
|
{ value: 3000, r: 255, g: 79, b: 79 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(0)} m AGL` : "None",
|
|
label: "Inversion Base Height",
|
|
unit: "m"
|
|
},
|
|
duct_base_m: {
|
|
breakpoints: [
|
|
{ value: 0, r: 0, g: 255, b: 163 },
|
|
{ value: 200, r: 125, g: 255, b: 212 },
|
|
{ value: 500, r: 255, g: 229, b: 102 },
|
|
{ value: 1000, r: 255, g: 144, b: 68 },
|
|
{ value: 2000, r: 255, g: 79, b: 79 }
|
|
],
|
|
format: v => v != null ? `${v.toFixed(0)} m AGL` : "None",
|
|
label: "Duct Base Height",
|
|
unit: "m"
|
|
},
|
|
duct_strength: {
|
|
breakpoints: [
|
|
{ value: 0, r: 80, g: 80, b: 80 },
|
|
{ value: 5, r: 255, g: 229, b: 102 },
|
|
{ value: 10, r: 125, g: 255, b: 212 },
|
|
{ value: 20, r: 0, g: 255, b: 163 },
|
|
{ value: 40, r: 0, g: 200, b: 130 }
|
|
],
|
|
format: v => v != null ? (v > 0 ? `${v.toFixed(1)} M-units` : "None") : "N/A",
|
|
label: "Duct Strength",
|
|
unit: "M"
|
|
}
|
|
}
|
|
|
|
function interpolateColor(value, breakpoints) {
|
|
if (value == null) return null
|
|
if (value <= breakpoints[0].value) return breakpoints[0]
|
|
if (value >= breakpoints[breakpoints.length - 1].value) return breakpoints[breakpoints.length - 1]
|
|
|
|
for (let i = 0; i < breakpoints.length - 1; i++) {
|
|
const lo = breakpoints[i]
|
|
const hi = breakpoints[i + 1]
|
|
if (value >= lo.value && value <= hi.value) {
|
|
const t = (value - lo.value) / (hi.value - lo.value)
|
|
return {
|
|
r: Math.round(lo.r + (hi.r - lo.r) * t),
|
|
g: Math.round(lo.g + (hi.g - lo.g) * t),
|
|
b: Math.round(lo.b + (hi.b - lo.b) * t)
|
|
}
|
|
}
|
|
}
|
|
return breakpoints[breakpoints.length - 1]
|
|
}
|
|
|
|
function buildDetailHTML(point) {
|
|
if (!point) return ""
|
|
|
|
const rows = [
|
|
{ label: "Temperature", value: COLOR_SCALES.temperature.format(point.temperature), color: "#ff9044" },
|
|
{ label: "Td Depression", value: COLOR_SCALES.dewpoint_depression.format(point.dewpoint_depression), color: "#7dffd4" },
|
|
{ label: "Rel. Humidity", value: COLOR_SCALES.surface_rh.format(point.surface_rh), color: "#00ffa3" },
|
|
{ label: "Sfc Pressure", value: point.surface_pressure_mb != null ? `${point.surface_pressure_mb.toFixed(1)} mb` : "N/A", color: "#aaa" },
|
|
{ label: "Refractivity", value: COLOR_SCALES.surface_refractivity.format(point.surface_refractivity), color: "#00ffa3" },
|
|
{ label: "N-Gradient", value: COLOR_SCALES.refractivity_gradient.format(point.refractivity_gradient), color: "#00ffa3" },
|
|
{ label: "BL Height", value: COLOR_SCALES.bl_height.format(point.bl_height), color: "#ffe566" },
|
|
{ label: "PWAT", value: COLOR_SCALES.pwat.format(point.pwat), color: "#4169e1" },
|
|
{ separator: true },
|
|
{ label: "T @ 850mb", value: COLOR_SCALES.temp_850mb.format(point.temp_850mb), color: "#ff9044" },
|
|
{ label: "Td @ 850mb", value: COLOR_SCALES.dewpoint_850mb.format(point.dewpoint_850mb), color: "#4169e1" },
|
|
{ label: "Lapse Rate", value: COLOR_SCALES.lapse_rate.format(point.lapse_rate), color: "#ffe566" },
|
|
{ label: "Inversion", value: COLOR_SCALES.inversion_strength.format(point.inversion_strength), color: point.inversion_strength > 0 ? "#00ffa3" : "#666" },
|
|
{ label: "Inv. Base", value: COLOR_SCALES.inversion_base_m.format(point.inversion_base_m), color: point.inversion_base_m != null ? "#7dffd4" : "#666" },
|
|
{ separator: true },
|
|
{ label: "Ducting", value: COLOR_SCALES.ducting.format(point.ducting), color: point.ducting ? "#00ffa3" : "#ff4f4f" },
|
|
{ label: "Duct Base", value: COLOR_SCALES.duct_base_m.format(point.duct_base_m), color: point.duct_base_m != null ? "#00ffa3" : "#666" },
|
|
{ label: "Duct Strength", value: COLOR_SCALES.duct_strength.format(point.duct_strength), color: point.duct_strength != null ? "#00ffa3" : "#666" }
|
|
]
|
|
|
|
const mobile = isMobile()
|
|
const closeBtn = mobile
|
|
? `<button onclick="document.getElementById('weather-detail-panel').style.display='none'" style="position:absolute;top:8px;right:10px;background:rgba(255,255,255,0.15);border:none;color:#fff;font-size:18px;line-height:1;width:28px;height:28px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;">×</button>`
|
|
: ""
|
|
const handle = mobile
|
|
? `<div style="display:flex;justify-content:center;padding:6px 0 2px;"><div style="width:32px;height:4px;border-radius:2px;background:rgba(255,255,255,0.3);"></div></div>`
|
|
: ""
|
|
|
|
const tableRows = rows.map(r => {
|
|
if (r.separator) {
|
|
return `<tr><td colspan="2" style="padding:2px 0;"><hr style="border:none;border-top:1px solid rgba(255,255,255,0.1);"></td></tr>`
|
|
}
|
|
return `<tr>
|
|
<td style="padding:3px 8px 3px 0;font-size:12px;white-space:nowrap;"><span style="color:${r.color};">\u25cf</span> ${r.label}</td>
|
|
<td style="padding:3px 0;font-size:12px;text-align:right;font-weight:600;">${r.value}</td>
|
|
</tr>`
|
|
}).join("")
|
|
|
|
const time = point.valid_time
|
|
? new Date(point.valid_time).toISOString().replace("T", " ").slice(0, 16) + " UTC"
|
|
: ""
|
|
|
|
return `<div style="min-width:${mobile ? 'auto' : '240px'};position:relative;">
|
|
${handle}${closeBtn}
|
|
<div style="background:rgba(255,255,255,0.1);padding:6px 10px;">
|
|
<strong style="font-size:14px;">Weather Details</strong>
|
|
</div>
|
|
<div style="padding:6px 10px;">
|
|
<div style="font-size:11px;opacity:0.6;margin-bottom:6px;">
|
|
${point.lat.toFixed(3)}\u00b0N, ${Math.abs(point.lon).toFixed(3)}\u00b0W — ${time}
|
|
</div>
|
|
<table style="width:100%;border-collapse:collapse;">${tableRows}</table>
|
|
</div>
|
|
</div>`
|
|
}
|
|
|
|
export const WeatherMap = {
|
|
mounted() {
|
|
this.map = L.map(this.el, {
|
|
center: [32.897, -97.038],
|
|
zoom: 7,
|
|
minZoom: 4,
|
|
maxZoom: 10
|
|
})
|
|
|
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
attribution: "© OpenStreetMap contributors",
|
|
maxZoom: 19
|
|
}).addTo(this.map)
|
|
|
|
this.weatherOverlay = null
|
|
this.weatherData = []
|
|
this.gridLookup = new Map()
|
|
this.selectedLayer = this.el.dataset.selectedLayer || "refractivity_gradient"
|
|
|
|
const initialData = JSON.parse(this.el.dataset.weather || "[]")
|
|
if (initialData.length > 0) {
|
|
this.weatherData = initialData
|
|
this.buildLookup()
|
|
this.renderLayer()
|
|
}
|
|
|
|
this.handleEvent("update_weather", ({ data }) => {
|
|
this.weatherData = data
|
|
this.buildLookup()
|
|
this.renderLayer()
|
|
})
|
|
|
|
this.detailPanel = document.getElementById("weather-detail-panel")
|
|
if (this.detailPanel) {
|
|
L.DomEvent.disableClickPropagation(this.detailPanel)
|
|
L.DomEvent.disableScrollPropagation(this.detailPanel)
|
|
}
|
|
|
|
// ESC hint
|
|
this.escHint = document.createElement("div")
|
|
this.escHint.className = "hidden md:block"
|
|
this.escHint.style.cssText = "position:absolute;top:12px;right:12px;z-index:1100;background:rgba(0,0,0,0.6);color:#fff;padding:4px 10px;border-radius:6px;font-size:12px;opacity:0;transition:opacity 0.2s;pointer-events:none;"
|
|
this.escHint.textContent = "Press ESC to close"
|
|
this.el.appendChild(this.escHint)
|
|
|
|
this.handleEvent("point_detail", (detail) => {
|
|
if (detail && detail.lat && this.detailPanel) {
|
|
this.detailPanel.innerHTML = buildDetailHTML(detail)
|
|
this.detailPanel.style.display = "block"
|
|
this.escHint.style.opacity = "1"
|
|
this.positionDetailPanel()
|
|
}
|
|
})
|
|
|
|
this.clickMarker = L.layerGroup().addTo(this.map)
|
|
|
|
this.map.on("click", (e) => {
|
|
this.clickMarker.clearLayers()
|
|
L.circleMarker([e.latlng.lat, e.latlng.lng], {
|
|
radius: 6, color: "#fff", weight: 2,
|
|
fillColor: "#888", fillOpacity: 0.8, interactive: false
|
|
}).addTo(this.clickMarker)
|
|
this.pushEvent("point_detail", { lat: e.latlng.lat, lon: e.latlng.lng })
|
|
})
|
|
|
|
this._escHandler = (e) => {
|
|
if (e.key === "Escape" && this.detailPanel) {
|
|
this.detailPanel.style.display = "none"
|
|
this.detailPanel.innerHTML = ""
|
|
this.clickMarker.clearLayers()
|
|
this.escHint.style.opacity = "0"
|
|
}
|
|
}
|
|
document.addEventListener("keydown", this._escHandler)
|
|
|
|
const controls = document.getElementById("weather-controls")
|
|
if (controls) {
|
|
L.DomEvent.disableClickPropagation(controls)
|
|
L.DomEvent.disableScrollPropagation(controls)
|
|
}
|
|
|
|
this.sendBounds()
|
|
this.map.on("moveend", () => this.sendBounds())
|
|
|
|
this._layerObserver = new MutationObserver(() => {
|
|
const newLayer = this.el.dataset.selectedLayer
|
|
if (newLayer && newLayer !== this.selectedLayer) {
|
|
this.selectedLayer = newLayer
|
|
this.renderLayer()
|
|
}
|
|
})
|
|
this._layerObserver.observe(this.el, { attributes: true, attributeFilter: ["data-selected-layer"] })
|
|
|
|
this.legend = L.control({ position: isMobile() ? "topright" : "bottomright" })
|
|
this.legend.onAdd = () => this.buildLegend()
|
|
this.legend.addTo(this.map)
|
|
},
|
|
|
|
destroyed() {
|
|
if (this._layerObserver) this._layerObserver.disconnect()
|
|
if (this._escHandler) document.removeEventListener("keydown", this._escHandler)
|
|
},
|
|
|
|
sendBounds() {
|
|
const b = this.map.getBounds()
|
|
this.pushEvent("map_bounds", {
|
|
south: b.getSouth(), north: b.getNorth(),
|
|
west: b.getWest(), east: b.getEast()
|
|
})
|
|
},
|
|
|
|
positionDetailPanel() {
|
|
if (!this.detailPanel || isMobile()) return
|
|
const controls = document.getElementById("weather-controls")
|
|
if (controls) {
|
|
const rect = controls.getBoundingClientRect()
|
|
this.detailPanel.style.left = rect.left + "px"
|
|
this.detailPanel.style.top = (rect.bottom + 8) + "px"
|
|
}
|
|
},
|
|
|
|
buildLookup() {
|
|
this.gridLookup = new Map()
|
|
this.weatherData.forEach(d => {
|
|
const key = `${d.lat.toFixed(3)},${d.lon.toFixed(3)}`
|
|
this.gridLookup.set(key, d)
|
|
})
|
|
},
|
|
|
|
renderLayer() {
|
|
if (this.weatherOverlay) {
|
|
this.map.removeLayer(this.weatherOverlay)
|
|
this.weatherOverlay = null
|
|
}
|
|
if (this.weatherData.length === 0) return
|
|
|
|
const layerId = this.selectedLayer
|
|
const scale = COLOR_SCALES[layerId]
|
|
if (!scale) return
|
|
|
|
const self = this
|
|
const isDucting = layerId === "ducting"
|
|
|
|
const Overlay = L.GridLayer.extend({
|
|
createTile(coords) {
|
|
const tile = document.createElement("canvas")
|
|
const size = this.getTileSize()
|
|
tile.width = size.x
|
|
tile.height = size.y
|
|
const ctx = tile.getContext("2d")
|
|
|
|
const step = 0.125
|
|
const nwPoint = coords.scaleBy(size)
|
|
const nw = this._map.unproject(nwPoint, coords.z)
|
|
const se = this._map.unproject(nwPoint.add(size), coords.z)
|
|
|
|
const latPerPx = (nw.lat - se.lat) / size.y
|
|
const lonPerPx = (se.lng - nw.lng) / size.x
|
|
const cellPxX = Math.max(1, Math.ceil(step / lonPerPx))
|
|
const cellPxY = Math.max(1, Math.ceil(step / Math.abs(latPerPx)))
|
|
const absLatPerPx = Math.abs(latPerPx)
|
|
|
|
for (let py = 0; py < size.y; py += cellPxY) {
|
|
const lat = nw.lat - py * absLatPerPx
|
|
for (let px = 0; px < size.x; px += cellPxX) {
|
|
const lon = nw.lng + px * lonPerPx
|
|
const rlat = (Math.round(lat / step) * step).toFixed(3)
|
|
const rlon = (Math.round(lon / step) * step).toFixed(3)
|
|
const point = self.gridLookup.get(`${rlat},${rlon}`)
|
|
if (!point) continue
|
|
|
|
const value = point[layerId]
|
|
|
|
if (isDucting) {
|
|
if (value) {
|
|
ctx.fillStyle = `rgba(${scale.trueColor.r},${scale.trueColor.g},${scale.trueColor.b},0.55)`
|
|
ctx.fillRect(px, py, cellPxX, cellPxY)
|
|
}
|
|
} else {
|
|
const c = interpolateColor(value, scale.breakpoints)
|
|
if (c) {
|
|
ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},0.55)`
|
|
ctx.fillRect(px, py, cellPxX, cellPxY)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return tile
|
|
}
|
|
})
|
|
|
|
this.weatherOverlay = new Overlay({ opacity: 1.0 })
|
|
this.weatherOverlay.addTo(this.map)
|
|
|
|
if (this.legend) {
|
|
this.map.removeControl(this.legend)
|
|
this.legend = L.control({ position: isMobile() ? "topright" : "bottomright" })
|
|
this.legend.onAdd = () => this.buildLegend()
|
|
this.legend.addTo(this.map)
|
|
}
|
|
},
|
|
|
|
buildLegend() {
|
|
const div = L.DomUtil.create("div")
|
|
const layerId = this.selectedLayer
|
|
const scale = COLOR_SCALES[layerId]
|
|
if (!scale) return div
|
|
|
|
const mobile = isMobile()
|
|
|
|
if (layerId === "ducting") {
|
|
const c = scale.trueColor
|
|
div.style.cssText = `background:#fff;color:#333;padding:${mobile ? '4px 6px' : '10px 14px'};border-radius:${mobile ? '6px' : '8px'};box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:${mobile ? '10px' : '13px'};line-height:2;`
|
|
div.innerHTML = `<strong style="color:#333;">${scale.label}</strong><br>` +
|
|
`<span style="background:rgb(${c.r},${c.g},${c.b});width:14px;height:14px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:6px;border:1px solid rgba(0,0,0,0.15);"></span>Ducting<br>` +
|
|
`<span style="background:transparent;width:14px;height:14px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:6px;border:1px solid rgba(0,0,0,0.15);"></span>None`
|
|
return div
|
|
}
|
|
|
|
const bp = scale.breakpoints
|
|
|
|
if (mobile) {
|
|
div.style.cssText = "background:#fff;color:#333;padding:4px 6px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,0.3);font-size:10px;line-height:1.6;"
|
|
div.innerHTML = bp.map(b =>
|
|
`<span style="background:rgb(${b.r},${b.g},${b.b});width:10px;height:10px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:3px;border:1px solid rgba(0,0,0,0.15);"></span>${b.value}`
|
|
).join("<br>")
|
|
} else {
|
|
div.style.cssText = "background:#fff;color:#333;padding:10px 14px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:13px;line-height:2;"
|
|
div.innerHTML = `<strong style="color:#333;">${scale.label}</strong><br>` +
|
|
bp.map(b =>
|
|
`<span style="background:rgb(${b.r},${b.g},${b.b});width:14px;height:14px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:6px;border:1px solid rgba(0,0,0,0.15);"></span><span style="color:#333;">${b.value} ${scale.unit}</span>`
|
|
).join("<br>")
|
|
}
|
|
return div
|
|
}
|
|
}
|