Updated Map View

This commit is contained in:
2026-06-20 22:21:17 +10:00
parent 6fad966b7e
commit 05ca994253
52 changed files with 2038 additions and 803 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, nextTick, ref } from 'vue'
import VueApexCharts from "vue3-apexcharts"
const props = defineProps<{
@@ -12,23 +12,63 @@ const props = defineProps<{
barHeight?: number
colors?: string[]
options?: object
limit?: number
}>()
const BAR_HEIGHT = computed(() => props.barHeight ?? 32)
const MAX_VISIBLE = computed(() => props.maxVisible ?? 12)
const chartHeight = computed(() => props.categories.length * BAR_HEIGHT.value + 40)
// ── Limit / Show More ─────────────────────────────────────────────────────────
const showAll = ref(false)
const isExpanding = ref(false)
async function expand() {
isExpanding.value = true
await nextTick() // paint "Loading..." before blocking on chart render
showAll.value = true
await nextTick() // wait for ApexCharts to finish
isExpanding.value = false
}
function collapse() {
showAll.value = false
}
const visibleCategories = computed(() =>
props.limit && !showAll.value
? props.categories.slice(0, props.limit)
: props.categories
)
const visibleSeries = computed(() =>
props.limit && !showAll.value
? props.series.map(s => ({ ...s, data: s.data.slice(0, props.limit) }))
: props.series
)
const hasMore = computed(() =>
!!props.limit && props.categories.length > props.limit
)
const hiddenCount = computed(() =>
props.limit ? props.categories.length - props.limit : 0
)
// ── Chart dimensions ──────────────────────────────────────────────────────────
const chartHeight = computed(() => visibleCategories.value.length * BAR_HEIGHT.value + 40)
const scrollHeight = computed(() => {
const visible = Math.min(props.categories.length, MAX_VISIBLE.value)
const visible = Math.min(visibleCategories.value.length, MAX_VISIBLE.value)
return `${visible * BAR_HEIGHT.value + 40}px`
})
// ── Tooltip state (exposed to parent via scoped slot) ─────────────────────────
// ── Tooltip state ─────────────────────────────────────────────────────────────
const tooltipVisible = ref(false)
const tooltipX = ref(0)
const tooltipY = ref(0)
const hoveredIndex = ref<number | null>(null)
const tooltipVisible = ref(false)
const tooltipX = ref(0)
const tooltipY = ref(0)
const hoveredIndex = ref<number | null>(null)
function onMouseMove(e: MouseEvent) {
tooltipX.value = e.clientX + 14
@@ -74,7 +114,7 @@ const chartOptions = computed(() => ({
colors: props.colors ?? ['#4da6ff', '#ffc107'],
dataLabels: { enabled: false },
xaxis: {
categories: props.categories,
categories: visibleCategories.value,
labels: { show: false },
axisBorder: { show: false },
axisTicks: { show: false },
@@ -113,7 +153,7 @@ const chartOptions = computed(() => ({
type="bar"
:height="chartHeight"
:options="options ?? chartOptions"
:series="series"
:series="visibleSeries"
/>
</div>
@@ -125,6 +165,22 @@ const chartOptions = computed(() => ({
:index="hoveredIndex"
/>
<div v-if="hasMore || (limit && showAll)" class="show-more-wrap">
<button
v-if="!showAll"
class="show-more"
:disabled="isExpanding"
:class="{ loading: isExpanding }"
@click="expand"
>
<span v-if="isExpanding" class="spinner" />
<span>{{ isExpanding ? 'Loading…' : `Show ${hiddenCount} more` }}</span>
</button>
<button v-else class="show-more" @click="collapse">
Show less
</button>
</div>
<div v-if="footerValue !== undefined" class="chart-footer">
<span class="total-count">{{ footerValue }}</span>
<span class="total-label">{{ footerLabel }}</span>
@@ -167,6 +223,49 @@ const chartOptions = computed(() => ({
.chart-scroll::-webkit-scrollbar-track { background: transparent; }
.chart-scroll::-webkit-scrollbar-thumb { background: #334455; border-radius: 2px; }
.show-more-wrap {
display: flex;
padding-left: 2px;
}
.show-more {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: 1px solid #334455;
border-radius: 4px;
color: #778899;
cursor: pointer;
font-size: 12px;
padding: 4px 10px;
transition: color 0.15s, border-color 0.15s;
}
.show-more:hover:not(:disabled) {
border-color: #4da6ff;
color: #4da6ff;
}
.show-more:disabled {
cursor: default;
opacity: 0.6;
}
.spinner {
width: 10px;
height: 10px;
border: 1.5px solid #556677;
border-top-color: #4da6ff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.chart-footer {
display: flex;
align-items: baseline;
@@ -32,6 +32,8 @@ const chartEvents = computed(() => ({
<template>
<ScrollingHorizontalBarChart
:limit="18
"
title="Top countries"
:series="flightStats.countries.value.series"
:categories="countries.map(c => c.name)"
@@ -41,6 +41,7 @@ const chartEvents = computed(() => ({
:events="chartEvents"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
:limit="12"
>
<template #tooltip="{ visible, x, y, index }">
<ChartTooltip :visible="visible" :x="x" :y="y">
@@ -40,6 +40,7 @@ const chartEvents = computed(() => ({
:categories="airlines.map(a => a.name)"
:footer-value="airlines.length"
footer-label="total airlines"
:limit="12"
:events="chartEvents"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
@@ -42,6 +42,7 @@ const chartEvents = computed(() => ({
:events="chartEvents"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
:limit="12"
>
<template #tooltip="{ visible, x, y, index }">
<ChartTooltip :visible="visible" :x="x" :y="y">
@@ -31,6 +31,7 @@ const series = computed(() => props.flightStats.topRoutes.value.series)
:footer-value="routes.length"
footer-label="total routes"
:max-visible="12"
:limit="12"
>
<template #tooltip="{ visible, x, y, index }">
<ChartTooltip :visible="visible" :x="x" :y="y">