Выгрузка карты из памяти + фиксы столбца адресов

1) Карта выгружается (map.destroy()) через 2с после ухода в режим «График»
   — освобождает тайлы/canvas/GPU (~350–450 МБ); отменяется при возврате.
   Метод MapView.destroy() (сброс map/objectManager/initPromise/pending);
   в onModeChange сброс mapLoadedKey, иначе refreshMap короткозамыкал render и
   свежий ObjectManager оставался пустым.

2) MIN_LABEL_W 140→200 — три пилюли Город/Адрес/Код не обрезаются на узком
   столбце; container-query прячет подпись «Показывать» при ширине < 255px.

3) Правый/левый отступ 7px у содержимого строки (#timeline .vis-label .vis-inner,
   специфичность выше собственного padding vis-timeline) — адрес не липнет к
   границе столбца.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
aaverbitskiy 2026-08-18 05:33:30 +00:00
parent 17b70b1f44
commit f0c53e2ce9
5 changed files with 44 additions and 5 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "mapdash-frontend", "name": "mapdash-frontend",
"private": true, "private": true,
"version": "1.0.1", "version": "1.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -420,10 +420,25 @@ async function refreshMap(): Promise<void> {
} }
} }
let mapDestroyTimer = 0;
async function onModeChange(mode: ViewMode): Promise<void> { async function onModeChange(mode: ViewMode): Promise<void> {
if (mode !== 'map') view.applyLayout(); // timeline visible (timeline or split) if (mode !== 'map') view.applyLayout(); // timeline visible (timeline or split)
if (mode === 'timeline') mapView.closePanel(); // map hidden — drop its floating card if (mode === 'timeline') {
if (mode !== 'timeline') await refreshMap(); // map visible (map or split) mapView.closePanel(); // map hidden — drop its floating card
// Free the Yandex map (tiles/canvas/GPU — the bulk of the tab's memory) a
// couple of seconds after it's closed; cancelled if a map view returns, so
// quick График⇄Карта toggles don't thrash a re-init.
window.clearTimeout(mapDestroyTimer);
mapDestroyTimer = window.setTimeout(() => {
mapView.destroy();
// The fresh map/ObjectManager will be empty, so drop the "already loaded"
// key — the next open re-fetches + re-renders instead of short-circuiting.
mapLoadedKey = '';
}, 2000);
} else {
window.clearTimeout(mapDestroyTimer); // map visible again — keep it
await refreshMap(); // map visible (map or split); ensureInit rebuilds if destroyed
}
} }
const viewModes = createViewModes( const viewModes = createViewModes(

View File

@ -63,6 +63,9 @@ export interface MapView {
ensureInit(): Promise<void>; ensureInit(): Promise<void>;
invalidateSize(): void; invalidateSize(): void;
closePanel(): void; closePanel(): void;
/** Destroy the Yandex map and free its tiles/canvas/GPU memory. The next
* ensureInit() re-creates it from scratch. */
destroy(): void;
/** Pan + zoom the map to the surface's location (its cluster). No-op if it has no coords. */ /** Pan + zoom the map to the surface's location (its cluster). No-op if it has no coords. */
focusBoard(boardId: string): void; focusBoard(boardId: string): void;
} }
@ -633,6 +636,20 @@ export function createMapView(els: MapElements, apiKey: string): MapView {
closePanel(): void { closePanel(): void {
closeMapPanel(); closeMapPanel();
}, },
destroy(): void {
closeMapPanel();
// map.destroy() tears down Yandex's DOM inside #map and releases its tile
// cache / canvas / GPU textures. Our legend/counter overlays are siblings
// of #map, so they survive. Reset state so ensureInit() rebuilds cleanly.
if (map) {
try { map.destroy(); } catch { /* already torn down */ }
}
map = null;
objectManager = null;
initPromise = null;
pendingFocus = null;
pending = null;
},
focusBoard(boardId: string): void { focusBoard(boardId: string): void {
pendingFocus = boardId; pendingFocus = boardId;
applyFocus(); // applies now if already rendered, else waits for the next draw applyFocus(); // applies now if already rendered, else waits for the next draw

View File

@ -327,8 +327,11 @@ input::placeholder { color: var(--text-muted); }
display: flex; align-items: center; gap: 6px; display: flex; align-items: center; gap: 6px;
padding: 0 12px; box-sizing: border-box; overflow: hidden; padding: 0 12px; box-sizing: border-box; overflow: hidden;
background: var(--bg); border-bottom: 1px solid var(--border); background: var(--bg); border-bottom: 1px solid var(--border);
container-type: inline-size;
} }
.ci-label { flex: 0 0 auto; font-size: 11px; color: var(--text-muted); white-space: nowrap; } .ci-label { flex: 0 0 auto; font-size: 11px; color: var(--text-muted); white-space: nowrap; }
/* Narrow column: drop the "Показывать" label so the three pills still fit. */
@container (max-width: 255px) { #chart-wrap #col-info .ci-label { display: none; } }
.ci-toggle { .ci-toggle {
flex: 0 0 auto; border: none; cursor: pointer; white-space: nowrap; flex: 0 0 auto; border: none; cursor: pointer; white-space: nowrap;
font-size: 12px; font-weight: 500; padding: 5px 11px; border-radius: 999px; font-size: 12px; font-weight: 500; padding: 5px 11px; border-radius: 999px;
@ -770,10 +773,14 @@ html.role-pending .manager-only { display: none !important; }
/* Row hover */ /* Row hover */
.vis-label.row-hover, .vis-group.row-hover { background-color: var(--row-hover) !important; } .vis-label.row-hover, .vis-group.row-hover { background-color: var(--row-hover) !important; }
/* Fixed/resizable label column */ /* Fixed/resizable label column */
.vis-label .vis-inner { /* #timeline lifts specificity above vis-timeline's own `.vis-labelset .vis-label
.vis-inner { padding: 5px }`, so the right padding actually applies. */
#timeline .vis-label .vis-inner {
display: inline-block; display: inline-block;
box-sizing: border-box;
min-width: var(--label-w); min-width: var(--label-w);
max-width: var(--label-w); max-width: var(--label-w);
padding-left: 7px; padding-right: 7px; /* keep the address off the column borders */
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle;
} }

View File

@ -54,7 +54,7 @@ export const zoomSteps: ReadonlyArray<{ days: number; label: string }> = [
const MONTH_BG_ID = '__month_hover_bg'; const MONTH_BG_ID = '__month_hover_bg';
const CUR_MONTH_BG_ID = '__cur_month_bg'; const CUR_MONTH_BG_ID = '__cur_month_bg';
const MIN_LABEL_W = 140; const MIN_LABEL_W = 200; // fits the three column-info pills (~188px) without clipping
// Uniform 3-letter month labels (moment's ru `MMM` mixes full/abbreviated with // Uniform 3-letter month labels (moment's ru `MMM` mixes full/abbreviated with
// dots — "март", "май", "нояб.", "дек."). scale is 'month' at every zoom step. // dots — "март", "май", "нояб.", "дек."). scale is 'month' at every zoom step.