mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Add the Map blocks page and its screenshot pages
This commit is contained in:
1 parent
b135a40064
commit
d041594429
16 files changed
+489
-27
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tabler/preview": minor
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Added the Map blocks page: a store locator, a delivery tracker, a live users map and a users by country card.
|
||||
@@ -521,6 +521,10 @@ Drop the card body to let the map fill the whole card. Add `rounded` to the cont
|
||||
|
||||
Live versions of these maps are on the [maps preview page](https://preview.tabler.io/maps.html).
|
||||
|
||||
### Blocks
|
||||
|
||||
The [map blocks preview page](https://preview.tabler.io/map-blocks.html) has larger examples that pair a map with other components: a store locator with a searchable list, a delivery tracker with a route and a status card over the map, a world map of live users, and a vector map of users by country next to a table.
|
||||
|
||||
## Using Mapbox GL JS
|
||||
|
||||
MapLibre GL JS started as a fork of Mapbox GL JS, so the two share most of their API. To use Mapbox instead, load its script and stylesheet, replace `maplibregl` with `mapboxgl`, set `mapboxgl.accessToken` to your public token, and use a `mapbox://` style URL.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import MapPalette from '@components/demo/MapPalette.astro'
|
||||
import MapStoreLocator from '@components/cards/MapStoreLocator.astro'
|
||||
import MapDeliveryTracker from '@components/cards/MapDeliveryTracker.astro'
|
||||
import MapLiveUsers from '@components/cards/MapLiveUsers.astro'
|
||||
import MapUsersByCountry from '@components/cards/MapUsersByCountry.astro'
|
||||
import DocsLink from '@ui/DocsLink.astro'
|
||||
---
|
||||
|
||||
<DefaultLayout title="Map blocks" pageMenu="plugins.map-blocks" pageLibs={['maplibre-gl', 'jsvectormap']} description="Ready-made blocks that pair a map with a list, a card or a table.">
|
||||
<DocsLink slot="page-header-actions" path="/ui/components/map" />
|
||||
<MapPalette />
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<MapStoreLocator />
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<MapDeliveryTracker />
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<MapLiveUsers />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<MapUsersByCountry />
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -80,24 +80,6 @@ const places = [
|
||||
<DefaultLayout title="Maps" pageMenu="plugins.maps" pageLibs={['maplibre-gl']} description="Interactive MapLibre maps — plain, styled, with custom markers and popups.">
|
||||
<DocsLink slot="page-header-actions" path="/ui/components/map" />
|
||||
<MapPalette />
|
||||
<CaptureScript>
|
||||
<script is:inline>
|
||||
// MapLibre draws on a canvas, so a layer needs a plain rgb() color instead of a CSS variable
|
||||
window.tablerMapColor = (name) => {
|
||||
const context = document.createElement('canvas').getContext('2d')
|
||||
const probe = document.body.appendChild(document.createElement('span'))
|
||||
|
||||
probe.style.color = `var(${name})`
|
||||
context.fillStyle = getComputedStyle(probe).color
|
||||
context.fillRect(0, 0, 1, 1)
|
||||
probe.remove()
|
||||
|
||||
const [red, green, blue] = context.getImageData(0, 0, 1, 1).data
|
||||
|
||||
return `rgb(${red}, ${green}, ${blue})`
|
||||
}
|
||||
</script>
|
||||
</CaptureScript>
|
||||
<div class="row row-cards">
|
||||
{
|
||||
mapEntries.map(([mapId, data]) =>
|
||||
|
||||
@@ -92,12 +92,13 @@ const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import *
|
||||
|
||||
<link rel="icon" href="./favicon-dev.ico" type="image/x-icon" />
|
||||
|
||||
{/* Library styles first, so the Tabler overrides in tabler-vendors.css win (same order as BaseLayout) */}
|
||||
{libCssFiles.map((href) => <link href={href} rel="stylesheet" />)}
|
||||
|
||||
<link href={`./dist/css/tabler${min}.css`} rel="stylesheet" />
|
||||
|
||||
{stylePlugins.map((plugin) => <link href={`./dist/css/tabler-${plugin}${min}.css`} rel="stylesheet" />)}
|
||||
|
||||
{libCssFiles.map((href) => <link href={href} rel="stylesheet" />)}
|
||||
|
||||
<!-- Inter for the captures: Tabler ships system font stacks (no web font),
|
||||
and the body's cv03/cv04/cv11 feature settings were tuned for Inter, so the
|
||||
pictures show the face the design was drawn in — the same on every machine
|
||||
@@ -191,7 +192,20 @@ const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import *
|
||||
if (document.readyState !== 'loading') resolve(undefined)
|
||||
else document.addEventListener('DOMContentLoaded', () => resolve(undefined), { once: true })
|
||||
})
|
||||
Promise.all([document.fonts.ready, domReady]).then(() => {
|
||||
// Maps draw their tiles after the page is ready, so wait until each one has loaded.
|
||||
// The first check is delayed: the maps are created in DOMContentLoaded handlers.
|
||||
const mapsReady = () =>
|
||||
new Promise((resolve) => {
|
||||
const check = () => {
|
||||
const maps = Object.values((window as { tabler_map?: Record<string, { loaded(): boolean }> }).tabler_map ?? {})
|
||||
|
||||
if (maps.every((map) => map.loaded())) resolve(undefined)
|
||||
else setTimeout(check, 100)
|
||||
}
|
||||
|
||||
setTimeout(check, 100)
|
||||
})
|
||||
Promise.all([document.fonts.ready, domReady, mapsReady()]).then(() => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => (document.documentElement.dataset.screenshotReady = 'true')))
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -64,12 +64,13 @@ const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import *
|
||||
|
||||
<link rel="icon" href="./favicon-dev.ico" type="image/x-icon" />
|
||||
|
||||
{/* Library styles first, so the Tabler overrides in tabler-vendors.css win (same order as BaseLayout) */}
|
||||
{libCssFiles.map((href) => <link href={href} rel="stylesheet" />)}
|
||||
|
||||
<link href={`./dist/css/tabler${rtlSuffix}${min}.css`} rel="stylesheet" />
|
||||
|
||||
{stylePlugins.map((plugin) => <link href={`./dist/css/tabler-${plugin}${rtlSuffix}${min}.css`} rel="stylesheet" />)}
|
||||
|
||||
{libCssFiles.map((href) => <link href={href} rel="stylesheet" />)}
|
||||
|
||||
<!-- Inter for the captures: Tabler ships system font stacks (no web font),
|
||||
and the body's cv03/cv04/cv11 feature settings were tuned for Inter, so the
|
||||
pictures show the face the design was drawn in — the same on every machine
|
||||
@@ -188,7 +189,20 @@ const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import *
|
||||
if (document.readyState !== 'loading') resolve(undefined)
|
||||
else document.addEventListener('DOMContentLoaded', () => resolve(undefined), { once: true })
|
||||
})
|
||||
Promise.all([document.fonts.ready, domReady]).then(() => {
|
||||
// Maps draw their tiles after the page is ready, so wait until each one has loaded.
|
||||
// The first check is delayed: the maps are created in DOMContentLoaded handlers.
|
||||
const mapsReady = () =>
|
||||
new Promise((resolve) => {
|
||||
const check = () => {
|
||||
const maps = Object.values((window as { tabler_map?: Record<string, { loaded(): boolean }> }).tabler_map ?? {})
|
||||
|
||||
if (maps.every((map) => map.loaded())) resolve(undefined)
|
||||
else setTimeout(check, 100)
|
||||
}
|
||||
|
||||
setTimeout(check, 100)
|
||||
})
|
||||
Promise.all([document.fonts.ready, domReady, mapsReady()]).then(() => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => (document.documentElement.dataset.screenshotReady = 'true')))
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
// Delivery tracker block: a map with the route of a delivery, solid where the
|
||||
// courier has been and dashed ahead, the courier and the destination as
|
||||
// markers, and the order status in a card over the map.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import MapPalette from '@components/demo/MapPalette.astro'
|
||||
import MapDeliveryTracker from '@components/cards/MapDeliveryTracker.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Map: delivery tracker" columns={1.8} pageLibs={['maplibre-gl']}>
|
||||
<MapPalette />
|
||||
<MapDeliveryTracker />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
// Live users block: a world map with a circle for each city, sized by the
|
||||
// number of active users, and the total in a card over the map.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import MapPalette from '@components/demo/MapPalette.astro'
|
||||
import MapLiveUsers from '@components/cards/MapLiveUsers.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Map: live users" columns={1.8} pageLibs={['maplibre-gl']}>
|
||||
<MapPalette />
|
||||
<MapLiveUsers />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
// Store locator block: a searchable list of stores next to a map. The first
|
||||
// store is selected, so its row is highlighted and its popup is open.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import MapPalette from '@components/demo/MapPalette.astro'
|
||||
import MapStoreLocator from '@components/cards/MapStoreLocator.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Map: store locator" columns={3} pageLibs={['maplibre-gl']}>
|
||||
<MapPalette />
|
||||
<MapStoreLocator />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
// A delivery on a map: the route, the courier position and the order status.
|
||||
// Needs <MapPalette /> on the page, before this card.
|
||||
import Map from '../demo/Map.astro'
|
||||
import CaptureScript from '../CaptureScript.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import Progress from '@ui/Progress.astro'
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import { staticPath } from '@shared/lib/assets'
|
||||
|
||||
// The courier is at the sixth point of the route: the part behind them is done
|
||||
const route = [
|
||||
[13.3695, 52.5251],
|
||||
[13.3777, 52.5208],
|
||||
[13.388, 52.5186],
|
||||
[13.3989, 52.5176],
|
||||
[13.405, 52.5192],
|
||||
[13.4132, 52.5219],
|
||||
[13.4215, 52.5165],
|
||||
[13.43, 52.5105],
|
||||
[13.4397, 52.505],
|
||||
]
|
||||
const courierAt = 5
|
||||
---
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Delivery tracker</CardTitle>
|
||||
</CardHeader>
|
||||
<Map mapId="delivery" title="Delivery tracker" center={[52.5215, 13.4046]} zoom={11.6} height="20rem" rounded={false}>
|
||||
<div slot="overlay" class="card card-sm position-absolute top-0 start-0 m-3 shadow" style="width: 15rem">
|
||||
<div class="card-body">
|
||||
<Subheader>Order #4821</Subheader>
|
||||
<div class="h3 mb-2">Arriving in 12 min</div>
|
||||
<Progress value={62} size="sm" color="green" />
|
||||
</div>
|
||||
</div>
|
||||
<div data-lng={route[0]?.[0]} data-lat={route[0]?.[1]}><span class="map-marker map-marker-dot"></span></div>
|
||||
<div data-lng={route[courierAt]?.[0]} data-lat={route[courierAt]?.[1]}>
|
||||
<span class="map-marker map-marker-green"><Icon name="truck-delivery" /></span>
|
||||
</div>
|
||||
<div data-lng={route.at(-1)?.[0]} data-lat={route.at(-1)?.[1]} data-anchor="bottom">
|
||||
<span class="map-marker map-marker-pin"><Icon name="home" /></span>
|
||||
</div>
|
||||
</Map>
|
||||
<CaptureScript>
|
||||
<script is:inline define:vars={{ route, courierAt }}>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const map = window.tabler_map['map-delivery']
|
||||
const line = (coordinates) => ({ type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates } } })
|
||||
|
||||
map.on('style.load', () => {
|
||||
const color = tablerMapColor('--tblr-primary')
|
||||
|
||||
// The part of the route still to drive is dashed, the part already driven is solid
|
||||
map.addSource('route-left', line(route.slice(courierAt)))
|
||||
map.addLayer({ id: 'route-left', type: 'line', source: 'route-left', paint: { 'line-color': color, 'line-width': 3, 'line-opacity': 0.6, 'line-dasharray': [1, 2] } })
|
||||
map.addSource('route-done', line(route.slice(0, courierAt + 1)))
|
||||
map.addLayer({ id: 'route-done', type: 'line', source: 'route-done', layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: { 'line-color': color, 'line-width': 3 } })
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</CaptureScript>
|
||||
<CardBody>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<Avatar src={staticPath('avatars/002m.jpg')} />
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="fw-medium">Mateusz Nowak</div>
|
||||
<div class="text-secondary">Your courier</div>
|
||||
</div>
|
||||
<div class="col-auto text-end">
|
||||
<div class="fw-medium">3.4 km</div>
|
||||
<div class="text-secondary">left to go</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="btn btn-icon" aria-label="Call the courier"><Icon name="phone" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
// A world map of active users, with the total in a card over the map.
|
||||
// Needs <MapPalette /> on the page, before this card.
|
||||
import Map from '../demo/Map.astro'
|
||||
import CaptureScript from '../CaptureScript.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import Trending from '@ui/Trending.astro'
|
||||
|
||||
const cities = [
|
||||
{ name: 'Berlin', lng: 13.405, lat: 52.52, users: 820 },
|
||||
{ name: 'London', lng: -0.1276, lat: 51.5072, users: 1240 },
|
||||
{ name: 'New York', lng: -74.006, lat: 40.7128, users: 1580 },
|
||||
{ name: 'Tokyo', lng: 139.6917, lat: 35.6895, users: 960 },
|
||||
{ name: 'São Paulo', lng: -46.6333, lat: -23.5505, users: 540 },
|
||||
{ name: 'Sydney', lng: 151.2093, lat: -33.8688, users: 310 },
|
||||
{ name: 'Lagos', lng: 3.3792, lat: 6.5244, users: 420 },
|
||||
{ name: 'Mumbai', lng: 72.8777, lat: 19.076, users: 1100 },
|
||||
]
|
||||
---
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Live users</CardTitle>
|
||||
</CardHeader>
|
||||
<Map mapId="live-users" title="Live users" center={[32, 30]} zoom={0.45} height="20rem" rounded={false}>
|
||||
<div slot="overlay" class="card card-sm position-absolute top-0 end-0 m-3 shadow">
|
||||
<div class="card-body">
|
||||
<Subheader>Active users</Subheader>
|
||||
<div class="h1 mb-1">3,544</div>
|
||||
<div class="text-secondary"><Trending value="+12.5" /> vs last hour</div>
|
||||
</div>
|
||||
</div>
|
||||
</Map>
|
||||
<CaptureScript>
|
||||
<script is:inline define:vars={{ cities }}>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const map = window.tabler_map['map-live-users']
|
||||
const tooltip = new maplibregl.Popup({ closeButton: false, offset: 12, className: 'map-tooltip' })
|
||||
|
||||
map.on('style.load', () => {
|
||||
map.addSource('cities', {
|
||||
type: 'geojson',
|
||||
data: { type: 'FeatureCollection', features: cities.map((city) => ({ type: 'Feature', properties: city, geometry: { type: 'Point', coordinates: [city.lng, city.lat] } })) },
|
||||
})
|
||||
map.addLayer({
|
||||
id: 'cities',
|
||||
type: 'circle',
|
||||
source: 'cities',
|
||||
paint: { 'circle-color': tablerMapColor('--tblr-primary'), 'circle-opacity': 0.6, 'circle-radius': ['interpolate', ['linear'], ['get', 'users'], 0, 3, 1600, 14] },
|
||||
})
|
||||
})
|
||||
|
||||
map.on('mousemove', 'cities', (event) => {
|
||||
const city = event.features[0].properties
|
||||
|
||||
map.getCanvas().style.cursor = 'pointer'
|
||||
tooltip.setLngLat([city.lng, city.lat]).setText(`${city.name}: ${city.users} users`).addTo(map)
|
||||
})
|
||||
map.on('mouseleave', 'cities', () => {
|
||||
map.getCanvas().style.cursor = ''
|
||||
tooltip.remove()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</CaptureScript>
|
||||
<CardBody>
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
<div class="fw-medium">1,580</div>
|
||||
<div class="text-secondary">New York</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="fw-medium">1,240</div>
|
||||
<div class="text-secondary">London</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="fw-medium">1,100</div>
|
||||
<div class="text-secondary">Mumbai</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
// A list of stores next to a map: picking a store moves the map and opens its popup.
|
||||
// Needs <MapPalette /> on the page, before this card.
|
||||
import Map from '../demo/Map.astro'
|
||||
import CaptureScript from '../CaptureScript.astro'
|
||||
import Badge from '@ui/Badge.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
|
||||
const stores = [
|
||||
{ name: 'Tabler Store Mitte', address: 'Friedrichstraße 112, 10117 Berlin', distance: '0.4 km', open: true, lng: 13.3889, lat: 52.5246 },
|
||||
{ name: 'Tabler Store Alexanderplatz', address: 'Karl-Liebknecht-Straße 9, 10178 Berlin', distance: '1.2 km', open: true, lng: 13.4094, lat: 52.5219 },
|
||||
{ name: 'Tabler Store Kreuzberg', address: 'Oranienstraße 34, 10999 Berlin', distance: '2.6 km', open: true, lng: 13.4197, lat: 52.5009 },
|
||||
{ name: 'Tabler Store Charlottenburg', address: 'Kantstraße 54, 10627 Berlin', distance: '4.8 km', open: false, lng: 13.3087, lat: 52.5063 },
|
||||
{ name: 'Tabler Store Prenzlauer Berg', address: 'Kastanienallee 21, 10435 Berlin', distance: '2.1 km', open: true, lng: 13.4098, lat: 52.5385 },
|
||||
{ name: 'Tabler Store Friedrichshain', address: 'Warschauer Straße 58, 10243 Berlin', distance: '3.9 km', open: false, lng: 13.4492, lat: 52.5083 },
|
||||
]
|
||||
---
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Store locator</CardTitle>
|
||||
</CardHeader>
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-4 border-end">
|
||||
<div class="p-3 border-bottom">
|
||||
<div class="input-icon">
|
||||
<span class="input-icon-addon"><Icon name="search" /></span>
|
||||
<input type="search" class="form-control" id="stores-search" placeholder="Search stores…" aria-label="Search stores" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush overflow-auto" id="stores-list" style="height: 24rem">
|
||||
{
|
||||
stores.map((store, index) => (
|
||||
<button type="button" class:list={['list-group-item list-group-item-action', index === 0 && 'active']} data-lng={store.lng} data-lat={store.lat} data-address={store.address}>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="fw-medium" data-name>
|
||||
{store.name}
|
||||
</span>
|
||||
<Badge color={store.open ? 'green' : 'secondary'} light class="ms-auto" text={store.open ? 'Open' : 'Closed'} />
|
||||
</div>
|
||||
<div class="text-secondary">{store.address}</div>
|
||||
<div class="text-secondary small mt-1">
|
||||
<Icon name="map-pin" class="icon-inline" /> {store.distance}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<Map mapId="stores" title="Store locator" center={[52.519, 13.39]} zoom={11.3} height="28.5rem" rounded={false} controls />
|
||||
</div>
|
||||
</div>
|
||||
<CaptureScript>
|
||||
<script is:inline>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const map = window.tabler_map['map-stores']
|
||||
const items = [...document.querySelectorAll('#stores-list [data-lng]')]
|
||||
|
||||
// One marker with a popup for each store in the list
|
||||
const markers = items.map((item) => {
|
||||
const element = document.createElement('span')
|
||||
const popup = document.createElement('div')
|
||||
|
||||
element.className = 'map-marker map-marker-pin'
|
||||
element.innerHTML = item.querySelector('.icon').outerHTML
|
||||
popup.innerHTML = '<div class="fw-medium"></div><div class="text-secondary"></div>'
|
||||
popup.firstChild.textContent = item.querySelector('[data-name]').textContent
|
||||
popup.lastChild.textContent = item.dataset.address
|
||||
|
||||
return new maplibregl.Marker({ element, anchor: 'bottom' })
|
||||
.setLngLat([Number(item.dataset.lng), Number(item.dataset.lat)])
|
||||
.setPopup(new maplibregl.Popup({ offset: 40, closeButton: false, focusAfterOpen: false }).setDOMContent(popup))
|
||||
.addTo(map)
|
||||
})
|
||||
|
||||
const select = (index, move) => {
|
||||
items.forEach((item, position) => item.classList.toggle('active', position === index))
|
||||
markers.forEach((marker, position) => marker.getPopup().isOpen() !== (position === index) && marker.togglePopup())
|
||||
|
||||
if (move) {
|
||||
map.flyTo({ center: markers[index].getLngLat(), zoom: 13 })
|
||||
}
|
||||
}
|
||||
|
||||
items.forEach((item, index) => item.addEventListener('click', () => select(index, true)))
|
||||
markers.forEach((marker, index) =>
|
||||
marker.getElement().addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
select(index, false)
|
||||
items[index].scrollIntoView({ block: 'nearest' })
|
||||
}),
|
||||
)
|
||||
|
||||
// Search hides the stores, and their markers, that do not match
|
||||
document.getElementById('stores-search').addEventListener('input', (event) => {
|
||||
const query = event.target.value.trim().toLowerCase()
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const hidden = !item.textContent.toLowerCase().includes(query)
|
||||
|
||||
item.hidden = hidden
|
||||
markers[index].getElement().hidden = hidden
|
||||
})
|
||||
})
|
||||
|
||||
select(0, false)
|
||||
})
|
||||
</script>
|
||||
</CaptureScript>
|
||||
</Card>
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
// A vector world map shaded by users, next to a table of the top countries.
|
||||
import MapVector from '../demo/MapVector.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import Flag from '@ui/Flag.astro'
|
||||
import Progress from '@ui/Progress.astro'
|
||||
import type { FlagCountry } from '@shared/lib/tokens'
|
||||
|
||||
const countries: { name: string; flag: FlagCountry; users: string; share: number }[] = [
|
||||
{ name: 'United States', flag: 'us', users: '12,480', share: 32 },
|
||||
{ name: 'Brazil', flag: 'br', users: '7,120', share: 18 },
|
||||
{ name: 'Germany', flag: 'de', users: '5,930', share: 15 },
|
||||
{ name: 'India', flag: 'in', users: '4,360', share: 11 },
|
||||
{ name: 'Australia', flag: 'au', users: '3,540', share: 9 },
|
||||
{ name: 'Poland', flag: 'pl', users: '2,210', share: 6 },
|
||||
]
|
||||
---
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Users by country</CardTitle>
|
||||
</CardHeader>
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-7 border-end">
|
||||
<CardBody>
|
||||
<MapVector mapId="world" ratio="16x9" />
|
||||
</CardBody>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<th class="text-end">Users</th>
|
||||
<th class="w-33">Share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
countries.map((country) => (
|
||||
<tr>
|
||||
<td>
|
||||
<Flag flag={country.flag} size="xs" class="me-2" />
|
||||
{country.name}
|
||||
</td>
|
||||
<td class="text-end">{country.users}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-secondary">{country.share}%</span>
|
||||
<Progress value={country.share} size="sm" class="flex-fill" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -22,9 +22,11 @@ interface Props {
|
||||
globe?: boolean;
|
||||
/** rounded corners; turn off when the map fills a box that has its own corners */
|
||||
rounded?: boolean;
|
||||
/** fixed height (any CSS length) instead of an aspect ratio */
|
||||
height?: string;
|
||||
}
|
||||
|
||||
const { mapId, ratio, center: centerProp, zoom: zoomProp, title, style: styleProp, controls: controlsProp, globe = false, rounded = true } = Astro.props;
|
||||
const { mapId, ratio, center: centerProp, zoom: zoomProp, title, style: styleProp, controls: controlsProp, globe = false, rounded = true, height } = Astro.props;
|
||||
|
||||
type MapData = {
|
||||
title?: string | undefined;
|
||||
@@ -60,8 +62,8 @@ const ratioClass = data?.ratio ?? ratio ?? '16x9';
|
||||
|
||||
{
|
||||
data && (
|
||||
<div class={`ratio ratio-${ratioClass}`}>
|
||||
<div>
|
||||
<div class:list={height ? 'position-relative' : `ratio ratio-${ratioClass}`} style={height ? `height: ${height}` : undefined}>
|
||||
<div class:list={[height && 'h-100']}>
|
||||
<div id={mapContainerId} class:list={['w-100 h-100', rounded && 'rounded']} role="img" aria-label={data.title ?? 'Map'} />
|
||||
<slot name="overlay" />
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,29 @@
|
||||
// Softer colors for the OpenFreeMap `positron` and `dark` map styles: near-white
|
||||
// land, pale water and muted labels. Defines `window.tablerMapPalette(map, mode)`;
|
||||
// put it on the page once, before the maps. `Map.astro` calls it on each style load.
|
||||
// Also defines `window.tablerMapColor(name)`, which turns a CSS variable into a
|
||||
// plain rgb() color for a map layer.
|
||||
import CaptureScript from '../CaptureScript.astro';
|
||||
---
|
||||
|
||||
<CaptureScript>
|
||||
<!-- BEGIN MAP PALETTE -->
|
||||
<script is:inline>
|
||||
// MapLibre draws on a canvas, so a layer needs a plain rgb() color instead of a CSS variable
|
||||
window.tablerMapColor = (name) => {
|
||||
const context = document.createElement('canvas').getContext('2d');
|
||||
const probe = document.body.appendChild(document.createElement('span'));
|
||||
|
||||
probe.style.color = `var(${name})`;
|
||||
context.fillStyle = getComputedStyle(probe).color;
|
||||
context.fillRect(0, 0, 1, 1);
|
||||
probe.remove();
|
||||
|
||||
const [red, green, blue] = context.getImageData(0, 0, 1, 1).data;
|
||||
|
||||
return `rgb(${red}, ${green}, ${blue})`;
|
||||
};
|
||||
|
||||
// Paint values by layer id: the first rule that matches a layer is used
|
||||
window.tablerMapPalette = (map, mode) => {
|
||||
const palettes = {
|
||||
|
||||
@@ -554,6 +554,11 @@
|
||||
"url": "map-fullsize.html",
|
||||
"title": "Map fullsize"
|
||||
},
|
||||
"map-blocks": {
|
||||
"url": "map-blocks.html",
|
||||
"title": "Map blocks",
|
||||
"badge": "New"
|
||||
},
|
||||
"maps-vector": {
|
||||
"url": "maps-vector.html",
|
||||
"title": "Map vector"
|
||||
|
||||
Reference in new issue
Block a user