commit 1abe7e2468e1e917d84a5a49fffaf1f9a91a6ef5 Author: Oleh Omelchenko Date: Thu Aug 27 07:30:00 2026 +0300 Шаблон портфоліо для курсу «Візуалізація даних» diff --git a/README.md b/README.md new file mode 100644 index 0000000..bea3a2b --- /dev/null +++ b/README.md @@ -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 він вантажить сам. +- Кожна робота на сторінці — це
, тобто її можна + згорнути. Розгорнуті за замовчуванням. +- Оформлення в 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` | вбудовує графіки. Правити не треба | diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..bc1e85b --- /dev/null +++ b/_config.yml @@ -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 diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 0000000..241f224 --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,62 @@ + + + + + +{% if page.heading %}{{ page.heading }} · {% endif %}{{ site.title }} +{%- if page.noindex %} + +{%- endif %} + + + + +
+ + + +
+ +
+

{{ page.heading | default: site.title }}

+ {%- if page.show_tagline %} +

{{ site.tagline }}

+ {%- endif %} +
+ +
+{{ content }} +
+ +
+

{{ site.footer }}

+
+ +
+
+ + + + + diff --git a/_works/01-template.md b/_works/01-template.md new file mode 100644 index 0000000..379e182 --- /dev/null +++ b/_works/01-template.md @@ -0,0 +1,12 @@ +--- +title: "Назва роботи" +example: true +--- + +**Питання.** На яке питання відповідає графік. + +![Підпис під графіком](specs/demo-bar.vl.json) + +**Рішення.** Що ви вирішили і чому саме так. + +**Що видно.** Що з цього побачив читач. diff --git a/_works/02-embedding.md b/_works/02-embedding.md new file mode 100644 index 0000000..c5843dd --- /dev/null +++ b/_works/02-embedding.md @@ -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") diff --git a/assets/embed.js b/assets/embed.js new file mode 100644 index 0000000..29ad326 --- /dev/null +++ b/assets/embed.js @@ -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 = ""; + 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(); + } +})(); diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..3e2f0c6 --- /dev/null +++ b/assets/style.css @@ -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; +} + +/* Робота на всю ширину:
. + Дашборди 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; } +} diff --git a/index.md b/index.md new file mode 100644 index 0000000..802ba87 --- /dev/null +++ b/index.md @@ -0,0 +1,21 @@ +--- +show_tagline: true +charts: true +--- + +{%- for work in site.works %} +
+ + {{ work.title }} + {%- if work.example %} приклад{% endif %} + + +
+
{{ work.content | markdownify }}
+ {%- if work.example %} +

Це приклад із шаблону. Замініть його своєю роботою або + видаліть файл _works/{{ work.slug }}.md.

+ {%- endif %} +
+
+{%- endfor %} diff --git a/specs/demo-bar.vl.json b/specs/demo-bar.vl.json new file mode 100644 index 0000000..c8bfd77 --- /dev/null +++ b/specs/demo-bar.vl.json @@ -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": "значення" + } + ] + } +} \ No newline at end of file diff --git a/specs/demo-line.vl.json b/specs/demo-line.vl.json new file mode 100644 index 0000000..7209bb7 --- /dev/null +++ b/specs/demo-line.vl.json @@ -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": "значення" + } + ] + } +} \ No newline at end of file