feat: separate activity maps by waterbody

This commit is contained in:
ik
2026-09-22 20:00:08 +07:00
parent 274c8e7000
commit 96c29ab9d9
2 changed files with 42 additions and 19 deletions
+41 -18
View File
@@ -2,27 +2,47 @@
import { spotPath, type Activity } from "../lib/api";
const { items } = Astro.props as { items: Activity[] };
const xValues = items.map((item) => item.x);
const yValues = items.map((item) => item.y);
const minX = Math.min(...xValues, 0);
const maxX = Math.max(...xValues, 1);
const minY = Math.min(...yValues, 0);
const maxY = Math.max(...yValues, 1);
const xRange = Math.max(1, maxX - minX);
const yRange = Math.max(1, maxY - minY);
const column = (value: number) => Math.min(20, Math.max(1, Math.round(((value - minX) / xRange) * 18) + 1));
const row = (value: number) => Math.min(20, Math.max(1, Math.round((1 - (value - minY) / yRange) * 18) + 1));
const groups = [...items.reduce((result, item) => {
const group = result.get(item.waterbody_slug) ?? [];
group.push(item);
result.set(item.waterbody_slug, group);
return result;
}, new Map<string, Activity[]>()).values()];
const plotPoints = (group: Activity[]) => {
const minX = Math.min(...group.map(item => item.x), 0);
const maxX = Math.max(...group.map(item => item.x), 1);
const minY = Math.min(...group.map(item => item.y), 0);
const maxY = Math.max(...group.map(item => item.y), 1);
const xRange = Math.max(1, maxX - minX);
const yRange = Math.max(1, maxY - minY);
const occupied = new Set<string>();
return group.map(item => {
const targetColumn = Math.min(19, Math.max(2, Math.round(((item.x - minX) / xRange) * 16) + 2));
const targetRow = Math.min(19, Math.max(2, Math.round((1 - (item.y - minY) / yRange) * 16) + 2));
let column = targetColumn;
let row = targetRow;
for (let distance = 0; occupied.has(`${column}:${row}`) && distance < 20; distance += 1) {
column = ((targetColumn - 2 + distance + 1) % 18) + 2;
row = ((targetRow - 2 + Math.floor((distance + 1) / 18)) % 18) + 2;
}
occupied.add(`${column}:${row}`);
return { item, column, row };
});
};
---
<section class="activity-map" aria-label="Схема координат горячих точек">
<header class="activity-map__header">
<div><span class="overline">Режим схемы</span><h3>Горячие точки на координатной сетке</h3></div>
<p>Показаны подтверждённые координаты текущей выборки. Это схема `x:y`, а не географическая карта водоёма.</p>
<div><span class="overline">Режим схемы</span><h3>Горячие точки по водоёмам</h3></div>
<p>Для каждого водоёма своя координатная сетка `x:y`. Совпадающие точки разнесены, чтобы каждая оставалась доступной.</p>
</header>
<div class="activity-map__plot" role="group" aria-label={`Координатная схема, точек: ${items.length}`}>
<span class="activity-map__axis activity-map__axis--x" aria-hidden="true">x →</span>
<span class="activity-map__axis activity-map__axis--y" aria-hidden="true">y ↑</span>
{items.map((item) => <a class:list={["activity-map__point", `activity-map__point--col-${column(item.x)}`, `activity-map__point--row-${row(item.y)}`]} href={spotPath(item)} aria-label={`${item.waterbody}, ${item.x}:${item.y}, ${item.fish}`}><span>{item.x}:{item.y}</span><strong>{item.fish}</strong></a>)}
</div>
{groups.map(group => <section class="activity-map__group" aria-labelledby={`activity-map-${group[0].waterbody_slug}`}>
<h4 id={`activity-map-${group[0].waterbody_slug}`}>{group[0].waterbody} <small>{group.length} {group.length === 1 ? "точка" : "точки"}</small></h4>
<div class="activity-map__plot" role="group" aria-label={`Координатная схема водоёма ${group[0].waterbody}, точек: ${group.length}`}>
<span class="activity-map__axis activity-map__axis--x" aria-hidden="true">x →</span>
<span class="activity-map__axis activity-map__axis--y" aria-hidden="true">y ↑</span>
{plotPoints(group).map(({ item, column, row }) => <a class:list={["activity-map__point", `activity-map__point--col-${column}`, `activity-map__point--row-${row}`]} href={spotPath(item)} aria-label={`${item.waterbody}, ${item.x}:${item.y}, ${item.fish}`} title={`${item.waterbody} · ${item.x}:${item.y} · ${item.fish}`}><span>{item.x}:{item.y}</span><strong>{item.fish}</strong></a>)}
</div>
</section>)}
</section>
<style>
@@ -30,6 +50,9 @@ const row = (value: number) => Math.min(20, Math.max(1, Math.round((1 - (value -
.activity-map__header { display: flex; justify-content: space-between; gap: 22px; align-items: end; }
.activity-map__header h3 { margin: 6px 0 0; font: 400 27px Georgia, serif; letter-spacing: -.03em; }
.activity-map__header p { max-width: 330px; margin: 0; color: var(--text-muted); font-size: 12px; line-height: 1.5; }
.activity-map__group { display: grid; gap: 10px; }
.activity-map__group h4 { margin: 0; font: 400 21px Georgia, serif; }
.activity-map__group h4 small { margin-left: 8px; color: var(--text-muted); font: 700 10px Inter, sans-serif; text-transform: uppercase; letter-spacing: .08em; }
.activity-map__plot { position: relative; display: grid; grid-template-columns: repeat(20, minmax(0, 1fr)); grid-template-rows: repeat(20, minmax(0, 1fr)); min-height: 470px; overflow: hidden; border: 1px solid var(--border); border-radius: 20px; background: linear-gradient(90deg, color-mix(in srgb, var(--accent-muted) 8%, transparent) 1px, transparent 1px), linear-gradient(color-mix(in srgb, var(--accent-muted) 8%, transparent) 1px, transparent 1px), var(--surface-soft); background-size: 10% 10%; }
.activity-map__plot::after { content: ""; position: absolute; inset: 8%; border: 1px dashed color-mix(in srgb, var(--accent-muted) 40%, transparent); border-radius: 12px; pointer-events: none; }
.activity-map__axis { position: absolute; z-index: 1; color: var(--text-subtle); font-size: 10px; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; }
@@ -39,7 +62,7 @@ const row = (value: number) => Math.min(20, Math.max(1, Math.round((1 - (value -
.activity-map__point::before { content: ""; position: absolute; left: 50%; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid color-mix(in srgb, var(--accent-muted) 45%, var(--border)); border-bottom: 1px solid color-mix(in srgb, var(--accent-muted) 45%, var(--border)); background: var(--surface); transform: translateX(-50%) rotate(45deg); }
.activity-map__point:hover, .activity-map__point:focus-visible { border-color: var(--focus); transform: scale(1.03); }
.activity-map__point span { color: var(--text-muted); font-size: 10px; font-weight: 750; }
.activity-map__point strong { max-width: 130px; overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.activity-map__point strong { max-width: 180px; overflow-wrap: anywhere; font-size: 12px; line-height: 1.2; }
:global(.activity-map__point--col-1) { grid-column: 1; } :global(.activity-map__point--col-2) { grid-column: 2; } :global(.activity-map__point--col-3) { grid-column: 3; } :global(.activity-map__point--col-4) { grid-column: 4; } :global(.activity-map__point--col-5) { grid-column: 5; } :global(.activity-map__point--col-6) { grid-column: 6; } :global(.activity-map__point--col-7) { grid-column: 7; } :global(.activity-map__point--col-8) { grid-column: 8; } :global(.activity-map__point--col-9) { grid-column: 9; } :global(.activity-map__point--col-10) { grid-column: 10; } :global(.activity-map__point--col-11) { grid-column: 11; } :global(.activity-map__point--col-12) { grid-column: 12; } :global(.activity-map__point--col-13) { grid-column: 13; } :global(.activity-map__point--col-14) { grid-column: 14; } :global(.activity-map__point--col-15) { grid-column: 15; } :global(.activity-map__point--col-16) { grid-column: 16; } :global(.activity-map__point--col-17) { grid-column: 17; } :global(.activity-map__point--col-18) { grid-column: 18; } :global(.activity-map__point--col-19) { grid-column: 19; } :global(.activity-map__point--col-20) { grid-column: 20; }
:global(.activity-map__point--row-1) { grid-row: 1; } :global(.activity-map__point--row-2) { grid-row: 2; } :global(.activity-map__point--row-3) { grid-row: 3; } :global(.activity-map__point--row-4) { grid-row: 4; } :global(.activity-map__point--row-5) { grid-row: 5; } :global(.activity-map__point--row-6) { grid-row: 6; } :global(.activity-map__point--row-7) { grid-row: 7; } :global(.activity-map__point--row-8) { grid-row: 8; } :global(.activity-map__point--row-9) { grid-row: 9; } :global(.activity-map__point--row-10) { grid-row: 10; } :global(.activity-map__point--row-11) { grid-row: 11; } :global(.activity-map__point--row-12) { grid-row: 12; } :global(.activity-map__point--row-13) { grid-row: 13; } :global(.activity-map__point--row-14) { grid-row: 14; } :global(.activity-map__point--row-15) { grid-row: 15; } :global(.activity-map__point--row-16) { grid-row: 16; } :global(.activity-map__point--row-17) { grid-row: 17; } :global(.activity-map__point--row-18) { grid-row: 18; } :global(.activity-map__point--row-19) { grid-row: 19; } :global(.activity-map__point--row-20) { grid-row: 20; }
@media (max-width: 720px) { .activity-map__header { display: grid; gap: 8px; align-items: start; } .activity-map__plot { min-height: 390px; } .activity-map__point { min-width: 62px; padding: 7px 8px; } .activity-map__point strong { max-width: 90px; } }