Шаблон портфоліо для курсу «Візуалізація даних»

This commit is contained in:
2026-09-06 23:10:16 +03:00
commit 1abe7e2468
10 changed files with 1438 additions and 0 deletions
+373
View File
@@ -0,0 +1,373 @@
# Портфоліо · шаблон
Сайт для ваших робіт із курсу «Візуалізація даних». Читач відкриває
посилання і бачить роботи одразу.
Сайт вбудовує графіки й дашборди з **Tableau Public** і специфікації
**Vega-Lite**. Кількість графіків в одній роботі не обмежена.
Одна робота — один файл у папці `_works/`. Текст пишете звичайним
Markdown, графік ставите одним рядком. HTML не потрібен.
Програм ставити не треба. Усі кроки робіть у браузері.
> **УВАГА. Сайт публічний.** Його бачить будь-хто в інтернеті, і на
> ньому стоїть ваше ім'я.
>
> 1. Беріть навчальні або відкриті дані.
> 2. Якщо графік зроблено на робочих даних, заберіть назви клієнтів
> і справжні суми.
> 3. У Tableau Public вимкніть **Allow Access**. Інакше книгу з
> даними можна завантажити.
---
## Крок 1. Зробіть із шаблону свій репозиторій
1. Угорі натисніть зелену кнопку **Use this template**.
2. Виберіть **Create a new repository**.
3. У полі **Repository name** напишіть `dataviz-portfolio`.
4. Виберіть **Public**. Приватний репозиторій GitHub не публікує.
5. Натисніть **Create repository**.
Далі працюйте у своєму репозиторії, а не в цьому.
## Крок 2. Увімкніть публікацію
1. У своєму репозиторії відкрийте вкладку **Settings**.
2. Зліва виберіть **Pages**.
3. У полі **Source** виберіть `Deploy from a branch`.
4. У полі **Branch** виберіть `main` і папку `/ (root)`.
5. Натисніть **Save**.
6. Зачекайте одну-дві хвилини й оновіть **Settings → Pages**.
Угорі з'явиться адреса вашого сайту:
```
https://ВАШ-ЛОГІН.github.io/dataviz-portfolio/
```
Відкрийте адресу. На сайті стоять дві роботи-приклади і список робіт
у бічній панелі. Приклади — це файли в папці `_works/`. Видаліть їх,
коли додасте свої роботи.
## Крок 3. Замініть ім'я на сайті
Ім'я стоїть тільки у файлі `_config.yml`.
1. Відкрийте `_config.yml`.
2. Замініть `title`, `role` і `tagline`.
3. Натисніть **Commit changes**.
```yaml
title: "Олена Ковальчук"
role: "Аналітик даних"
tagline: "Роботи з курсу «Візуалізація даних» KSE, 2026."
```
Сайт оновиться через хвилину.
Лапки навколо тексту обов'язкові. Усередині тексту беріть лапки «такі».
Якщо копію зроблено до 7 вересня 2026 і на головній сторінці лишилось
«Ім'я Прізвище», ім'я стоїть ще в одному місці: відкрийте `index.md`
і в рядку `heading` замініть «Ім'я Прізвище» на своє.
---
## Додайте роботу
Одна робота — один файл у папці `_works/`. Графік стоїть у тексті
роботи, на своєму місці.
1. Відкрийте папку `_works`.
2. Натисніть **Add file → Create new file**.
3. Напишіть ім'я файлу латиницею, з `.md` на кінці: `03-facets.md`.
4. Вставте текст нижче і замініть його своїм.
5. Натисніть **Commit changes**.
```markdown
---
title: "Назва роботи"
---
**Питання:** на яке питання відповідає графік.
![Підпис під графіком](https://public.tableau.com/app/profile/ваш.профіль/viz/Книга/Аркуш)
**Рішення:** що ви вирішили і чому.
**Що видно:** що з цього побачив читач.
```
Роботи стоять на сторінці за іменем файлу, за абеткою. Ставте число
попереду: `01-`, `02-`, `03-`. Пункт у бічній панелі з'являється сам.
Найсильнішу роботу ставте першою: її побачать усі.
### Три способи поставити графік
Кожен спосіб — один рядок у тексті роботи.
#### Графік із Tableau Public
Скопіюйте адресу з адресного рядка і поставте її в текст:
```markdown
![Підпис під графіком](https://public.tableau.com/app/profile/ваш.профіль/viz/Книга/Аркуш)
```
#### Специфікація в тексті роботи
Файл завантажувати не треба. Вставте специфікацію між двома рядками
з трьох зворотних апострофів. На першому рядку допишіть `vega-lite`:
````markdown
```vega-lite
{
"mark": "bar",
"data": { "values": [ { "місяць": "Січень", "продажі": 28 } ] },
"encoding": {
"x": { "field": "місяць", "type": "nominal" },
"y": { "field": "продажі", "type": "quantitative" }
}
}
```
````
#### Специфікація окремим файлом
Так робіть, коли специфікація довга або потрібна в кількох роботах.
1. Відкрийте папку `specs`.
2. Натисніть **Add file → Upload files**.
3. Перетягніть файл. Ім'я — латиницею, без пробілів.
4. Натисніть **Commit changes**.
5. Поставте в текст роботи рядок:
```markdown
![Підпис під графіком](specs/my-chart.json)
```
Текст у квадратних дужках стає підписом під графіком. Якщо підпис не
потрібен, лишіть дужки порожніми: `![](specs/my-chart.json)`.
### Поставте кілька графіків в одній роботі
Кожен графік стоїть у тексті на своєму місці. Кількість не обмежена.
Два графіки стають в **один ряд**, коли їхні рядки йдуть підряд, без
порожнього рядка між ними:
```markdown
![Ліворуч](specs/chart-a.json)
![Праворуч](specs/chart-b.json)
```
Ряд ставте тоді, коли читач має порівняти два графіки. Порожній
рядок між ними дає два окремі графіки, один під одним. На вузькому
екрані ряд теж розпадається на два графіки один під одним.
Усі чотири записи стоять поруч у роботі-прикладі
`_works/02-embedding.md`. Заготовка для власної роботи —
`_works/01-template.md`.
### Ширина і висота
Графік займає ширину колонки тексту.
Специфікація без поля `width` підлаштовується під колонку. Якщо поле
`width` є, графік лишається такої ширини. Якщо графік ширший за
колонку, з'являється горизонтальна прокрутка.
Дашборд Tableau ширший і вищий за колонку. Допишіть у лапках після
адреси слово `wide` і висоту в пікселях:
```markdown
![Дашборд продажів](https://public.tableau.com/…/viz/Книга/Дашборд "wide 1150")
```
Слово `wide` виводить графік за межі колонки тексту. Число задає
висоту рамки. Якщо низ дашборда обрізаний, збільшуйте число.
### Багато правок за раз: github.dev
Через сайт github.com зручно правити один файл. Коли файлів кілька,
відкрийте **github.dev** — той самий редактор VS Code, тільки
в браузері. Ставити нічого не треба.
1. Відкрийте свій репозиторій на github.com.
2. Натисніть клавішу **`.`** (крапка). Або замініть у адресі
`github.com` на `github.dev`.
3. Зліва — дерево файлів, посередині — редактор.
4. Правки відправте в панелі **Source Control** (третя іконка зліва):
напишіть повідомлення і натисніть **Commit & Push**.
Так за один коміт можна змінити кілька робіт, перейменувати файли
і знайти текст по всьому репозиторію одразу
(`Ctrl`/`Cmd` + `Shift` + `F`).
github.dev — редактор, а не сайт: графіків він не показує.
Щоб перевірити специфікацію до коміту, вставте її у
[Vega Editor](https://vega.github.io/editor/). Сторінку видно
через хвилину після коміту.
### Поля в шапці роботи
Шапка стоїть між двома рядками з трьох дефісів, на початку файлу.
| Поле | Навіщо |
|---|---|
| `title` | заголовок роботи. Обов'язкове поле |
| `wide: true` | робота на всю ширину сторінки. Для дашбордів |
| `example: true` | позначка «приклад» і рядок «замініть своєю роботою». У своїх роботах не пишіть |
Графіки в шапці не описують. Графік стоїть у тексті роботи.
Робота на сторінці згортається. За замовчуванням усі роботи
розгорнуті. У бічній панелі є кнопка «Згорнути всі».
Текст під шапкою — звичайний Markdown. Жирний шрифт, списки й
посилання працюють.
### Перед тим як вбудувати графік Tableau
1. Відкрийте графік на Tableau Public.
2. Натисніть шестерню у верхньому правому куті.
3. Вимкніть **Show Sheets**. Інакше поруч із графіком видно всі
аркуші книги, разом із чернетками.
### Дашборд ширший за графік
Дашборд не вміщується в колонку тексту. Виправте розмір у Tableau:
1. Відкрийте дашборд у Tableau.
2. У полі **Size** виберіть **Automatic**.
Якщо розмір змінити не можна, допишіть `wide` і висоту в лапках після
адреси. Дивіться розділ «Ширина і висота».
---
## Додайте ще одну сторінку
Наприклад, сторінку `about.md` — про себе.
1. Натисніть **Add file → Create new file**. Ім'я — `about.md`.
2. Вставте текст:
```markdown
---
heading: "Про мене"
---
Аналітик у ритейлі. Тут коротко про досвід і контакти.
```
3. Натисніть **Commit changes**.
4. Відкрийте `_config.yml` і допишіть сторінку в список `nav`:
```yaml
nav:
- name: "Про мене"
file: "about.html"
```
5. Натисніть **Commit changes**.
Графік на такій сторінці ставте тим самим рядком, що й у роботі:
```markdown
![Підпис під графіком](specs/my-chart.json)
```
У полі `file` пишіть ім'я з `.html`, навіть якщо файл має `.md`.
Поки список `nav` порожній, у бічній панелі немає розділу сторінок.
Після першої сторінки поруч з'явиться посилання «Роботи» на головну.
---
## Якщо щось не показується
Спершу відкрийте вкладку **Actions** у своєму репозиторії. Зелена
галочка означає, що сайт зібрано. Червоний хрестик означає помилку
в якомусь файлі. Тоді сайт лишається таким, як був до помилки.
| Що бачите | Що робити |
|---|---|
| Червоний хрестик в **Actions** | Відкрийте останній запис і прочитайте рядок з `Error:`. Найчастіша причина — лапки в полі `title` |
| «Не вдалося прочитати адресу Tableau» | Відкрийте графік на Tableau Public і скопіюйте адресу з адресного рядка заново |
| У рамці англійське «Where's the viz?» | Адресу прочитано, графіка за нею немає. Книгу або аркуш перейменовано, приховано чи видалено |
| «Файл … не знайдено» | Перевірте, що файл лежить у папці `specs`, а ім'я в рядку збігається. Великі й малі літери мають значення |
| «Специфікація не читається як JSON» | Вставте специфікацію у Vega Editor. Він покаже, де зайва або пропущена кома |
| «Vega-Lite відмовилась малювати» | Специфікація читається, але Vega-Lite не розуміє полів. Текст помилки стоїть у самій підказці |
| У вкладці **Preview** на GitHub замість графіка значок битої картинки | Так і має бути. GitHub малює тільки текст. Графік з'являється тільки на самому сайті |
| На місці графіка порожньо | Адреса має закінчуватись на `.json` або вести на `public.tableau.com`. Інші адреси лишаються звичайними картинками |
| Два графіки стали один під одним | Між їхніми рядками є порожній рядок. Приберіть його |
| Дашборд обрізаний | Виберіть **Size → Automatic** у Tableau або допишіть `"wide 1150"` після адреси |
| Робота не з'явилась | Перевірте, що файл лежить у папці `_works` і має `.md` на кінці |
| Роботи стоять не в тому порядку | Роботи йдуть за іменем файлу. Перейменуйте: `01-`, `02-`, `03-` |
| Сайт не оновився | Зачекайте хвилину й оновіть сторінку через `Ctrl`/`Cmd` + `Shift` + `R` |
### Спитайте ШІ
Скопіюйте текст нижче в ChatGPT, Claude або Gemini. Текст описує,
як влаштований сайт, тому модель відповідатиме по суті. Свою
проблему допишіть у кінці.
```text
Допоможи розібратися з моїм сайтом-портфоліо. Відповідай українською,
покроково, і пам'ятай: я працюю тільки через сайт github.com у
браузері — через github.com і редактор github.dev, без командного
рядка і без програм на комп'ютері.
Як влаштований сайт:
- Це GitHub Pages з Jekyll. Репозиторій публічний, гілка main, папка
публікації — корінь.
- Налаштування сайту в _config.yml: title, role, tagline, footer і
список сторінок nav (у кожної name і file).
- Головна сторінка — index.md, вона показує всі роботи одразу.
Розмітка одна: _layouts/default.html.
- Кожна робота портфоліо — окремий файл у папці _works/. У його шапці
між --- стоїть title і необов'язкові wide та example. Полів для
графіків у шапці немає.
- Текст роботи під шапкою — звичайний Markdown. Графік стоїть просто
в тексті одним рядком:
![Підпис](specs/файл.json) — специфікація Vega-Lite з папки specs/;
![Підпис](https://public.tableau.com/…) — графік Tableau Public;
блок ```vega-lite з JSON усередині — специфікація прямо в тексті.
Два таких рядки без порожнього рядка між ними стають парою графіків
в один ряд. У лапках після адреси можна дописати wide і число
(висота рамки Tableau).
- Файл assets/embed.js знаходить ці рядки вже в браузері й замінює їх
на графіки. Бібліотеки Vega-Lite він вантажить сам.
- Кожна робота на сторінці — це <details class="work">, тобто її можна
згорнути. Розгорнуті за замовчуванням.
- Оформлення в assets/style.css, вбудовування графіків — у
assets/embed.js. Ці два файли я не змінюю.
- Сторінка сама збирає список робіт і бічну панель.
Що мені треба:
[опишіть, що ви робили, що очікували побачити і що побачили насправді]
Текст помилки з вкладки Actions, якщо він є:
[вставте сюди]
```
Якщо модель радить поставити Ruby, Jekyll чи Node або запустити щось
у терміналі, скажіть їй, що ви працюєте тільки в браузері на
github.com. Попросіть інший шлях.
---
## Що в папці
| Файл або папка | Навіщо |
|---|---|
| `_config.yml` | ім'я, підпис, список сторінок |
| `_works/` | ваші роботи, по одному файлу на кожну |
| `specs/` | файли специфікацій Vega-Lite |
| `index.md` | сторінка з роботами. Її бачать першою |
| `_layouts/default.html` | спільна розмітка сторінок. Правити не треба |
| `assets/style.css` | оформлення. Кольори — у змінних на початку файлу |
| `assets/embed.js` | вбудовує графіки. Правити не треба |
+27
View File
@@ -0,0 +1,27 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ НАЛАШТУВАННЯ САЙТУ ║
# ║ Ім'я стоїть тільки тут. Замінили — змінилося на всіх сторінках. ║
# ╚══════════════════════════════════════════════════════════════════╝
title: "Ім'я Прізвище"
role: "Аналітик даних"
tagline: "Аналітик даних. Тут зібрані мої роботи з курсу «Візуалізація даних» KSE, 2026: від перших графіків у Tableau до інтерактивних специфікацій."
footer: "Зроблено на курсі «Візуалізація даних» · Kyiv School of Economics"
# Додаткові сторінки. Поки список порожній, у панелі їх немає.
# Додали сторінку — допишіть її сюди, і посилання з'являться.
nav: []
# Нижче — службове. Правити не треба.
collections:
works:
output: false
defaults:
- scope:
path: ""
values:
layout: "default"
exclude:
- README.md
+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if page.heading %}{{ page.heading }} · {% endif %}{{ site.title }}</title>
{%- if page.noindex %}
<meta name="robots" content="noindex">
{%- endif %}
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<div class="layout">
<aside class="side">
<a class="side-name" href="index.html">{{ site.title }}</a>
<p class="side-role">{{ site.role }}</p>
{%- if site.nav.size > 0 %}
<nav class="side-nav">
<a href="index.html"{% if page.url == "/index.html" %} class="here"{% endif %}>Роботи</a>
{%- for item in site.nav %}
<a href="{{ item.file }}"{% if page.url contains item.file %} class="here"{% endif %}>{{ item.name }}</a>
{%- endfor %}
</nav>
{%- endif %}
{%- if page.charts and site.works.size > 0 %}
<nav class="side-works">
{%- for work in site.works %}
<a href="#work-{{ work.slug }}">{{ work.title }}</a>
{%- endfor %}
</nav>
{%- endif %}
</aside>
<div class="content">
<header class="page-head">
<h1>{{ page.heading | default: site.title }}</h1>
{%- if page.show_tagline %}
<p class="tagline">{{ site.tagline }}</p>
{%- endif %}
</header>
<main>
{{ content }}
</main>
<footer class="page-foot">
<p>{{ site.footer }}</p>
</footer>
</div><!-- .content -->
</div><!-- .layout -->
<!-- Вбудовує графіки. Бібліотеки Vega-Lite цей файл вантажить сам,
і тільки тоді, коли на сторінці є специфікація. -->
<script src="assets/embed.js"></script>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
---
title: "Назва роботи"
example: true
---
**Питання.** На яке питання відповідає графік.
![Підпис під графіком](specs/demo-bar.vl.json)
**Рішення.** Що ви вирішили і чому саме так.
**Що видно.** Що з цього побачив читач.
+48
View File
@@ -0,0 +1,48 @@
---
title: "Як поставити графік у текст"
example: true
---
Ця робота показує чотири записи. Кожен — один рядок у тексті.
**Специфікація з теки `specs`.**
![Підпис під графіком](specs/demo-bar.vl.json)
**Два графіки в одному ряду.** Два рядки підряд, без порожнього рядка
між ними.
![Ліворуч](specs/demo-bar.vl.json)
![Праворуч](specs/demo-line.vl.json)
**Специфікація просто в тексті.** Файл не потрібен.
```vega-lite
{
"title": { "text": "Графік, написаний у тексті роботи", "anchor": "start", "fontSize": 15 },
"height": 160,
"data": {
"values": [
{ "крок": "Крок 1", "значення": 12 },
{ "крок": "Крок 2", "значення": 26 },
{ "крок": "Крок 3", "значення": 19 }
]
},
"mark": { "type": "bar", "color": "#003964" },
"encoding": {
"x": {
"field": "крок",
"type": "nominal",
"title": null,
"sort": null,
"axis": { "labelAngle": 0 }
},
"y": { "field": "значення", "type": "quantitative", "title": null }
}
}
```
**Графік із Tableau Public.** Нижче стоїть приклад Tableau,
не моя робота. Замініть адресу своєю.
![Приклад Tableau Public](https://public.tableau.com/views/RegionalSampleWorkbook/Storms "650")
+380
View File
@@ -0,0 +1,380 @@
/* Вбудовує графіки у сторінку.
*
* Цей файл правити не треба. Графік ставиться в тексті роботи одним
* рядком, там, де він потрібен:
*
* ![Підпис](specs/файл.json) — специфікація Vega-Lite
* ![Підпис](https://public.tableau.com/…) — графік Tableau Public
* ```vega-lite … ``` — специфікація просто в тексті
*
* Два таких рядки поруч, без порожнього рядка між ними, стають парою
* графіків в один ряд. У дужках після адреси можна дописати wide
* (на всю ширину) і число (висота рамки Tableau).
*
* Блок із помилкою показує підказку і не ламає решту сторінки.
*/
(function () {
"use strict";
/* Бібліотеки Vega-Lite. Вантажаться, лише коли на сторінці є
специфікація: сторінці з самим Tableau вони не потрібні. */
var VEGA = [
"https://cdn.jsdelivr.net/npm/vega@5",
"https://cdn.jsdelivr.net/npm/vega-lite@5",
"https://cdn.jsdelivr.net/npm/vega-embed@6"
];
function box(el, kind, title, hint) {
var d = document.createElement("div");
d.className = "viz-msg viz-msg--" + kind;
d.innerHTML = "<b></b><span></span>";
d.querySelector("b").textContent = title;
d.querySelector("span").textContent = hint || "";
el.replaceChildren(d);
}
/* ── що це за адреса ───────────────────────────────────────────── */
function isSpec(src) { return /\.json(\?|#|$)/i.test(src || ""); }
function isTableau(src) { return /public\.tableau\.com/i.test(src || ""); }
function isChart(src) { return isSpec(src) || isTableau(src); }
/* ── Tableau Public ────────────────────────────────────────────── */
/* Адреса графіка Tableau Public → адреса, яку можна вбудувати.
Приймає і те, що видно в адресному рядку, і готову адресу /views/. */
function tableauEmbedUrl(raw) {
var url = String(raw).trim().split("?")[0];
var m = url.match(/[#!/]*\/(?:viz|views|vizhome)\/([^/]+)\/([^/?#]+)/);
if (!m) return null;
/* tabs=no обов'язковий: без нього рамка показує всі аркуші книги,
разом із чернетками, і будь-хто може в них клацнути. */
return "https://public.tableau.com/views/" + m[1] + "/" + m[2] +
"?:showVizHome=no&:embed=y&:tabs=no&:display_count=no&:toolbar=no";
}
function mountTableau(el) {
var raw = el.getAttribute("data-tableau");
var src = tableauEmbedUrl(raw);
if (!src) {
box(el, "warn", "Не вдалося прочитати адресу Tableau",
"Має бути адреса вигляду " +
"public.tableau.com/app/profile/…/viz/Книга/Аркуш. " +
"Відкрийте графік на Tableau Public і скопіюйте адресу з адресного рядка.");
return;
}
var frame = document.createElement("iframe");
frame.className = "viz-frame";
frame.src = src;
frame.loading = "lazy";
frame.title = "Графік Tableau Public";
frame.setAttribute("allowfullscreen", "");
frame.style.height = (el.getAttribute("data-height") || 620) + "px";
/* Пряме посилання під рамкою. Усередині рамки Tableau малює власну
сторінку помилки, якої ми звідси не бачимо. Посилання дає читачеві
шлях до графіка, а автору показує, чи жива адреса. */
var link = document.createElement("p");
link.className = "viz-link";
var a = document.createElement("a");
a.href = String(raw).trim();
a.target = "_blank";
a.rel = "noopener";
a.textContent = "Відкрити на Tableau Public";
link.appendChild(a);
el.replaceChildren(frame, link);
}
/* ── Vega-Lite ─────────────────────────────────────────────────── */
var vegaReady = null;
function loadScript(src) {
return new Promise(function (ok, fail) {
var s = document.createElement("script");
s.src = src;
s.async = false;
s.onload = ok;
s.onerror = function () { fail(new Error("cdn: " + src)); };
document.head.appendChild(s);
});
}
function ensureVega() {
if (typeof window.vegaEmbed === "function") return Promise.resolve();
if (!vegaReady) {
vegaReady = VEGA.reduce(function (chain, src) {
return chain.then(function () { return loadScript(src); });
}, Promise.resolve());
}
return vegaReady;
}
/* Специфікація без width займає всю ширину колонки, тому та сама
робота читається і на телефоні. Для facet, concat і repeat так
не можна: там ширину рахує сама Vega-Lite. */
function fitToColumn(spec) {
if (!spec || typeof spec !== "object") return spec;
if ("width" in spec) return spec;
if (spec.facet || spec.hconcat || spec.vconcat || spec.concat || spec.repeat) return spec;
spec.width = "container";
return spec;
}
function specSource(el) {
if (el.inlineSpec) return Promise.resolve(el.inlineSpec);
var path = el.getAttribute("data-spec");
return fetch(path).then(function (r) {
if (!r.ok) throw new Error("404");
return r.json();
});
}
function mountSpec(el) {
var path = el.getAttribute("data-spec") || "специфікація в тексті роботи";
ensureVega()
.then(function () { return specSource(el); })
.then(function (spec) {
return window.vegaEmbed(el, fitToColumn(spec), { actions: false, renderer: "svg" });
})
.then(function (res) { el.vegaView = res && res.view; })
.catch(function (err) {
var msg = String((err && err.message) || err);
if (/^cdn:/.test(msg)) {
box(el, "warn", "Бібліотека Vega-Lite не завантажилась",
"Перевірте інтернет і оновіть сторінку.");
return;
}
var hint;
if (/404|fetch|load|network/i.test(msg)) {
hint = "Файл " + path + " не знайдено. Перевірте, що він лежить у теці " +
"specs/ і що ім'я збігається. Великі й малі літери мають значення.";
} else if (/JSON|token|Unexpected/i.test(msg)) {
hint = "Специфікація не читається як JSON. Найчастіша причина — зайва " +
"або пропущена кома. Вставте специфікацію у Vega Editor: він " +
"покаже, де саме.";
} else {
hint = "Vega-Lite відмовилась малювати цю специфікацію: " + msg +
". Перевірте її у Vega Editor.";
}
box(el, "warn", "Графік не вдалося показати", hint);
});
}
/* ── графіки, поставлені в тексті ──────────────────────────────── */
function vizFor(src, height) {
var el = document.createElement("div");
el.className = "viz";
if (height) el.setAttribute("data-height", height);
if (isTableau(src)) el.setAttribute("data-tableau", src);
else el.setAttribute("data-spec", src);
return el;
}
function figureFor(node, caption, wide) {
var fig = document.createElement("figure");
fig.className = "chart" + (wide ? " wide" : "");
fig.appendChild(node);
if (caption) {
var cap = document.createElement("figcaption");
cap.textContent = caption;
fig.appendChild(cap);
}
return fig;
}
/* Підпис у лапках після адреси: wide — на всю ширину, число —
висота рамки Tableau. ![Графік](адреса "wide 900") */
function readOptions(title) {
var o = {};
String(title || "").split(/\s+/).forEach(function (word) {
if (/^wide$/i.test(word)) o.wide = true;
else if (/^\d+$/.test(word)) o.height = word;
});
return o;
}
function figureForImage(img) {
var o = readOptions(img.getAttribute("title"));
return figureFor(vizFor(img.getAttribute("src"), o.height), img.getAttribute("alt"), o.wide);
}
function chartImages(node) {
return [].slice.call(node.querySelectorAll("img")).filter(function (img) {
return isChart(img.getAttribute("src"));
});
}
function upgradeImages(root) {
/* Спочатку абзаци, у яких стоять самі графіки. Два графіки в
одному абзаці — це рядок із двох: так їх ставлять поруч. */
[].slice.call(root.querySelectorAll("p")).forEach(function (p) {
var imgs = chartImages(p);
if (!imgs.length) return;
var alone = imgs.length === p.children.length && p.textContent.trim() === "";
var figs = imgs.map(figureForImage);
if (!alone) {
imgs.forEach(function (img, i) { img.replaceWith(figs[i]); });
return;
}
if (figs.length === 1) {
p.replaceWith(figs[0]);
return;
}
var row = document.createElement("div");
row.className = "chart-row";
figs.forEach(function (f) { row.appendChild(f); });
p.replaceWith(row);
});
/* Графіки поза абзацом: у списку, у таблиці, у цитаті. */
chartImages(root).forEach(function (img) {
img.replaceWith(figureForImage(img));
});
}
/* Специфікація, написана просто в тексті роботи:
```vega-lite … ``` */
function upgradeFences(root) {
var blocks = [].slice.call(root.querySelectorAll('[class*="language-vega-lite"]'));
blocks.forEach(function (el) {
/* Підсвітка коду загортає код у кілька рівнів. Беремо
найзовнішній блок: його ми і замінюємо на графік. */
var block = el.closest("div[class*='language-vega-lite']") ||
el.closest("pre") || el;
if (block.dataset.chartDone) return;
block.dataset.chartDone = "1";
var code = el.matches("code") ? el : (el.querySelector("code") || el);
var viz = document.createElement("div");
viz.className = "viz";
var fig = figureFor(viz, null, false);
block.replaceWith(fig);
try {
viz.inlineSpec = JSON.parse(code.textContent);
} catch (e) {
box(viz, "warn", "Специфікація в тексті не читається як JSON",
"Найчастіша причина — зайва або пропущена кома. Вставте її у " +
"Vega Editor: він покаже, де саме. Повідомлення браузера: " +
String(e.message || e));
}
});
}
/* ── бічна панель ──────────────────────────────────────────────── */
/* Підсвічує в бічній панелі ту роботу, яка зараз на екрані. */
function followScroll() {
var links = document.querySelectorAll(".side-works a");
if (!links.length || !("IntersectionObserver" in window)) return;
var pairs = [];
links.forEach(function (a) {
var el = document.getElementById(a.getAttribute("href").slice(1));
if (el) pairs.push({ a: a, el: el });
});
var seen = new Set();
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) seen.add(e.target); else seen.delete(e.target);
});
pairs.forEach(function (p) { p.a.classList.remove("here"); });
for (var i = 0; i < pairs.length; i++) {
if (seen.has(pairs[i].el)) { pairs[i].a.classList.add("here"); break; }
}
}, { rootMargin: "-20% 0px -70% 0px" });
pairs.forEach(function (p) { io.observe(p.el); });
}
/* Клік у бічній панелі або адреса з #якорем мають розгортати роботу,
навіть якщо читач її згорнув. */
function openOnJump() {
function openHash() {
var id = location.hash.slice(1);
if (!id) return;
var el = document.getElementById(id);
if (el && el.tagName === "DETAILS") el.open = true;
}
openHash();
window.addEventListener("hashchange", openHash);
document.querySelectorAll('.side-works a').forEach(function (a) {
a.addEventListener("click", function () {
var el = document.getElementById(a.getAttribute("href").slice(1));
if (el && el.tagName === "DETAILS") el.open = true;
});
});
}
/* Одна кнопка: згорнути всі роботи або розгорнути назад. */
function foldAll() {
var works = document.querySelectorAll("details.work");
var side = document.querySelector(".side");
if (!works.length || !side) return;
var btn = document.createElement("button");
btn.className = "side-fold";
btn.type = "button";
side.appendChild(btn);
function label() {
var open = document.querySelectorAll("details.work[open]").length;
btn.textContent = open ? "Згорнути всі" : "Розгорнути всі";
}
btn.addEventListener("click", function () {
var open = document.querySelectorAll("details.work[open]").length;
works.forEach(function (w) { w.open = open === 0; });
label();
});
works.forEach(function (w) { w.addEventListener("toggle", label); });
label();
}
/* ── старт ─────────────────────────────────────────────────────── */
function mountAll() {
var main = document.querySelector("main") || document.body;
try {
upgradeFences(main);
upgradeImages(main);
} catch (e) {
/* Розпізнавання графіків у тексті не має ламати решту сторінки. */
}
document.querySelectorAll(".viz").forEach(function (el) {
try {
if (el.hasAttribute("data-tableau")) mountTableau(el);
else if (el.inlineSpec || el.hasAttribute("data-spec")) mountSpec(el);
} catch (e) {
box(el, "warn", "Цей блок не вдалося показати", String(e.message || e));
}
});
followScroll();
openOnJump();
foldAll();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", mountAll);
} else {
mountAll();
}
})();
+385
View File
@@ -0,0 +1,385 @@
/* Оформлення сторінки портфоліо. Правити не обов'язково —
але якщо хочете свої кольори, міняйте змінні в :root нижче. */
:root {
--ink: #1f2a33;
--ink-soft: #56646f;
--accent: #003964;
--line: #e0e6ea;
--bg: #ffffff;
--bg-soft: #fbfcfd;
--warn: #f15b43;
--bleed: 0rem;
}
/* Пороги рахуються так: розкладка впирається в 66rem, тож блок
виступає за неї безпечно, поки ширина вікна ≥ 1008px + 2 × виступ. */
@media (min-width: 1200px) { :root { --bleed: 5rem; } }
@media (min-width: 1400px) { :root { --bleed: 9rem; } }
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 17px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}
/* ── розкладка: бічна панель + вміст ───────────────────────────── */
.layout {
display: grid;
grid-template-columns: 15rem minmax(0, 1fr);
gap: 2rem;
max-width: 66rem;
margin: 0 auto;
padding: 0 1.5rem;
}
.content { min-width: 0; }
.side {
min-width: 0;
position: sticky;
top: 0;
align-self: start;
max-height: 100vh;
overflow-y: auto;
padding: 3.5rem 0 2rem;
}
.side-name {
display: block;
font-size: 1.15rem;
font-weight: 650;
line-height: 1.25;
color: var(--accent);
text-decoration: none;
}
.side-role {
margin: 0.3rem 0 1.6rem;
font-size: 0.85rem;
color: var(--ink-soft);
}
.side-nav a {
display: block;
padding: 0.4rem 0;
font-size: 0.95rem;
color: var(--ink-soft);
text-decoration: none;
border-left: 2px solid transparent;
padding-left: 0.8rem;
margin-left: -0.8rem;
}
.side-nav a:hover { color: var(--accent); }
.side-nav a.here {
color: var(--accent);
font-weight: 600;
border-left-color: var(--accent);
}
.side-works { margin-top: 1.2rem; }
/* Рамка-роздільник потрібна тільки тоді, коли вище є список сторінок. */
.side-nav + .side-works {
padding-top: 1.2rem;
border-top: 1px solid var(--line);
}
.side-works a {
display: block;
padding: 0.35rem 0 0.35rem 0.8rem;
margin-left: -0.8rem;
font-size: 0.85rem;
line-height: 1.35;
color: var(--ink-soft);
text-decoration: none;
border-left: 2px solid transparent;
}
.side-works a:hover { color: var(--accent); }
.side-works a.here { color: var(--accent); border-left-color: var(--accent); }
/* ── шапка ─────────────────────────────────────────────────────── */
.page-head {
padding: 3.5rem 0 2.5rem;
border-bottom: 1px solid var(--line);
}
.page-head h1 {
margin: 0;
font-size: 2.4rem;
line-height: 1.15;
letter-spacing: -0.02em;
color: var(--accent);
}
.tagline {
margin: 0.6rem 0 0;
color: var(--ink-soft);
font-size: 1.05rem;
}
/* ── роботи ────────────────────────────────────────────────────── */
main { padding: 0; }
.work {
scroll-margin-top: 1.5rem;
padding: 2rem 0;
border-bottom: 1px solid var(--line);
}
.work:last-of-type { border-bottom: 0; }
/* Заголовок роботи — він же кнопка «згорнути». */
.work > summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.3rem 0;
user-select: none;
}
.work > summary::-webkit-details-marker { display: none; }
.work > summary::before {
content: "";
flex: 0 0 auto;
width: 0.5rem;
height: 0.5rem;
border-right: 2px solid var(--ink-soft);
border-bottom: 2px solid var(--ink-soft);
transform: rotate(-45deg);
transition: transform 0.15s;
}
.work[open] > summary::before { transform: rotate(45deg); }
.work > summary:hover::before { border-color: var(--accent); }
.work-title {
font-size: 1.4rem;
font-weight: 650;
line-height: 1.25;
color: var(--accent);
}
.work-body { padding-top: 0.6rem; }
/* Кнопка «згорнути всі» в бічній панелі. */
.side-fold {
margin-top: 1.2rem;
padding: 0;
border: 0;
background: none;
font: inherit;
font-size: 0.8rem;
color: var(--ink-soft);
cursor: pointer;
text-align: left;
}
.side-fold:hover { color: var(--accent); }
/* Міру тримає текст, а не блок: рядок довший за 34rem читати важче.
Графік, поставлений у тексті роботи, цієї межі не має — він займає
всю ширину колонки. */
.note {
margin: 0 0 1.4rem;
color: var(--ink-soft);
}
.note > p, .note > ul, .note > ol, .note > blockquote,
.note > h2, .note > h3, .note > h4, .note > table { max-width: 34rem; }
.note b, .note strong { color: var(--ink); font-weight: 600; }
.note p { margin: 0 0 0.7rem; }
.note p:last-child { margin-bottom: 0; }
.note + .note { margin-top: 1.2rem; }
/* Рядок «це приклад» — видимий, але не на місці опису роботи. */
.note.swap {
max-width: 34rem;
margin-top: -0.9rem;
padding-left: 0.8rem;
border-left: 2px solid var(--line);
font-size: 0.85rem;
}
/* Позначка прикладу. Приберете приклад — приберете і її. */
.tag {
display: inline-block;
padding: 0.15em 0.5em;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--bg);
font-size: 0.6em;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-soft);
vertical-align: middle;
}
/* Контейнер графіка. Широкий графік прокручується всередині себе,
а не розтягує сторінку. */
.viz {
max-width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Робота на всю ширину: <section class="work wide">.
Дашборди Tableau ширші за текстову колонку, і в ній їх обрізає. */
/* Ширший блок для дашбордів: вилазить за колонку тексту рівно на
--bleed з кожного боку. Класичний трюк «на всю ширину вікна» тут
не працює: колонка стоїть праворуч від панелі, тобто не по центру
вікна, і центрування зсуває блок за правий край. */
.work.wide .note > .chart,
.work.wide .note > .chart-row,
.chart.wide {
/* Виступ тільки вправо. Ліворуч стоїть липка бічна панель, і
симетричний виступ наїжджав би на неї. */
width: calc(100% + var(--bleed));
max-width: none; /* інакше .viz{max-width:100%} тримає в колонці */
}
@media (max-width: 720px) {
.work.wide .note > .chart,
.work.wide .note > .chart-row,
.chart.wide { width: 100%; }
}
/* vega-embed робить свій контейнер inline-block, і той обгортається
під ширину графіка, вилазячи з колонки. Повертаємо блок. */
.viz.vega-embed { display: block; }
/* ── графік, поставлений у тексті роботи ───────────────────────── */
/* Рядок ![Підпис](specs/файл.json) браузер спершу читає як картинку.
Ховаємо її, поки embed.js не поставив на це місце графік: інакше
на секунду блимає значок «зображення не завантажилось». Так само
з блоком ```vega-lite```: це специфікація, а не текст для читача. */
main img[src$=".json"],
main img[src*="public.tableau.com"],
main [class*="language-vega-lite"] { display: none; }
main pre:has(> code[class*="language-vega-lite"]) { display: none; }
.chart { margin: 0 0 1.4rem; }
.chart .viz { width: 100%; }
/* Скріншот, поставлений у тексті роботи, як звичайна картинка. */
.note img {
display: block;
max-width: 100%;
height: auto;
border: 1px solid var(--line);
border-radius: 8px;
}
.chart figcaption {
margin: 0.5rem 0 0;
font-size: 0.85rem;
color: var(--ink-soft);
}
/* Два графіки в одному ряду. Ставте їх поруч тоді, коли читач має
їх порівняти: око порівнює те, що бачить разом.
На вузькому екрані ряд розпадається на два графіки один під одним. */
.chart-row {
display: flex;
gap: 1.4rem;
margin: 0 0 1.4rem;
}
/* Колонка з підписом унизу: графіки в ряду різної висоти, а підписи
мають стояти на одному рівні. */
.chart-row .chart {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-width: 0;
margin: 0;
}
.chart-row .chart figcaption { margin-top: auto; padding-top: 0.5rem; }
@media (max-width: 720px) {
.chart-row { display: block; }
.chart-row .chart { margin: 0 0 1.4rem; }
.chart-row .chart:last-child { margin-bottom: 0; }
}
.viz-frame {
display: block;
width: 100%;
min-height: 400px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--bg-soft);
}
.viz-link {
margin: 0.6rem 0 0;
font-size: 0.85rem;
}
.viz-link a { color: var(--ink-soft); }
.viz-link a:hover { color: var(--accent); }
/* ── підказки замість графіка ──────────────────────────────────── */
.viz-msg {
padding: 2rem 1.5rem;
border-radius: 8px;
background: var(--bg-soft);
font-size: 0.95rem;
}
.viz-msg b { display: block; color: var(--accent); margin-bottom: 0.35rem; }
.viz-msg span { color: var(--ink-soft); }
.viz-msg--warn { border: 1px solid var(--warn); border-left-width: 4px; }
.viz-msg--warn b { color: var(--warn); }
/* ── підвал ────────────────────────────────────────────────────── */
.page-foot {
padding: 2.5rem 0 4rem;
border-top: 1px solid var(--line);
color: var(--ink-soft);
font-size: 0.9rem;
}
.page-foot p { margin: 0; }
@media (max-width: 720px) {
/* minmax(0,…), не 1fr: інакше колонка розтягується під найширший
вміст — вбудований графік — і сторінка їде вбік. */
.layout { grid-template-columns: minmax(0, 1fr); gap: 0; }
.side {
position: static;
max-height: none;
padding: 2rem 0 1rem;
border-bottom: 1px solid var(--line);
}
.side-role { margin-bottom: 1rem; }
.side-nav { display: flex; gap: 1.2rem; }
.side-nav a { border-left: 0; padding-left: 0; margin-left: 0; }
.side-nav a.here { border-bottom: 2px solid var(--accent); }
.side-works { display: none; }
.page-head { padding-top: 2rem; }
.page-head h1 { font-size: 1.9rem; }
.work { padding: 2rem 0; }
}
+21
View File
@@ -0,0 +1,21 @@
---
show_tagline: true
charts: true
---
{%- for work in site.works %}
<details class="work{% if work.wide %} wide{% endif %}" id="work-{{ work.slug }}" open>
<summary>
<span class="work-title">{{ work.title }}</span>
{%- if work.example %} <span class="tag">приклад</span>{% endif %}
</summary>
<div class="work-body">
<div class="note">{{ work.content | markdownify }}</div>
{%- if work.example %}
<p class="note swap">Це приклад із шаблону. Замініть його своєю роботою або
видаліть файл <code>_works/{{ work.slug }}.md</code>.</p>
{%- endif %}
</div>
</details>
{%- endfor %}
+61
View File
@@ -0,0 +1,61 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "Демонстраційний графік шаблону. Дані вигадані. Замініть його своїм.",
"title": {
"text": "Заголовок графіка",
"anchor": "start",
"fontSize": 15
},
"height": 180,
"data": {
"values": [
{
"категорія": "Категорія А",
"значення": 42
},
{
"категорія": "Категорія Б",
"значення": 31
},
{
"категорія": "Категорія В",
"значення": 27
},
{
"категорія": "Категорія Г",
"значення": 18
}
]
},
"mark": {
"type": "bar",
"color": "#003964"
},
"encoding": {
"y": {
"field": "категорія",
"type": "nominal",
"title": null,
"sort": "-x"
},
"x": {
"field": "значення",
"type": "quantitative",
"title": null,
"axis": {
"tickCount": 5,
"format": "d"
}
},
"tooltip": [
{
"field": "категорія",
"title": "категорія"
},
{
"field": "значення",
"title": "значення"
}
]
}
}
+69
View File
@@ -0,0 +1,69 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "Другий демонстраційний графік шаблону. Дані вигадані.",
"title": {
"text": "Заголовок другого графіка",
"anchor": "start",
"fontSize": 15
},
"height": 180,
"data": {
"values": [
{
"місяць": "Січ",
"значення": 18
},
{
"місяць": "Лют",
"значення": 24
},
{
"місяць": "Бер",
"значення": 21
},
{
"місяць": "Кві",
"значення": 33
},
{
"місяць": "Тра",
"значення": 39
},
{
"місяць": "Чер",
"значення": 36
}
]
},
"mark": {
"type": "line",
"color": "#003964",
"point": true
},
"encoding": {
"x": {
"field": "місяць",
"type": "nominal",
"title": null,
"sort": null,
"axis": {
"labelAngle": 0
}
},
"y": {
"field": "значення",
"type": "quantitative",
"title": null
},
"tooltip": [
{
"field": "місяць",
"title": "місяць"
},
{
"field": "значення",
"title": "значення"
}
]
}
}