docs(map): уточнены заголовки и добавлены образцы легенды

This commit is contained in:
ddadmin committed 2026-09-20 13:11:44 -04:00
1 parent b9bcfe7230
commit 6be0442838
4 files changed
+116 -41

No files matched your search

+49 -17
View File
@@ -53,11 +53,9 @@
outline: 3px solid #d79078;
outline-offset: 2px;
}
.legend { display: flex; flex-wrap: wrap; gap: 15px; color: var(--muted); }
.legend { display: flex; flex-wrap: wrap; gap: 8px 20px; color: var(--muted); }
.legend-item { display: inline-flex; align-items: center; gap: 7px; }
.legend-line { width: 34px; border-top: 2px solid var(--line); }
.legend-line.dashed { border-top-style: dashed; }
.legend-line.dotted { border-top-style: dotted; }
.legend-icon { display: block; width: 50px; height: 20px; flex: none; }
main {
display: grid;
grid-template-columns: minmax(0, 1fr) 370px;
@@ -71,7 +69,7 @@
border-radius: 10px;
background: var(--panel);
}
svg { display: block; width: 100%; min-width: 1040px; height: auto; }
#map { display: block; width: 100%; min-width: 1040px; height: auto; }
.layer-title { font-size: 15px; font-weight: 700; text-anchor: middle; }
.layer-caption { font-size: 11px; fill: var(--muted); text-anchor: middle; }
.zone-caption { font-size: 12px; fill: var(--muted); }
@@ -87,7 +85,7 @@
.edge-line.dotted { stroke-dasharray: 2 6; stroke-linecap: round; }
.edge-hit { fill: none; stroke: transparent; stroke-width: 14; cursor: pointer; }
.node { cursor: pointer; transition: opacity .15s; }
.node rect {
.node rect, .legend-node rect {
fill: #f1f2f2;
stroke: #858b90;
stroke-width: 1.2;
@@ -130,12 +128,12 @@
a { color: #8a351e; text-underline-offset: 2px; }
.hint { color: var(--muted); font-size: 13px; }
.edge[hidden] { display: none; }
.node.student rect { stroke-dasharray: 5 3; }
.node.student rect, .legend-node.student rect { stroke-dasharray: 5 3; }
.node.student .stream-mark { fill: #875a14; }
.edge.expected .edge-line { stroke-dasharray: 5 4; }
.node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; }
.node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; }
.node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); }
.edge.expected .edge-line, .legend-arrow.expected .edge-line { stroke-dasharray: 5 4; }
.node.is-upstream rect, .legend-node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; }
.node.is-downstream rect, .legend-node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; }
.node.is-selected rect, .legend-node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); }
.relation-button { border-radius: 5px; padding: 5px 7px; text-align: left; width: 100%; margin-top: 5px; font-size: 13px; }
.badge { font-size: 13px; padding: 3px 8px; border-radius: 4px; background: #eef1ed; display: inline-block; }
.badge.student { background: #fff1cf; color: #775018; }
@@ -155,9 +153,41 @@
<div class="controls">
<div class="switcher" id="switcher" aria-label="Вид связей"></div>
<p id="view-hint" class="hint"></p>
<div class="legend" aria-label="Обозначения">
<span>Сплошная рамка: готово</span><span>Пунктирная рамка: задание</span>
<span id="upstream-legend">Синий: источники</span><span id="downstream-legend">Зеленый: потребители</span><span>Оранжевый: выбор</span>
<div class="legend" aria-label="Обозначения стрелок">
<span class="legend-item">
<svg class="legend-icon legend-arrow" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span id="ready-edge-legend">Готовая загрузка</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-arrow expected" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span id="expected-edge-legend">Загрузка по ТЗ</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-arrow is-path" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span>Выделенные связи</span>
</span>
</div>
<div class="legend" aria-label="Обозначения таблиц">
<span class="legend-item">
<svg class="legend-icon legend-node" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Готовая таблица</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node student" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Учебная заглушка</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-upstream" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span id="upstream-legend">Источники</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-downstream" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span id="downstream-legend">Потребители</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-selected" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Выбранная таблица</span>
</span>
</div>
</div>
<main>
@@ -5142,9 +5172,11 @@
}
function renderState(message = "") {
document.querySelectorAll("[data-view]").forEach(b => b.setAttribute("aria-pressed",String(b.dataset.view === state.view)));
document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Синий: источники" : "Синий: связанные родители";
document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Зеленый: потребители" : "Зеленый: связанные потомки";
document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Пунктир — ожидается по ТЗ. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK.";
document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Источники" : "Связанные родители";
document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Потребители" : "Связанные потомки";
document.getElementById("ready-edge-legend").textContent = state.view === "flow" ? "Готовая загрузка" : "Связь по ключам";
document.getElementById("expected-edge-legend").textContent = state.view === "flow" ? "Загрузка по ТЗ" : "Связь с учебной заглушкой";
document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK.";
if (state.selected?.type === "node") renderNodeCard(nodeById.get(state.selected.id));
else if (state.selected?.type === "edge") renderEdgeCard(edgeById.get(state.selected.id));
else renderIntro(message);
+49 -17
View File
@@ -52,11 +52,9 @@
outline: 3px solid #d79078;
outline-offset: 2px;
}
.legend { display: flex; flex-wrap: wrap; gap: 15px; color: var(--muted); }
.legend { display: flex; flex-wrap: wrap; gap: 8px 20px; color: var(--muted); }
.legend-item { display: inline-flex; align-items: center; gap: 7px; }
.legend-line { width: 34px; border-top: 2px solid var(--line); }
.legend-line.dashed { border-top-style: dashed; }
.legend-line.dotted { border-top-style: dotted; }
.legend-icon { display: block; width: 50px; height: 20px; flex: none; }
main {
display: grid;
grid-template-columns: minmax(0, 1fr) 370px;
@@ -70,7 +68,7 @@
border-radius: 10px;
background: var(--panel);
}
svg { display: block; width: 100%; min-width: 1040px; height: auto; }
#map { display: block; width: 100%; min-width: 1040px; height: auto; }
.layer-title { font-size: 15px; font-weight: 700; text-anchor: middle; }
.layer-caption { font-size: 11px; fill: var(--muted); text-anchor: middle; }
.zone-caption { font-size: 12px; fill: var(--muted); }
@@ -86,7 +84,7 @@
.edge-line.dotted { stroke-dasharray: 2 6; stroke-linecap: round; }
.edge-hit { fill: none; stroke: transparent; stroke-width: 14; cursor: pointer; }
.node { cursor: pointer; transition: opacity .15s; }
.node rect {
.node rect, .legend-node rect {
fill: #f1f2f2;
stroke: #858b90;
stroke-width: 1.2;
@@ -129,12 +127,12 @@
a { color: #8a351e; text-underline-offset: 2px; }
.hint { color: var(--muted); font-size: 13px; }
.edge[hidden] { display: none; }
.node.student rect { stroke-dasharray: 5 3; }
.node.student rect, .legend-node.student rect { stroke-dasharray: 5 3; }
.node.student .stream-mark { fill: #875a14; }
.edge.expected .edge-line { stroke-dasharray: 5 4; }
.node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; }
.node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; }
.node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); }
.edge.expected .edge-line, .legend-arrow.expected .edge-line { stroke-dasharray: 5 4; }
.node.is-upstream rect, .legend-node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; }
.node.is-downstream rect, .legend-node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; }
.node.is-selected rect, .legend-node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); }
.relation-button { border-radius: 5px; padding: 5px 7px; text-align: left; width: 100%; margin-top: 5px; font-size: 13px; }
.badge { font-size: 13px; padding: 3px 8px; border-radius: 4px; background: #eef1ed; display: inline-block; }
.badge.student { background: #fff1cf; color: #775018; }
@@ -154,9 +152,41 @@
<div class="controls">
<div class="switcher" id="switcher" aria-label="Вид связей"></div>
<p id="view-hint" class="hint"></p>
<div class="legend" aria-label="Обозначения">
<span>Сплошная рамка: готово</span><span>Пунктирная рамка: задание</span>
<span id="upstream-legend">Синий: источники</span><span id="downstream-legend">Зеленый: потребители</span><span>Оранжевый: выбор</span>
<div class="legend" aria-label="Обозначения стрелок">
<span class="legend-item">
<svg class="legend-icon legend-arrow" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span id="ready-edge-legend">Готовая загрузка</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-arrow expected" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span id="expected-edge-legend">Загрузка по ТЗ</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-arrow is-path" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span>Выделенные связи</span>
</span>
</div>
<div class="legend" aria-label="Обозначения таблиц">
<span class="legend-item">
<svg class="legend-icon legend-node" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Готовая таблица</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node student" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Учебная заглушка</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-upstream" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span id="upstream-legend">Источники</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-downstream" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span id="downstream-legend">Потребители</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-selected" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Выбранная таблица</span>
</span>
</div>
</div>
<main>
@@ -393,9 +423,11 @@
}
function renderState(message = "") {
document.querySelectorAll("[data-view]").forEach(b => b.setAttribute("aria-pressed",String(b.dataset.view === state.view)));
document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Синий: источники" : "Синий: связанные родители";
document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Зеленый: потребители" : "Зеленый: связанные потомки";
document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Пунктир — ожидается по ТЗ. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK.";
document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Источники" : "Связанные родители";
document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Потребители" : "Связанные потомки";
document.getElementById("ready-edge-legend").textContent = state.view === "flow" ? "Готовая загрузка" : "Связь по ключам";
document.getElementById("expected-edge-legend").textContent = state.view === "flow" ? "Загрузка по ТЗ" : "Связь с учебной заглушкой";
document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK.";
if (state.selected?.type === "node") renderNodeCard(nodeById.get(state.selected.id));
else if (state.selected?.type === "edge") renderEdgeCard(edgeById.get(state.selected.id));
else renderIntro(message);
+15 -5
View File
@@ -60,7 +60,9 @@ Docker для карты не нужны. Храните HTML рядом с ос
Для связи сегмента с талоном понадобятся оба поля ключа, иначе талон
первого рейса можно ошибочно присоединить ко второму.
## Как устроен поток целиком?
<a id="как-устроен-поток-целиком"></a>
## Путь данных от источника до витрин
Оставьте билет выбранным и переключитесь на **"Поток данных"**.
Таблицы останутся на местах, выбранный билет сохранится; теперь стрелки
@@ -86,7 +88,9 @@ Docker для карты не нужны. Храните HTML рядом с ос
проверки календаря. Порядок выполнения смотрите по ссылке **"DAG · порядок задач"**
в карточке. Поля, SQL и проверки раскрываются там же, после выбора таблицы.
## Как собирается факт?
<a id="как-собирается-факт"></a>
## Сборка факта продаж
Откройте [dds.fact_flight_sales](architecture-map.html#node=dds.fact_flight_sales).
В нашем примере в нем будет **три строки**, по одной на каждый сегмент.
@@ -111,7 +115,9 @@ SK здесь обозначены словами: конкретные числ
Нет талона - сегмент сохраняется, `seat_no` остается `NULL`, `is_boarded = false`.
В этом стенде признак посадки означает именно наличие талона.
### Откуда берутся два аэропорта
<a id="откуда-берутся-два-аэропорта"></a>
### Аэропорты вылета и прилета
В карточке факта откройте связи **"Коды двух аэропортов"**,
**"Аэропорт вылета"** и **"Аэропорт прилета"**:
@@ -128,7 +134,9 @@ SK здесь обозначены словами: конкретные числ
SQL использует `LEFT JOIN`, поэтому строка с таким пропуском сохранится;
допустимость пропуска отдельно оценивает [DQ факта](../../sql/dds/fact_flight_sales_dq.sql).
### Какая версия маршрута нужна рейсу
<a id="какая-версия-маршрута-нужна-рейсу"></a>
### Версия маршрута на дату рейса
Связь **"Версия на дату рейса"** ведет к `dds.dim_routes` по `route_bk = route_no`
и дате `scheduled_departure::DATE`. Дата должна попасть в полуоткрытый
@@ -139,7 +147,9 @@ SQL использует `LEFT JOIN`, поэтому строка с таким
поиска `airplane_sk` в `dim_airplanes`. Поэтому одного заполненного справочника
моделей недостаточно: без подходящей версии маршрута `airplane_sk` не появится.
## Что кормит витрины?
<a id="что-кормит-витрины"></a>
## Агрегация фактов в витрине продаж
Перейдите к [dm.sales_report](architecture-map.html#node=dm.sales_report).
Зерно меняется: день вылета × два аэропорта × класс обслуживания.
+3 -2
View File
@@ -43,8 +43,9 @@
11. `LEFT JOIN dds.dim_passengers` даёт `passenger_sk`.
12. `LEFT JOIN ods.boarding_passes` даёт `seat_no` и признак `is_boarded`.
Так список связей превращается в [схему «Как собирается факт?»](db_schema.md#как-собирается-факт):
четыре обязательных входа рисуются сплошными стрелками, а остальные пунктирными.
Этот путь разобран в разделе [«Сборка факта продаж»](db_schema.md#сборка-факта-продаж).
На карте сплошные стрелки показывают готовые загрузки, пунктирные — ожидаемые
по ТЗ. Тип соединения и поведение при отсутствии строки описаны в карточке связи.
## Как проверить свой слой