commit 54c3647c96e69126b19ecd3dadd903e861ddcef9 Author: Oleh Omelchenko <79406976+olehomelchenko@users.noreply.github.com> Date: Thu Aug 27 14:28:09 2026 +0300 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd86673 --- /dev/null +++ b/README.md @@ -0,0 +1,331 @@ +# Портфоліо · шаблон + +Сайт для ваших робіт із курсу «Візуалізація даних». Хто відкриє +посилання, одразу бачить самі роботи — без проміжних сторінок. +Вбудовує графіки й дашборди з **Tableau Public** і специфікації +**Vega-Lite**. + +Одна робота — це **один файл** у теці `_works/`. Пишете звичайним +текстом. HTML не потрібен ніде. + +Установлювати нічого не треба. Усе робиться в браузері. + +> **Сайт публічний.** Його бачить будь-хто в інтернеті, і ваше ім'я +> стоїть на ньому. Беріть навчальні або відкриті дані. Якщо графік +> зроблено на робочих даних, заберіть назви клієнтів і справжні суми +> або візьміть інший графік. У Tableau Public вимкніть **Allow +> Access**: інакше книгу з даними можна завантажити. + +--- + +## Крок 1. Зробіть із шаблону свій репозиторій + +1. Угорі натисніть зелену кнопку **Use this template**. +2. Виберіть **Create a new repository**. +3. У полі **Repository name** напишіть `dataviz-portfolio`. +4. Виберіть **Public**. Приватний репозиторій не публікується. +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/ +``` + +Відкрийте її. Сайт уже працює: дві роботи-приклади і список робіт +у бічній панелі. + +## Крок 3. Підпишіть сайт своїм іменем + +Ім'я стоїть в одному місці — у файлі `_config.yml`. + +1. Відкрийте `_config.yml`. +2. Замініть `title`, `role` і `tagline`. +3. Натисніть **Commit changes**. + +```yaml +title: "Олена Ковальчук" +role: "Аналітик даних" +tagline: "Роботи з курсу «Візуалізація даних» KSE, 2026." +``` + +Через хвилину сайт оновиться сам. + +Лапки навколо тексту обов'язкові. Якщо в тексті потрібні лапки, +беріть «такі». + +--- + +## Як додати роботу + +Одна робота — один файл у теці `_works/`. + +1. Відкрийте теку `_works`. +2. Натисніть **Add file → Create new file**. +3. Напишіть ім'я файлу латиницею, з `.md` на кінці: `03-facets.md`. +4. Вставте текст нижче і замініть його своїм. +5. Натисніть **Commit changes**. + +Робота з **Tableau Public**: + +```markdown +--- +title: "Назва роботи" +card: "Один рядок для картки на головній." +tableau: "https://public.tableau.com/app/profile/ваш.профіль/viz/Книга/Аркуш" +--- + +**Питання:** на яке питання відповідає графік. + +**Рішення:** що ви вирішили і чому. + +**Що видно:** що з цього побачив читач. +``` + +Робота з **Vega-Lite**: + +```markdown +--- +title: "Назва роботи" +card: "Один рядок для картки на головній." +spec: "specs/my-chart.json" +--- + +**Питання:** на яке питання відповідає графік. +``` + +Файл специфікації покладіть у теку `specs`: **Add file → Upload +files** → перетягніть файл → **Commit changes**. Ім'я файлу — +латиницею, без пробілів. + +Роботи стоять на сторінці за іменем файлу, за абеткою. Тому ставте +число попереду: `01-`, `02-`, `03-`. Пункт у бічній панелі +з'являється сам. + +Найсильнішу роботу ставте першою. Її побачать усі, решту — +не обов'язково. + +### Що можна написати в шапці + +| Поле | Навіщо | +|---|---| +| `title` | заголовок роботи. Обов'язкове | +| `card` | короткий опис. Знадобиться, якщо додасте окрему головну | +| `tableau` | адреса графіка з адресного рядка Tableau Public | +| `spec` | ім'я файлу специфікації в теці `specs` | +| `before` | картинка «було» над графіком. Для розборів «до/після» | +| `steps` | кілька картинок-спроб перед графіком. Для розборів у кілька кроків | +| `wide: true` | графік на всю ширину сторінки. Для дашбордів | +| `height: 1150` | висота рамки в пікселях. Для дашбордів | + +Кожна робота на сторінці згортається: читач може закрити зайве й +лишити тільки те, що хоче роздивитись. За замовчуванням усі роботи +розгорнуті. У бічній панелі є кнопка «Згорнути всі». + +Текст під шапкою — звичайний Markdown. `**жирний**`, списки, +посилання працюють. + +### Перед тим як вбудовувати графік Tableau + +1. Відкрийте свій графік на Tableau Public. +2. Натисніть шестерню у верхньому правому куті. +3. Вимкніть **Show Sheets**. + +Інакше поруч із графіком видно всі аркуші книги, зокрема чернетки. +У тому самому меню вимкніть **Allow Access**, якщо графік зроблено на +робочих даних. + +### Дашборд ширший за графік + +Дашборд вищий і ширший за окремий графік, тому в текстовій колонці +його обріже. Найпростіший шлях — виправити розмір у Tableau: + +1. Відкрийте дашборд у Tableau. +2. У полі **Size** виберіть **Automatic**. + +Якщо розмір змінити не можна, допишіть у шапку роботи два рядки: + +```yaml +wide: true +height: 1150 +``` + +Висоту підберіть на око. Якщо низ дашборда обрізаний, збільшуйте +число, поки дашборд не вміститься. + +### Розбір «до / стало» + +Найсильніша робота в портфоліо — не картинка, а хід думки. Покладіть +скріншот вихідного графіка в теку `specs` (або в будь-яку іншу) і +допишіть у шапку: + +```yaml +before: "specs/vacancies-before.png" +``` + +Над графіком з'явиться картинка з підписом «Було», під нею — ваш +графік із підписом «Стало». + +Якщо спроб було кілька, покажіть їх усі — так видно хід думки, а не +тільки результат: + +```yaml +steps: + - img: "specs/step1.png" + label: "Спроба 1" + note: "два ряди окремо" + - img: "specs/step2.png" + label: "Спроба 2" + note: "подвійна вісь" +final_label: "Що вийшло" +``` + +Кожна спроба має пояснити, **чому** вона не підійшла. Тупик без +діагнозу — просто чернетка; тупик із діагнозом — доказ, що ви +розуміли, що робите. + +У шаблоні є два готові розбори: «Скільки кандидатів на одну вакансію» +(`_works/01-candidates-per-vacancy.md`) і «Чи заробляємо ми на цьому +обороті» (`_works/02-margin-case.md`). Подивіться, як він +побудований, і напишіть свій за тією ж рамкою: + +- **Питання** — на що читач хотів відповіді; +- **Що не працювало** — чому попередній графік не відповідав; +- **Рішення** — що ви зробили і чому саме так; +- **Що я відкинув** — варіант, який ви розглянули й не взяли. Один + такий абзац доводить, що рішення було свідоме; +- **Що стало видно** — конкретна знахідка, з числом; +- **Що я б ще змінив** — те, що лишилось слабким. Робота, яка + вдає завершеність, читається як навчальна вправа. + +Один такий розбір у портфоліо коштує більше, ніж п'ять акуратних +графіків без пояснень. + +--- + +## Як додати ще одну сторінку + +Наприклад, `about.md` — про себе. + +1. Натисніть **Add file → Create new file**. Ім'я — `about.md`. +2. Вставте текст: + + ```markdown + --- + heading: "Про мене" + --- + + Аналітик у ритейлі. Тут коротко про досвід і контакти. + ``` + +3. Натисніть **Commit changes**. + + Щоб вставити графік на такій сторінці, напишіть один рядок: + + ```liquid + {% include viz.html tableau="https://public.tableau.com/app/profile/…/viz/Книга/Аркуш" %} + ``` + + або `{% include viz.html spec="specs/my-chart.json" %}`. + +4. Відкрийте `_config.yml` і допишіть сторінку в список `nav`: + + ```yaml + nav: + - name: "Про мене" + file: "about.html" + ``` + +5. Натисніть **Commit changes**. + +У полі `file` пишіть ім'я з `.html`, навіть якщо файл має `.md`. +Поки список `nav` порожній, у бічній панелі немає розділу сторінок. +Щойно ви додасте першу сторінку, поруч з'явиться і посилання +«Роботи» на головну. + +--- + +## Якщо щось не показується + +Спершу подивіться на вкладку **Actions** у своєму репозиторії. +Зелена галочка — сайт зібрано. Червоний хрестик — у якомусь файлі +помилка, і сайт лишився таким, яким був до неї. + +| Що бачите | Що робити | +|---|---| +| Червоний хрестик в **Actions** | Відкрийте останній запис і прочитайте рядок з `Error:`. Найчастіше — лапки або відступ у шапці роботи | +| «Не вдалося прочитати адресу Tableau» | Відкрийте графік на Tableau Public і скопіюйте адресу з адресного рядка заново | +| У рамці англійське «Where's the viz?» | Адресу прочитано, графіка за нею немає. Ви перейменували, приховали або видалили книгу чи аркуш | +| «Файл … не знайдено» | Перевірте, що файл лежить у теці `specs`, а ім'я в полі `spec:` збігається | +| «Специфікація не читається як JSON» | Вставте специфікацію у Vega Editor. Він покаже, де зайва або пропущена кома | +| Дашборд обрізаний | Виберіть **Size → Automatic** у Tableau або допишіть `wide: true` і `height:` | +| Робота не з'явилась | Перевірте, що файл лежить у теці `_works` і має `.md` на кінці | +| Роботи стоять не в тому порядку | Вони йдуть за іменем файлу. Перейменуйте: `01-`, `02-`, `03-` | +| Сайт не оновився | Зачекайте хвилину й оновіть сторінку через `Ctrl`/`Cmd` + `Shift` + `R` | + +### Якщо не виходить — спитайте ШІ + +Скопіюйте текст нижче в ChatGPT, Claude або Gemini. Він описує, як +влаштований сайт, тому модель відповідатиме по суті, а не навмання. +Свою проблему допишіть у кінці. + +```text +Допоможи розібратися з моїм сайтом-портфоліо. Відповідай українською, +покроково, і пам'ятай: я працюю тільки через сайт github.com у +браузері, без командного рядка і без програм на комп'ютері. + +Як влаштований сайт: +- Це GitHub Pages з Jekyll. Репозиторій публічний, гілка main, тека + публікації — корінь. +- Налаштування сайту в _config.yml: title, role, tagline, footer і + список сторінок nav (у кожної name і file). +- Головна сторінка — index.md, вона показує всі роботи одразу. + Розмітка одна: _layouts/default.html. +- Кожна робота портфоліо — окремий файл у теці _works/. У його шапці + між --- стоять поля: title, card, а також tableau (адреса графіка + Tableau Public) або spec (ім'я файлу в теці specs/). Необов'язкові + поля: wide: true і height (число). +- Текст роботи під шапкою — звичайний Markdown. +- Кожна робота на сторінці — це
, тобто її можна + згорнути. Розгорнуті за замовчуванням. +- На вільній сторінці графік вставляється через {% include viz.html %}. +- Файли специфікацій Vega-Lite лежать у теці specs/. +- Оформлення в 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..f5a2744 --- /dev/null +++ b/_config.yml @@ -0,0 +1,28 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ НАЛАШТУВАННЯ САЙТУ ║ +# ║ Ім'я стоїть тільки тут. Замінили — змінилося на всіх сторінках. ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +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 + - ЯК-ПИТАТИ-ШІ.md diff --git a/_includes/viz.html b/_includes/viz.html new file mode 100644 index 0000000..836b5a1 --- /dev/null +++ b/_includes/viz.html @@ -0,0 +1,11 @@ +{%- comment -%} +Вбудовує графік на будь-якій сторінці, не тільки в роботі з _works/. +Приклади: + {% include viz.html tableau="https://public.tableau.com/app/profile/…/viz/Книга/Аркуш" %} + {% include viz.html spec="specs/my-chart.json" %} + {% include viz.html tableau="…" wide=true height=1150 %} +{%- endcomment -%} +
diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 0000000..5e2ff5d --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,62 @@ + + + + + +{% if page.heading %}{{ page.heading }} · {% endif %}{{ site.title }} +{% if page.charts %} + + + +{% endif %} + + + + +
+ + + +
+ +
+

{{ page.heading }}

+ {%- if page.show_tagline %} +

{{ site.tagline }}

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

{{ site.footer }}

+
+ +
+
+ +{% if page.charts %}{% endif %} + + diff --git a/_works/01-candidates-per-vacancy.md b/_works/01-candidates-per-vacancy.md new file mode 100644 index 0000000..73a7b4e --- /dev/null +++ b/_works/01-candidates-per-vacancy.md @@ -0,0 +1,80 @@ +--- +title: "Скільки кандидатів на одну вакансію" +card: "Чотири підходи до одних даних. Два перші відповідали не на те питання, третій читався неправильно — і тільки четвертий показав, що паритет зламався за пʼять місяців до вторгнення." +steps: + - img: "specs/cand-step1.png" + label: "Спроба 1" + note: "два ряди окремо" + - img: "specs/cand-step2.png" + label: "Спроба 2" + note: "подвійна вісь" + - img: "specs/candidates-before.png" + label: "Спроба 3" + note: "співвідношення, але стовпчики читаються як складені" +final_label: "Що вийшло" +spec: "specs/candidates-per-vacancy.vl.json" +wide: true +--- + +**Питання.** Як змінилося співвідношення «кандидати на одну вакансію» +на ринку праці — і що саме дало цю зміну: приплив кандидатів чи обвал +вакансій. Дві тижневі серії, березень 2020 — липень 2026. + +**Спроба 1: два ряди окремо.** Найочевидніше рішення — показати обидві +серії, кожну своїм графіком. Обидва графіки правильні, і кожен окремо +читається легко. Але питання було про **співвідношення**, а щоб його +дістати, читач мусить ділити один ряд на інший, тиждень за тижнем. +Людина цього не робить. Графіки відповідають на «скільки кандидатів» +і «скільки вакансій» — не на те, що питали. + +**Спроба 2: подвійна вісь.** Класичний наступний крок: обидві серії в +одному кадрі, кожна на своїй осі. Тепер лінії видно разом — і вони +перетинаються на початку 2022-го, точно біля вертикалі вторгнення. +Виглядає як знахідка. + +Це неправда. Точка перетину тут — **артефакт масштабу**: ліва вісь +іде до 100 тисяч, права до 30 тисяч, і саме це співвідношення осей +ставить перетин туди, де він стоїть. Розтягніть праву вісь удвічі, і +«момент перелому» переїде на рік. Подвійна вісь дала мені готовий +хибний висновок і зробила його переконливим на вигляд. + +**Спроба 3: обчислити співвідношення.** Якщо питання про зв'язок двох +рядів — треба обчислити зв'язок і зробити його головним графіком. +Лінія «кандидатів на вакансію», лінія паритету на 1, вертикаль +вторгнення, а сирі величини — прозорими стовпчиками на тлі, щоб не +загубити масштаб. + +Тут з'явилася нова вада, вже не логічна, а перцептивна: обидва +стовпчики однакової ширини від нуля читаються як **складений** +стовпчик. Перекриття дає темнішу «нижню секцію», а вища серія виглядає +сегментом зверху. Графік технічно правильний, а читається неправильно. +Жодна автоматична перевірка цього не ловить — це видно тільки оком. + +**Що вийшло.** Дві ширини замість однієї: вузький помаранчевий +стовпчик перед широким блідо-синім, як у bullet-графіка. Різниця ширин +робить «перед» однозначним. Стовпчики лишаються на **прихованій** +шкалі — другої осі немає взагалі, тож графік не запрошує читати лінію +по шкалі стовпчиків. Точні числа живуть у підказці й у двох підписах +на кінці. + +**Що стало видно.** Кандидати обійшли вакансії назавжди у вересні +2021 — за пʼять місяців **до** вторгнення. Вторгнення не перевернуло +знак, воно рознесло вже перевернуте співвідношення: 1,4 напередодні +проти 12,5 зараз. У логарифмі обвал вакансій (÷3,7) пояснює близько +60 % зсуву, приплив кандидатів (×2,4) — решту 40 %. + +Порівняйте з другою спробою: там «перелом» стояв у лютому 2022-го. +Різниця між двома висновками — не в даних, а у виборі кодування. + +**Що я б ще змінив.** На лінійній осі провал 2021-го під паритет +стиснутий у нижні 6 % полотна: підпис на нього вказує, але вся драма +візуально post-2022. Для аналітичної аудиторії логарифмічна вісь +чесніша — там однакові відстані вгору і вниз означають однакові +кратності. Для загальної потрібне окреме речення пояснення, тому тут +лишилася лінійна. + +**Як це зроблено.** Чотири спроби — це чотири раунди діалогу з +агентом: він давав форми і компроміси, я вибирав і ламав. Найцінніше +повідомлення в усій розмові було моїм: один скріншот і рядок «сині +завжди зверху помаранчевих». Рендер-перевірка агента ловить накладання +підписів, але не те, що кодування **читається** не так. diff --git a/_works/02-margin-case.md b/_works/02-margin-case.md new file mode 100644 index 0000000..a5ad1af --- /dev/null +++ b/_works/02-margin-case.md @@ -0,0 +1,35 @@ +--- +title: "Чи заробляємо ми на цьому обороті" +card: "Розбір: два ряди окремо не відповідали на питання про зв'язок. Обчислена маржа показала два збиткові місяці." +before: "specs/margin-before.png" +spec: "specs/margin.vl.json" +example: true +--- + +**Питання.** Керівник продажів питав щотижня одне й те саме: «обіг +росте, але чи ми на ньому заробляємо». На дашборді стояли два графіки +один під одним — продажі й прибуток за місяцями. + +**Що не працювало.** Обидва графіки правильні, і прибуток навіть +двічі провалюється нижче нуля. Але відповіді на питання там немає: +щоб її дістати, читач мусить подумки ділити один ряд на інший, +місяць за місяцем. Людина цього не робить. На тлі піків у +вісімнадцять тисяч ті два провали виглядають дрібними зарубками. + +**Рішення.** Питання було про **зв'язок** двох рядів, тому я обчислив +зв'язок і зробив його головним графіком: маржа у відсотках від +продажів, нульова лінія, збиткові місяці позначені точками. + +**Що я відкинув.** Спершу зробив подвійну вісь — продажі зліва, +прибуток справа. Виглядало компактно, але масштаб правої осі можна +підібрати так, щоб та сама пара рядів «доводила» будь-що. Коли +питання про співвідношення, подвійна вісь дає привід для суперечки +замість відповіді. + +**Що стало видно.** Січень 2015-го — мінус вісімнадцять відсотків, +найгірший місяць за чотири роки. На парі сирих графіків він губився +серед звичайних коливань. + +**Що я б ще змінив.** Маржа нічого не каже про причину. Наступний +крок — розбити той місяць за підкатегоріями і знижками: гіпотеза, що +провал дали великі знижки на меблі, але вона поки не перевірена. diff --git a/_works/03-sales-diff.md b/_works/03-sales-diff.md new file mode 100644 index 0000000..7efd4ba --- /dev/null +++ b/_works/03-sales-diff.md @@ -0,0 +1,14 @@ +--- +title: "Що виросло, а що впало" +card: "Замість двох графіків поруч — одна обчислена різниця з нульовою віссю. Втрату в Machines стало видно одразу." +tableau: "https://public.tableau.com/app/profile/oleh.omelchenko7227/viz/DatavizWeek3-Makeover/Sheet10" +example: true +--- + +**Питання:** які підкатегорії втратили в продажах за рік. + +**Рішення:** одна обчислена різниця з нульовою віссю, відсортована за +величиною, замість двох стовпчикових графіків поруч. + +**Що видно:** Machines — єдина помітна втрата. На парі графіків вона +губилася між великими числами. diff --git a/_works/04-seasonality.md b/_works/04-seasonality.md new file mode 100644 index 0000000..cce4ea2 --- /dev/null +++ b/_works/04-seasonality.md @@ -0,0 +1,14 @@ +--- +title: "Сезонність за категоріями" +card: "Величину несе яскравість, тому четверті квартали видно одразу. Найсильніший пік 2017-го дала техніка." +spec: "specs/example.vl.json" +example: true +--- + +**Питання:** чи однакова сезонність у трьох категорій товару. + +**Рішення:** теплокарта. Величину несе яскравість, а позиція витрачена +на категорію і квартал. + +**Що видно:** четверті квартали світліші в усіх трьох. Найсильніший +пік 2017-го дала техніка. diff --git a/assets/embed.js b/assets/embed.js new file mode 100644 index 0000000..59dcfb9 --- /dev/null +++ b/assets/embed.js @@ -0,0 +1,182 @@ +/* Вбудовує графіки у сторінку робіт. + * + * Цей файл правити не треба. Роботи ви описуєте у файлах теки _works/, + * а звідки брати графік, каже їхня «шапка»: + * tableau: "адреса з адресного рядка Tableau Public" + * spec: "specs/файл.json" + * + * Блок із помилкою показує підказку і не ламає решту сторінки. + */ +(function () { + "use strict"; + + 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); + } + + /* Адреса графіка 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", + "У полі 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); + } + + function mountSpec(el) { + var path = el.getAttribute("data-spec"); + + if (typeof vegaEmbed !== "function") { + box(el, "warn", "Бібліотека Vega-Lite не завантажилась", + "Перевірте інтернет і оновіть сторінку."); + return; + } + + vegaEmbed(el, path, { actions: false, renderer: "svg" }) + .catch(function (err) { + var msg = String(err && err.message || err); + var hint = /fetch|load|404|Not Found/i.test(msg) + ? "Файл " + path + " не знайдено. Перевірте, що він лежить у теці specs/ " + + "і що ім'я в полі spec: збігається. Великі й малі літери мають значення." + : "Специфікація не читається як JSON. Найчастіша причина — зайва або " + + "пропущена кома. Вставте специфікацію у Vega Editor: він покаже, де саме."; + box(el, "warn", "Графік не вдалося показати", hint); + }); + } + + /* Підсвічує в бічній панелі ту роботу, яка зараз на екрані. */ + 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() { + document.querySelectorAll(".viz").forEach(function (el) { + try { + if (el.hasAttribute("data-tableau")) mountTableau(el); + else if (el.hasAttribute("data-spec")) mountSpec(el); + else box(el, "todo", "Тут буде графік", + "У шапці роботи напишіть tableau: з адресою або spec: з іменем файлу."); + } 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..e73ccf9 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,412 @@ +/* Оформлення сторінки портфоліо. Правити не обов'язково — + але якщо хочете свої кольори, міняйте змінні в :root нижче. */ + +:root { + --ink: #1f2a33; + --ink-soft: #56646f; + --accent: #003964; + --line: #e0e6ea; + --bg: #ffffff; + --bg-soft: #fbfcfd; + --warn: #f15b43; + --page: 46rem; + --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; +} + +/* ── головна: заголовок розділу і картки ───────────────────────── */ + +.back { margin: 0 0 1.2rem; font-size: 0.9rem; } +.back a { color: var(--ink-soft); } +.back a:hover { color: var(--accent); } + +.section-title { + margin: 2.5rem 0 1.2rem; + font-size: 1.1rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ink-soft); +} + +.cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); + gap: 1rem; + padding-bottom: 1rem; +} + +.card { + display: block; + padding: 1.4rem 1.5rem 1.2rem; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--bg-soft); + text-decoration: none; + color: inherit; +} + +.card:hover { border-color: var(--accent); } + +.card h3 { + margin: 0 0 0.4rem; + font-size: 1.05rem; + line-height: 1.3; + color: var(--accent); +} + +.card p { + margin: 0 0 1rem; + font-size: 0.9rem; + color: var(--ink-soft); +} + +.card-go { font-size: 0.85rem; color: var(--ink-soft); } +.card:hover .card-go { color: var(--accent); } + +/* ── роботи ────────────────────────────────────────────────────── */ + +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; } + +/* Пара «було / стало» для розбору однієї роботи. */ +.ba { margin: 0 0 1.4rem; } + +.ba img { + display: block; + width: 100%; + border: 1px solid var(--line); + border-radius: 8px; +} + +.ba figcaption, .ba-label { + margin: 0.5rem 0 0; + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--ink-soft); +} + +/* Пояснення після назви кроку — звичайним текстом, не капсом. */ +.ba figcaption span { + font-weight: 400; + letter-spacing: 0; + text-transform: none; +} + +.ba-label { margin: 0 0 0.5rem; } + +/* Кнопка «згорнути всі» в бічній панелі. */ +.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); } + +.note { + margin: 0 0 1.4rem; + color: var(--ink-soft); + 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 + .viz, .note + .note { margin-top: 1.2rem; } + +/* заголовок розділу, який Jekyll робить із «## Роботи» */ +h2.section-title { margin: 2.5rem 0 1.2rem; } + +/* Рядок «це приклад» — видимий, але не на місці опису роботи. */ +.note.swap { + 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; +} + +.card .tag { margin-bottom: 0.5rem; } + +/* Контейнер графіка. Широкий графік прокручується всередині себе, + а не розтягує сторінку. */ +.viz { + max-width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +/* Робота на всю ширину:
. + Дашборди Tableau ширші за текстову колонку, і в ній їх обрізає. */ +/* Ширший блок для дашбордів: вилазить за колонку тексту рівно на + --bleed з кожного боку. Класичний трюк «на всю ширину вікна» тут + не працює: колонка стоїть праворуч від панелі, тобто не по центру + вікна, і центрування зсуває блок за правий край. */ +/* Кроки розбору виступають так само, як фінальний графік: інакше + «до» і «після» різного розміру, і порівняння нечесне. */ +.work.wide .ba, +.work.wide .ba-label, +.work.wide .viz, +.viz.wide { + /* Виступ тільки вправо. Ліворуч стоїть липка бічна панель, і + симетричний виступ наїжджав би на неї. */ + width: calc(100% + var(--bleed)); + max-width: none; /* інакше .viz{max-width:100%} тримає в колонці */ +} + +@media (max-width: 720px) { + .work.wide .ba, + .work.wide .ba-label, + .work.wide .viz, + .viz.wide { width: 100%; } +} + +/* vega-embed робить свій контейнер inline-block, і той обгортається + під ширину графіка, вилазячи з колонки. Повертаємо блок. */ +.viz.vega-embed { display: block; } + +.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--todo { border: 2px dashed var(--line); } +.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..a174523 --- /dev/null +++ b/index.md @@ -0,0 +1,42 @@ +--- +heading: "Ім'я Прізвище" +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 %} + + {%- if work.steps %} + {%- for step in work.steps %} +
+ {{ step.label }} +
{{ step.label }}{% if step.note %} — {{ step.note }}{% endif %}
+
+ {%- endfor %} +

{{ work.final_label | default: "Стало" }}

+ {%- elsif work.before %} +
+ Було: {{ work.title }} +
Було
+
+

Стало

+ {%- endif %} + +
+
+
+{%- endfor %} diff --git a/specs/cand-step1.png b/specs/cand-step1.png new file mode 100644 index 0000000..b6be704 Binary files /dev/null and b/specs/cand-step1.png differ diff --git a/specs/cand-step2.png b/specs/cand-step2.png new file mode 100644 index 0000000..268ad98 Binary files /dev/null and b/specs/cand-step2.png differ diff --git a/specs/candidates-before.png b/specs/candidates-before.png new file mode 100644 index 0000000..421b4ef Binary files /dev/null and b/specs/candidates-before.png differ diff --git a/specs/candidates-per-vacancy.vl.json b/specs/candidates-per-vacancy.vl.json new file mode 100644 index 0000000..1d06956 --- /dev/null +++ b/specs/candidates-per-vacancy.vl.json @@ -0,0 +1,2324 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "Candidates per vacancy (weekly ratio line) over dimmed monthly magnitude bars. Bars use a hidden 0-265k count scale (two-width overlap, bullet style); the ratio axis is the only visible axis. Data comes from the named source 'labor_market_weekly' (fields: week, candidates, vacancies) - rebind it at runtime or serve labor-market-weekly.json next to this file.", + "width": "container", + "height": 430, + "background": "#fcfcfb", + "padding": { + "left": 5, + "top": 5, + "right": 46, + "bottom": 5 + }, + "config": { + "font": "system-ui, -apple-system, 'Segoe UI', sans-serif", + "axis": { + "labelColor": "#898781", + "titleColor": "#52514e", + "gridColor": "#e1e0d9", + "gridWidth": 1, + "domainColor": "#c3c2b7", + "tickColor": "#c3c2b7", + "labelFontSize": 11, + "titleFontSize": 11.5, + "titleFontWeight": 500, + "titlePadding": 8 + }, + "legend": { + "labelColor": "#52514e", + "labelFontSize": 12, + "titleColor": "#52514e" + }, + "line": { + "strokeWidth": 2, + "strokeCap": "round", + "strokeJoin": "round" + }, + "view": { + "stroke": null + } + }, + "data": { + "name": "labor_market_weekly", + "values": [ + { + "week": "2020-03-23", + "candidates": 18321, + "vacancies": 4927 + }, + { + "week": "2020-03-30", + "candidates": 18936, + "vacancies": 4216 + }, + { + "week": "2020-04-06", + "candidates": 18705, + "vacancies": 4570 + }, + { + "week": "2020-04-13", + "candidates": 19150, + "vacancies": 4920 + }, + { + "week": "2020-04-20", + "candidates": 19574, + "vacancies": 5125 + }, + { + "week": "2020-04-27", + "candidates": 19880, + "vacancies": 5183 + }, + { + "week": "2020-05-04", + "candidates": 20230, + "vacancies": 5286 + }, + { + "week": "2020-05-11", + "candidates": 20462, + "vacancies": 5115 + }, + { + "week": "2020-05-18", + "candidates": 20822, + "vacancies": 5432 + }, + { + "week": "2020-05-25", + "candidates": 21131, + "vacancies": 5615 + }, + { + "week": "2020-06-01", + "candidates": 21269, + "vacancies": 5823 + }, + { + "week": "2020-06-08", + "candidates": 21384, + "vacancies": 6084 + }, + { + "week": "2020-06-15", + "candidates": 20960, + "vacancies": 6318 + }, + { + "week": "2020-06-22", + "candidates": 21284, + "vacancies": 6454 + }, + { + "week": "2020-06-29", + "candidates": 21442, + "vacancies": 6540 + }, + { + "week": "2020-07-06", + "candidates": 21745, + "vacancies": 7019 + }, + { + "week": "2020-07-13", + "candidates": 21591, + "vacancies": 7046 + }, + { + "week": "2020-07-20", + "candidates": 21547, + "vacancies": 7284 + }, + { + "week": "2020-07-27", + "candidates": 21411, + "vacancies": 7514 + }, + { + "week": "2020-08-03", + "candidates": 21334, + "vacancies": 7458 + }, + { + "week": "2020-08-10", + "candidates": 21163, + "vacancies": 7676 + }, + { + "week": "2020-08-17", + "candidates": 20927, + "vacancies": 7879 + }, + { + "week": "2020-08-24", + "candidates": 20969, + "vacancies": 8012 + }, + { + "week": "2020-08-31", + "candidates": 20287, + "vacancies": 8569 + }, + { + "week": "2020-09-07", + "candidates": 20154, + "vacancies": 9989 + }, + { + "week": "2020-09-14", + "candidates": 20204, + "vacancies": 9543 + }, + { + "week": "2020-09-21", + "candidates": 20075, + "vacancies": 10058 + }, + { + "week": "2020-09-28", + "candidates": 20124, + "vacancies": 10135 + }, + { + "week": "2020-10-05", + "candidates": 19927, + "vacancies": 10778 + }, + { + "week": "2020-10-12", + "candidates": 19549, + "vacancies": 10726 + }, + { + "week": "2020-10-19", + "candidates": 19296, + "vacancies": 11426 + }, + { + "week": "2020-10-26", + "candidates": 19142, + "vacancies": 12008 + }, + { + "week": "2020-11-02", + "candidates": 18905, + "vacancies": 12300 + }, + { + "week": "2020-11-09", + "candidates": 18518, + "vacancies": 13124 + }, + { + "week": "2020-11-16", + "candidates": 18248, + "vacancies": 13225 + }, + { + "week": "2020-11-23", + "candidates": 18170, + "vacancies": 13413 + }, + { + "week": "2020-11-30", + "candidates": 17800, + "vacancies": 14061 + }, + { + "week": "2020-12-07", + "candidates": 17393, + "vacancies": 13939 + }, + { + "week": "2020-12-14", + "candidates": 16901, + "vacancies": 14221 + }, + { + "week": "2020-12-21", + "candidates": 16375, + "vacancies": 13573 + }, + { + "week": "2020-12-28", + "candidates": 15702, + "vacancies": 12037 + }, + { + "week": "2021-01-04", + "candidates": 16357, + "vacancies": 11888 + }, + { + "week": "2021-01-11", + "candidates": 16971, + "vacancies": 14127 + }, + { + "week": "2021-01-18", + "candidates": 16944, + "vacancies": 15651 + }, + { + "week": "2021-01-25", + "candidates": 16905, + "vacancies": 17219 + }, + { + "week": "2021-02-01", + "candidates": 17022, + "vacancies": 18082 + }, + { + "week": "2021-02-08", + "candidates": 16804, + "vacancies": 17414 + }, + { + "week": "2021-02-15", + "candidates": 16699, + "vacancies": 17930 + }, + { + "week": "2021-02-22", + "candidates": 16673, + "vacancies": 18450 + }, + { + "week": "2021-03-01", + "candidates": 16775, + "vacancies": 18923 + }, + { + "week": "2021-03-08", + "candidates": 16825, + "vacancies": 18875 + }, + { + "week": "2021-03-15", + "candidates": 16885, + "vacancies": 19080 + }, + { + "week": "2021-03-22", + "candidates": 16567, + "vacancies": 19506 + }, + { + "week": "2021-03-29", + "candidates": 16223, + "vacancies": 19997 + }, + { + "week": "2021-04-05", + "candidates": 16246, + "vacancies": 20362 + }, + { + "week": "2021-04-12", + "candidates": 16122, + "vacancies": 20432 + }, + { + "week": "2021-04-19", + "candidates": 15942, + "vacancies": 20416 + }, + { + "week": "2021-04-26", + "candidates": 15497, + "vacancies": 20627 + }, + { + "week": "2021-05-03", + "candidates": 14889, + "vacancies": 19656 + }, + { + "week": "2021-05-10", + "candidates": 15151, + "vacancies": 20128 + }, + { + "week": "2021-05-17", + "candidates": 16148, + "vacancies": 21201 + }, + { + "week": "2021-05-24", + "candidates": 16193, + "vacancies": 21607 + }, + { + "week": "2021-05-31", + "candidates": 16156, + "vacancies": 22631 + }, + { + "week": "2021-06-07", + "candidates": 15451, + "vacancies": 22309 + }, + { + "week": "2021-06-14", + "candidates": 15428, + "vacancies": 22282 + }, + { + "week": "2021-06-21", + "candidates": 15412, + "vacancies": 21494 + }, + { + "week": "2021-06-28", + "candidates": 15695, + "vacancies": 21985 + }, + { + "week": "2021-07-05", + "candidates": 16249, + "vacancies": 22585 + }, + { + "week": "2021-07-12", + "candidates": 16194, + "vacancies": 22658 + }, + { + "week": "2021-07-19", + "candidates": 16308, + "vacancies": 23064 + }, + { + "week": "2021-07-26", + "candidates": 16498, + "vacancies": 23514 + }, + { + "week": "2021-08-02", + "candidates": 16624, + "vacancies": 23782 + }, + { + "week": "2021-08-09", + "candidates": 16849, + "vacancies": 23946 + }, + { + "week": "2021-08-16", + "candidates": 17889, + "vacancies": 24445 + }, + { + "week": "2021-08-23", + "candidates": 20236, + "vacancies": 24039 + }, + { + "week": "2021-08-30", + "candidates": 21681, + "vacancies": 24118 + }, + { + "week": "2021-09-06", + "candidates": 23552, + "vacancies": 24361 + }, + { + "week": "2021-09-13", + "candidates": 24580, + "vacancies": 24421 + }, + { + "week": "2021-09-20", + "candidates": 25674, + "vacancies": 25083 + }, + { + "week": "2021-09-27", + "candidates": 26343, + "vacancies": 25163 + }, + { + "week": "2021-10-04", + "candidates": 27086, + "vacancies": 25324 + }, + { + "week": "2021-10-11", + "candidates": 27343, + "vacancies": 24537 + }, + { + "week": "2021-10-18", + "candidates": 28320, + "vacancies": 25634 + }, + { + "week": "2021-10-25", + "candidates": 28628, + "vacancies": 25825 + }, + { + "week": "2021-11-01", + "candidates": 29271, + "vacancies": 26318 + }, + { + "week": "2021-11-08", + "candidates": 29852, + "vacancies": 27431 + }, + { + "week": "2021-11-15", + "candidates": 30343, + "vacancies": 26802 + }, + { + "week": "2021-11-22", + "candidates": 30461, + "vacancies": 26745 + }, + { + "week": "2021-11-29", + "candidates": 30839, + "vacancies": 26571 + }, + { + "week": "2021-12-06", + "candidates": 31216, + "vacancies": 26701 + }, + { + "week": "2021-12-13", + "candidates": 31331, + "vacancies": 26377 + }, + { + "week": "2021-12-20", + "candidates": 31279, + "vacancies": 25244 + }, + { + "week": "2021-12-27", + "candidates": 30962, + "vacancies": 22121 + }, + { + "week": "2022-01-03", + "candidates": 31931, + "vacancies": 20319 + }, + { + "week": "2022-01-10", + "candidates": 33143, + "vacancies": 23622 + }, + { + "week": "2022-01-17", + "candidates": 34226, + "vacancies": 25758 + }, + { + "week": "2022-01-24", + "candidates": 35036, + "vacancies": 28009 + }, + { + "week": "2022-01-31", + "candidates": 35917, + "vacancies": 28965 + }, + { + "week": "2022-02-07", + "candidates": 36750, + "vacancies": 27448 + }, + { + "week": "2022-02-14", + "candidates": 36984, + "vacancies": 27009 + }, + { + "week": "2022-02-21", + "candidates": 36567, + "vacancies": 24824 + }, + { + "week": "2022-02-28", + "candidates": 33898, + "vacancies": 19653 + }, + { + "week": "2022-03-07", + "candidates": 36069, + "vacancies": 15596 + }, + { + "week": "2022-03-14", + "candidates": 37879, + "vacancies": 10408 + }, + { + "week": "2022-03-21", + "candidates": 39458, + "vacancies": 13950 + }, + { + "week": "2022-03-28", + "candidates": 40871, + "vacancies": 15442 + }, + { + "week": "2022-04-04", + "candidates": 42007, + "vacancies": 15910 + }, + { + "week": "2022-04-11", + "candidates": 42881, + "vacancies": 14670 + }, + { + "week": "2022-04-18", + "candidates": 43676, + "vacancies": 14934 + }, + { + "week": "2022-04-25", + "candidates": 44448, + "vacancies": 15054 + }, + { + "week": "2022-05-02", + "candidates": 45370, + "vacancies": 15411 + }, + { + "week": "2022-05-09", + "candidates": 46237, + "vacancies": 15574 + }, + { + "week": "2022-05-16", + "candidates": 47388, + "vacancies": 15289 + }, + { + "week": "2022-05-23", + "candidates": 47848, + "vacancies": 15319 + }, + { + "week": "2022-05-30", + "candidates": 49400, + "vacancies": 15342 + }, + { + "week": "2022-06-06", + "candidates": 50276, + "vacancies": 15160 + }, + { + "week": "2022-06-13", + "candidates": 51203, + "vacancies": 14726 + }, + { + "week": "2022-06-20", + "candidates": 52334, + "vacancies": 14720 + }, + { + "week": "2022-06-27", + "candidates": 53209, + "vacancies": 14336 + }, + { + "week": "2022-07-04", + "candidates": 54315, + "vacancies": 14137 + }, + { + "week": "2022-07-11", + "candidates": 55196, + "vacancies": 13739 + }, + { + "week": "2022-07-18", + "candidates": 56175, + "vacancies": 13557 + }, + { + "week": "2022-07-25", + "candidates": 57136, + "vacancies": 13490 + }, + { + "week": "2022-08-01", + "candidates": 58140, + "vacancies": 13490 + }, + { + "week": "2022-08-08", + "candidates": 59030, + "vacancies": 13197 + }, + { + "week": "2022-08-15", + "candidates": 59802, + "vacancies": 13159 + }, + { + "week": "2022-08-22", + "candidates": 60408, + "vacancies": 12709 + }, + { + "week": "2022-08-29", + "candidates": 61258, + "vacancies": 12561 + }, + { + "week": "2022-09-05", + "candidates": 62400, + "vacancies": 12901 + }, + { + "week": "2022-09-12", + "candidates": 63054, + "vacancies": 13097 + }, + { + "week": "2022-09-19", + "candidates": 63660, + "vacancies": 13572 + }, + { + "week": "2022-09-26", + "candidates": 64382, + "vacancies": 13360 + }, + { + "week": "2022-10-03", + "candidates": 65206, + "vacancies": 13355 + }, + { + "week": "2022-10-10", + "candidates": 65633, + "vacancies": 13007 + }, + { + "week": "2022-10-17", + "candidates": 66511, + "vacancies": 12987 + }, + { + "week": "2022-10-24", + "candidates": 67194, + "vacancies": 12881 + }, + { + "week": "2022-10-31", + "candidates": 69151, + "vacancies": 12483 + }, + { + "week": "2022-11-07", + "candidates": 70213, + "vacancies": 12377 + }, + { + "week": "2022-11-14", + "candidates": 70973, + "vacancies": 11845 + }, + { + "week": "2022-11-21", + "candidates": 71476, + "vacancies": 11507 + }, + { + "week": "2022-11-28", + "candidates": 72578, + "vacancies": 11097 + }, + { + "week": "2022-12-05", + "candidates": 73921, + "vacancies": 10632 + }, + { + "week": "2022-12-12", + "candidates": 75211, + "vacancies": 10091 + }, + { + "week": "2022-12-19", + "candidates": 76011, + "vacancies": 9682 + }, + { + "week": "2022-12-26", + "candidates": 76488, + "vacancies": 8727 + }, + { + "week": "2023-01-02", + "candidates": 78485, + "vacancies": 8300 + }, + { + "week": "2023-01-09", + "candidates": 80623, + "vacancies": 8791 + }, + { + "week": "2023-01-16", + "candidates": 82525, + "vacancies": 9300 + }, + { + "week": "2023-01-23", + "candidates": 84187, + "vacancies": 9874 + }, + { + "week": "2023-01-30", + "candidates": 71123, + "vacancies": 9905 + }, + { + "week": "2023-02-06", + "candidates": 72302, + "vacancies": 9623 + }, + { + "week": "2023-02-13", + "candidates": 74007, + "vacancies": 9644 + }, + { + "week": "2023-02-20", + "candidates": 75377, + "vacancies": 9575 + }, + { + "week": "2023-02-27", + "candidates": 76634, + "vacancies": 9536 + }, + { + "week": "2023-03-06", + "candidates": 77995, + "vacancies": 9489 + }, + { + "week": "2023-03-13", + "candidates": 79206, + "vacancies": 9466 + }, + { + "week": "2023-03-20", + "candidates": 80236, + "vacancies": 9478 + }, + { + "week": "2023-03-27", + "candidates": 81489, + "vacancies": 9483 + }, + { + "week": "2023-04-03", + "candidates": 82405, + "vacancies": 9573 + }, + { + "week": "2023-04-10", + "candidates": 82725, + "vacancies": 9327 + }, + { + "week": "2023-04-17", + "candidates": 83114, + "vacancies": 9285 + }, + { + "week": "2023-04-24", + "candidates": 83908, + "vacancies": 9335 + }, + { + "week": "2023-05-01", + "candidates": 84029, + "vacancies": 9469 + }, + { + "week": "2023-05-08", + "candidates": 84773, + "vacancies": 9438 + }, + { + "week": "2023-05-15", + "candidates": 84886, + "vacancies": 9682 + }, + { + "week": "2023-05-22", + "candidates": 85350, + "vacancies": 9500 + }, + { + "week": "2023-05-29", + "candidates": 85642, + "vacancies": 9523 + }, + { + "week": "2023-06-05", + "candidates": 86030, + "vacancies": 9236 + }, + { + "week": "2023-06-12", + "candidates": 86598, + "vacancies": 9343 + }, + { + "week": "2023-06-19", + "candidates": 87038, + "vacancies": 9364 + }, + { + "week": "2023-06-26", + "candidates": 87600, + "vacancies": 9236 + }, + { + "week": "2023-07-03", + "candidates": 88361, + "vacancies": 9471 + }, + { + "week": "2023-07-10", + "candidates": 89268, + "vacancies": 9283 + }, + { + "week": "2023-07-17", + "candidates": 89760, + "vacancies": 9223 + }, + { + "week": "2023-07-24", + "candidates": 89946, + "vacancies": 9219 + }, + { + "week": "2023-07-31", + "candidates": 90477, + "vacancies": 8117 + }, + { + "week": "2023-08-07", + "candidates": 91008, + "vacancies": 7600 + }, + { + "week": "2023-08-14", + "candidates": 91338, + "vacancies": 7092 + }, + { + "week": "2023-08-21", + "candidates": 91164, + "vacancies": 6653 + }, + { + "week": "2023-08-28", + "candidates": 91360, + "vacancies": 6774 + }, + { + "week": "2023-09-04", + "candidates": 91495, + "vacancies": 6879 + }, + { + "week": "2023-09-11", + "candidates": 91959, + "vacancies": 6761 + }, + { + "week": "2023-09-18", + "candidates": 92184, + "vacancies": 7075 + }, + { + "week": "2023-09-25", + "candidates": 92326, + "vacancies": 6952 + }, + { + "week": "2023-10-02", + "candidates": 91820, + "vacancies": 7008 + }, + { + "week": "2023-10-09", + "candidates": 91720, + "vacancies": 7052 + }, + { + "week": "2023-10-16", + "candidates": 91718, + "vacancies": 7061 + }, + { + "week": "2023-10-23", + "candidates": 92155, + "vacancies": 6999 + }, + { + "week": "2023-10-30", + "candidates": 92129, + "vacancies": 6888 + }, + { + "week": "2023-11-06", + "candidates": 92547, + "vacancies": 6852 + }, + { + "week": "2023-11-13", + "candidates": 92533, + "vacancies": 6941 + }, + { + "week": "2023-11-20", + "candidates": 92696, + "vacancies": 6935 + }, + { + "week": "2023-11-27", + "candidates": 92698, + "vacancies": 6915 + }, + { + "week": "2023-12-04", + "candidates": 92422, + "vacancies": 6795 + }, + { + "week": "2023-12-11", + "candidates": 91568, + "vacancies": 6685 + }, + { + "week": "2023-12-18", + "candidates": 90801, + "vacancies": 6367 + }, + { + "week": "2023-12-25", + "candidates": 89337, + "vacancies": 5701 + }, + { + "week": "2024-01-01", + "candidates": 89594, + "vacancies": 5716 + }, + { + "week": "2024-01-08", + "candidates": 90414, + "vacancies": 6142 + }, + { + "week": "2024-01-15", + "candidates": 90746, + "vacancies": 6577 + }, + { + "week": "2024-01-22", + "candidates": 90327, + "vacancies": 6939 + }, + { + "week": "2024-01-29", + "candidates": 90200, + "vacancies": 6983 + }, + { + "week": "2024-02-05", + "candidates": 90189, + "vacancies": 6852 + }, + { + "week": "2024-02-12", + "candidates": 89834, + "vacancies": 6900 + }, + { + "week": "2024-02-19", + "candidates": 89650, + "vacancies": 7200 + }, + { + "week": "2024-02-26", + "candidates": 89809, + "vacancies": 7512 + }, + { + "week": "2024-03-04", + "candidates": 89681, + "vacancies": 7523 + }, + { + "week": "2024-03-11", + "candidates": 90170, + "vacancies": 6752 + }, + { + "week": "2024-03-18", + "candidates": 90291, + "vacancies": 7074 + }, + { + "week": "2024-03-25", + "candidates": 90293, + "vacancies": 7305 + }, + { + "week": "2024-04-01", + "candidates": 90411, + "vacancies": 7424 + }, + { + "week": "2024-04-08", + "candidates": 90017, + "vacancies": 7468 + }, + { + "week": "2024-04-15", + "candidates": 89761, + "vacancies": 7461 + }, + { + "week": "2024-04-22", + "candidates": 89117, + "vacancies": 7214 + }, + { + "week": "2024-04-29", + "candidates": 88531, + "vacancies": 7288 + }, + { + "week": "2024-05-06", + "candidates": 88303, + "vacancies": 7187 + }, + { + "week": "2024-05-13", + "candidates": 87963, + "vacancies": 7054 + }, + { + "week": "2024-05-20", + "candidates": 87541, + "vacancies": 7189 + }, + { + "week": "2024-05-27", + "candidates": 87180, + "vacancies": 7211 + }, + { + "week": "2024-06-03", + "candidates": 86782, + "vacancies": 7277 + }, + { + "week": "2024-06-10", + "candidates": 86785, + "vacancies": 7232 + }, + { + "week": "2024-06-17", + "candidates": 86265, + "vacancies": 7159 + }, + { + "week": "2024-06-24", + "candidates": 86183, + "vacancies": 6958 + }, + { + "week": "2024-07-01", + "candidates": 86120, + "vacancies": 7280 + }, + { + "week": "2024-07-08", + "candidates": 86067, + "vacancies": 7269 + }, + { + "week": "2024-07-15", + "candidates": 85534, + "vacancies": 7279 + }, + { + "week": "2024-07-22", + "candidates": 85656, + "vacancies": 7352 + }, + { + "week": "2024-07-29", + "candidates": 85788, + "vacancies": 7396 + }, + { + "week": "2024-08-05", + "candidates": 85888, + "vacancies": 7507 + }, + { + "week": "2024-08-12", + "candidates": 85544, + "vacancies": 7549 + }, + { + "week": "2024-08-19", + "candidates": 85074, + "vacancies": 7357 + }, + { + "week": "2024-08-26", + "candidates": 84826, + "vacancies": 7030 + }, + { + "week": "2024-09-02", + "candidates": 85171, + "vacancies": 7208 + }, + { + "week": "2024-09-09", + "candidates": 85444, + "vacancies": 7447 + }, + { + "week": "2024-09-16", + "candidates": 85150, + "vacancies": 7401 + }, + { + "week": "2024-09-23", + "candidates": 85166, + "vacancies": 7427 + }, + { + "week": "2024-09-30", + "candidates": 84683, + "vacancies": 7226 + }, + { + "week": "2024-10-07", + "candidates": 84641, + "vacancies": 7348 + }, + { + "week": "2024-10-14", + "candidates": 84640, + "vacancies": 7494 + }, + { + "week": "2024-10-21", + "candidates": 83925, + "vacancies": 7406 + }, + { + "week": "2024-10-28", + "candidates": 83615, + "vacancies": 7367 + }, + { + "week": "2024-11-04", + "candidates": 83532, + "vacancies": 7365 + }, + { + "week": "2024-11-11", + "candidates": 83010, + "vacancies": 7425 + }, + { + "week": "2024-11-18", + "candidates": 82605, + "vacancies": 7574 + }, + { + "week": "2024-11-25", + "candidates": 82003, + "vacancies": 7586 + }, + { + "week": "2024-12-02", + "candidates": 81870, + "vacancies": 7382 + }, + { + "week": "2024-12-09", + "candidates": 81385, + "vacancies": 7426 + }, + { + "week": "2024-12-16", + "candidates": 80443, + "vacancies": 7142 + }, + { + "week": "2024-12-23", + "candidates": 78891, + "vacancies": 6456 + }, + { + "week": "2024-12-30", + "candidates": 78567, + "vacancies": 5942 + }, + { + "week": "2025-01-06", + "candidates": 80219, + "vacancies": 6350 + }, + { + "week": "2025-01-13", + "candidates": 80558, + "vacancies": 6794 + }, + { + "week": "2025-01-20", + "candidates": 80995, + "vacancies": 7339 + }, + { + "week": "2025-01-27", + "candidates": 81569, + "vacancies": 7983 + }, + { + "week": "2025-02-03", + "candidates": 82141, + "vacancies": 7676 + }, + { + "week": "2025-02-10", + "candidates": 82541, + "vacancies": 7917 + }, + { + "week": "2025-02-17", + "candidates": 82792, + "vacancies": 7964 + }, + { + "week": "2025-02-24", + "candidates": 82603, + "vacancies": 8232 + }, + { + "week": "2025-03-03", + "candidates": 82417, + "vacancies": 8008 + }, + { + "week": "2025-03-10", + "candidates": 82824, + "vacancies": 7959 + }, + { + "week": "2025-03-17", + "candidates": 82681, + "vacancies": 7957 + }, + { + "week": "2025-03-24", + "candidates": 83243, + "vacancies": 8007 + }, + { + "week": "2025-03-31", + "candidates": 83774, + "vacancies": 8065 + }, + { + "week": "2025-04-07", + "candidates": 84178, + "vacancies": 8059 + }, + { + "week": "2025-04-14", + "candidates": 83783, + "vacancies": 7970 + }, + { + "week": "2025-04-21", + "candidates": 83588, + "vacancies": 7984 + }, + { + "week": "2025-04-28", + "candidates": 83286, + "vacancies": 7926 + }, + { + "week": "2025-05-05", + "candidates": 83755, + "vacancies": 7978 + }, + { + "week": "2025-05-12", + "candidates": 83849, + "vacancies": 8028 + }, + { + "week": "2025-05-19", + "candidates": 83707, + "vacancies": 8265 + }, + { + "week": "2025-05-26", + "candidates": 83643, + "vacancies": 8389 + }, + { + "week": "2025-06-02", + "candidates": 83385, + "vacancies": 8387 + }, + { + "week": "2025-06-09", + "candidates": 83215, + "vacancies": 8357 + }, + { + "week": "2025-06-16", + "candidates": 83214, + "vacancies": 8319 + }, + { + "week": "2025-06-23", + "candidates": 83042, + "vacancies": 8135 + }, + { + "week": "2025-06-30", + "candidates": 83784, + "vacancies": 8211 + }, + { + "week": "2025-07-07", + "candidates": 83907, + "vacancies": 8471 + }, + { + "week": "2025-07-14", + "candidates": 83825, + "vacancies": 8653 + }, + { + "week": "2025-07-21", + "candidates": 83530, + "vacancies": 8826 + }, + { + "week": "2025-07-28", + "candidates": 83797, + "vacancies": 8773 + }, + { + "week": "2025-08-04", + "candidates": 83770, + "vacancies": 8816 + }, + { + "week": "2025-08-11", + "candidates": 83567, + "vacancies": 8877 + }, + { + "week": "2025-08-18", + "candidates": 83423, + "vacancies": 9092 + }, + { + "week": "2025-08-25", + "candidates": 83605, + "vacancies": 8978 + }, + { + "week": "2025-09-01", + "candidates": 84643, + "vacancies": 9020 + }, + { + "week": "2025-09-08", + "candidates": 84728, + "vacancies": 9021 + }, + { + "week": "2025-09-15", + "candidates": 84619, + "vacancies": 9193 + }, + { + "week": "2025-09-22", + "candidates": 84709, + "vacancies": 9362 + }, + { + "week": "2025-09-29", + "candidates": 84630, + "vacancies": 9312 + }, + { + "week": "2025-10-06", + "candidates": 84628, + "vacancies": 9444 + }, + { + "week": "2025-10-13", + "candidates": 84270, + "vacancies": 9508 + }, + { + "week": "2025-10-20", + "candidates": 84020, + "vacancies": 9462 + }, + { + "week": "2025-10-27", + "candidates": 83650, + "vacancies": 9505 + }, + { + "week": "2025-11-03", + "candidates": 83579, + "vacancies": 9602 + }, + { + "week": "2025-11-10", + "candidates": 83087, + "vacancies": 9648 + }, + { + "week": "2025-11-17", + "candidates": 82749, + "vacancies": 9624 + }, + { + "week": "2025-11-24", + "candidates": 82221, + "vacancies": 9636 + }, + { + "week": "2025-12-01", + "candidates": 82066, + "vacancies": 9421 + }, + { + "week": "2025-12-08", + "candidates": 81693, + "vacancies": 9420 + }, + { + "week": "2025-12-15", + "candidates": 81040, + "vacancies": 9106 + }, + { + "week": "2025-12-22", + "candidates": 79990, + "vacancies": 8285 + }, + { + "week": "2025-12-29", + "candidates": 79528, + "vacancies": 7440 + }, + { + "week": "2026-01-05", + "candidates": 81278, + "vacancies": 8218 + }, + { + "week": "2026-01-12", + "candidates": 81498, + "vacancies": 8557 + }, + { + "week": "2026-01-19", + "candidates": 82168, + "vacancies": 9304 + }, + { + "week": "2026-01-26", + "candidates": 82439, + "vacancies": 10010 + }, + { + "week": "2026-02-02", + "candidates": 83010, + "vacancies": 9587 + }, + { + "week": "2026-02-09", + "candidates": 83210, + "vacancies": 9604 + }, + { + "week": "2026-02-16", + "candidates": 83714, + "vacancies": 9601 + }, + { + "week": "2026-02-23", + "candidates": 83700, + "vacancies": 9548 + }, + { + "week": "2026-03-02", + "candidates": 83984, + "vacancies": 9474 + }, + { + "week": "2026-03-09", + "candidates": 84308, + "vacancies": 9355 + }, + { + "week": "2026-03-16", + "candidates": 84621, + "vacancies": 9260 + }, + { + "week": "2026-03-23", + "candidates": 85119, + "vacancies": 9127 + }, + { + "week": "2026-03-30", + "candidates": 86013, + "vacancies": 8762 + }, + { + "week": "2026-04-06", + "candidates": 86339, + "vacancies": 8610 + }, + { + "week": "2026-04-13", + "candidates": 86366, + "vacancies": 8221 + }, + { + "week": "2026-04-20", + "candidates": 86790, + "vacancies": 7825 + }, + { + "week": "2026-04-27", + "candidates": 86898, + "vacancies": 7785 + }, + { + "week": "2026-05-04", + "candidates": 87141, + "vacancies": 7834 + }, + { + "week": "2026-05-11", + "candidates": 87614, + "vacancies": 8118 + }, + { + "week": "2026-05-18", + "candidates": 87658, + "vacancies": 8149 + }, + { + "week": "2026-05-25", + "candidates": 88061, + "vacancies": 8173 + }, + { + "week": "2026-06-01", + "candidates": 88405, + "vacancies": 7946 + }, + { + "week": "2026-06-08", + "candidates": 88971, + "vacancies": 7901 + }, + { + "week": "2026-06-15", + "candidates": 88786, + "vacancies": 7755 + }, + { + "week": "2026-06-22", + "candidates": 89092, + "vacancies": 7572 + }, + { + "week": "2026-06-29", + "candidates": 89343, + "vacancies": 7248 + }, + { + "week": "2026-07-06", + "candidates": 89844, + "vacancies": 7155 + }, + { + "week": "2026-07-13", + "candidates": 89944, + "vacancies": 7093 + }, + { + "week": "2026-07-20", + "candidates": 90361, + "vacancies": 6980 + }, + { + "week": "2026-07-27", + "candidates": 90448, + "vacancies": 7249 + } + ] + }, + "transform": [ + { + "calculate": "datum.candidates / datum.vacancies", + "as": "ratio" + } + ], + "resolve": { + "scale": { + "y": "independent" + } + }, + "layer": [ + { + "description": "Wide pale bars: monthly mean candidates (hidden count scale)", + "mark": { + "type": "bar", + "width": { + "band": 0.72 + }, + "opacity": 0.15 + }, + "encoding": { + "x": { + "field": "week", + "timeUnit": "yearmonth", + "type": "temporal", + "bandPosition": 0.5, + "title": null, + "axis": { + "grid": false, + "format": "%Y", + "tickCount": "year", + "labelFontSize": 11 + } + }, + "y": { + "aggregate": "mean", + "field": "candidates", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 265000 + ] + }, + "axis": null + }, + "color": { + "datum": "Candidates", + "scale": { + "domain": [ + "Candidates", + "Vacancies" + ], + "range": [ + "#2a78d6", + "#eb6834" + ] + }, + "legend": { + "orient": "top", + "title": null, + "symbolType": "square", + "symbolSize": 140, + "symbolOpacity": 0.35, + "labelExpr": "datum.label + ' (count)'" + } + } + } + }, + { + "description": "Narrow bars in front: monthly mean vacancies (same hidden count scale)", + "mark": { + "type": "bar", + "width": { + "band": 0.32 + }, + "opacity": 0.32 + }, + "encoding": { + "x": { + "field": "week", + "timeUnit": "yearmonth", + "type": "temporal", + "bandPosition": 0.5, + "title": null, + "axis": { + "grid": false, + "format": "%Y", + "tickCount": "year", + "labelFontSize": 11 + } + }, + "y": { + "aggregate": "mean", + "field": "vacancies", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 265000 + ] + }, + "axis": null + }, + "color": { + "datum": "Vacancies", + "scale": { + "domain": [ + "Candidates", + "Vacancies" + ], + "range": [ + "#2a78d6", + "#eb6834" + ] + }, + "legend": { + "orient": "top", + "title": null, + "symbolType": "square", + "symbolSize": 140, + "symbolOpacity": 0.35, + "labelExpr": "datum.label + ' (count)'" + } + } + } + }, + { + "description": "Count end label: last weekly candidates", + "transform": [ + { + "window": [ + { + "op": "row_number", + "as": "rn" + } + ], + "sort": [ + { + "field": "week", + "order": "descending" + } + ] + }, + { + "filter": "datum.rn == 1" + }, + { + "calculate": "format(datum.candidates / 1000, '.0f') + 'k'", + "as": "lbl" + } + ], + "mark": { + "type": "text", + "align": "left", + "dx": 7, + "fontSize": 10.5, + "fill": "#898781" + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "y": { + "field": "candidates", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 265000 + ] + }, + "axis": null + }, + "text": { + "field": "lbl" + } + } + }, + { + "description": "Count end label: last weekly vacancies", + "transform": [ + { + "window": [ + { + "op": "row_number", + "as": "rn" + } + ], + "sort": [ + { + "field": "week", + "order": "descending" + } + ] + }, + { + "filter": "datum.rn == 1" + }, + { + "calculate": "format(datum.vacancies / 1000, '.1f') + 'k'", + "as": "lbl" + } + ], + "mark": { + "type": "text", + "align": "left", + "dx": 7, + "fontSize": 10.5, + "fill": "#898781" + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "y": { + "field": "vacancies", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 265000 + ] + }, + "axis": null + }, + "text": { + "field": "lbl" + } + } + }, + { + "description": "Parity line: 1 candidate per vacancy", + "data": { + "values": [ + { + "y": 1 + } + ] + }, + "mark": { + "type": "rule", + "stroke": "#c3c2b7", + "strokeWidth": 1 + }, + "encoding": { + "y": { + "field": "y", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 16 + ] + }, + "axis": null + } + } + }, + { + "description": "Parity label (halo)", + "data": { + "values": [ + { + "week": "2020-04-06", + "y": 0.55, + "t": "1:1 parity" + } + ] + }, + "mark": { + "type": "text", + "align": "left", + "fontSize": 10.5, + "fill": "#fcfcfb", + "stroke": "#fcfcfb", + "strokeWidth": 3.5 + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "y": { + "field": "y", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 16 + ] + }, + "axis": null + }, + "text": { + "field": "t" + } + } + }, + { + "description": "Parity label", + "data": { + "values": [ + { + "week": "2020-04-06", + "y": 0.55, + "t": "1:1 parity" + } + ] + }, + "mark": { + "type": "text", + "align": "left", + "fontSize": 10.5, + "fill": "#898781" + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "y": { + "field": "y", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 16 + ] + }, + "axis": null + }, + "text": { + "field": "t" + } + } + }, + { + "description": "Full-scale invasion rule", + "data": { + "values": [ + { + "week": "2022-02-24" + } + ] + }, + "mark": { + "type": "rule", + "stroke": "#c3c2b7", + "strokeWidth": 1 + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + } + } + }, + { + "description": "Full-scale invasion label", + "data": { + "values": [ + { + "week": "2022-02-24", + "t": "Full-scale invasion" + } + ] + }, + "mark": { + "type": "text", + "align": "left", + "dx": 6, + "y": 14, + "fontSize": 11, + "fill": "#52514e" + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "text": { + "field": "t" + } + } + }, + { + "description": "2021 inversion annotation (halo)", + "data": { + "values": [ + { + "week": "2021-06-01", + "y": 2.45, + "t": "Jan–Sep 2021: vacancies > candidates" + } + ] + }, + "mark": { + "type": "text", + "align": "center", + "fontSize": 10.5, + "fill": "#fcfcfb", + "stroke": "#fcfcfb", + "strokeWidth": 3.5 + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "y": { + "field": "y", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 16 + ] + }, + "axis": null + }, + "text": { + "field": "t" + } + } + }, + { + "description": "2021 inversion annotation", + "data": { + "values": [ + { + "week": "2021-06-01", + "y": 2.45, + "t": "Jan–Sep 2021: vacancies > candidates" + } + ] + }, + "mark": { + "type": "text", + "align": "center", + "fontSize": 10.5, + "fill": "#898781" + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "y": { + "field": "y", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 16 + ] + }, + "axis": null + }, + "text": { + "field": "t" + } + } + }, + { + "description": "The hero: weekly candidates-per-vacancy ratio", + "mark": { + "type": "line", + "stroke": "#0b0b0b" + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal", + "title": null, + "axis": { + "grid": false, + "format": "%Y", + "tickCount": "year", + "labelFontSize": 11 + } + }, + "y": { + "field": "ratio", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 16 + ] + }, + "axis": { + "tickCount": 5, + "title": "Candidates per vacancy" + } + } + } + }, + { + "description": "Ratio end label (derived from last row)", + "transform": [ + { + "window": [ + { + "op": "row_number", + "as": "rn" + } + ], + "sort": [ + { + "field": "week", + "order": "descending" + } + ] + }, + { + "filter": "datum.rn == 1" + }, + { + "calculate": "format(datum.ratio, '.1f')", + "as": "lbl" + } + ], + "mark": { + "type": "text", + "align": "left", + "dx": 7, + "fontSize": 12, + "fontWeight": 600, + "fill": "#0b0b0b" + }, + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "y": { + "field": "ratio", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 16 + ] + }, + "axis": null + }, + "text": { + "field": "lbl" + } + } + }, + { + "description": "Crosshair + tooltip on nearest week", + "mark": { + "type": "rule", + "stroke": "#c3c2b7", + "strokeWidth": 1 + }, + "params": [ + { + "name": "hover", + "select": { + "type": "point", + "fields": [ + "week" + ], + "nearest": true, + "on": "mouseover", + "clear": "mouseout" + } + } + ], + "encoding": { + "x": { + "field": "week", + "type": "temporal" + }, + "opacity": { + "condition": { + "param": "hover", + "empty": false, + "value": 0.6 + }, + "value": 0 + }, + "tooltip": [ + { + "field": "week", + "type": "temporal", + "title": "Week", + "format": "%b %d, %Y" + }, + { + "field": "candidates", + "type": "quantitative", + "title": "Candidates", + "format": "," + }, + { + "field": "vacancies", + "type": "quantitative", + "title": "Vacancies", + "format": "," + }, + { + "field": "ratio", + "type": "quantitative", + "title": "Per vacancy", + "format": ".2f" + } + ] + } + } + ] +} diff --git a/specs/example.vl.json b/specs/example.vl.json new file mode 100644 index 0000000..fdd45a9 --- /dev/null +++ b/specs/example.vl.json @@ -0,0 +1,307 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "Приклад роботи в шаблоні портфоліо: величина закодована яскравістю, не веселкою.", + "title": { + "text": "Продажі за кварталами", + "subtitle": "Sample Superstore, 2014–2017", + "anchor": "start", + "fontSize": 16, + "subtitleFontSize": 12, + "subtitleColor": "#7a8894" + }, + "width": 620, + "height": 150, + "data": { + "values": [ + { + "cat": "Канцелярія", + "quarter": "2014 Q1", + "sales": 14529 + }, + { + "cat": "Канцелярія", + "quarter": "2014 Q2", + "sales": 31244 + }, + { + "cat": "Канцелярія", + "quarter": "2014 Q3", + "sales": 53924 + }, + { + "cat": "Канцелярія", + "quarter": "2014 Q4", + "sales": 52080 + }, + { + "cat": "Канцелярія", + "quarter": "2015 Q1", + "sales": 23059 + }, + { + "cat": "Канцелярія", + "quarter": "2015 Q2", + "sales": 32320 + }, + { + "cat": "Канцелярія", + "quarter": "2015 Q3", + "sales": 35761 + }, + { + "cat": "Канцелярія", + "quarter": "2015 Q4", + "sales": 46093 + }, + { + "cat": "Канцелярія", + "quarter": "2016 Q1", + "sales": 29441 + }, + { + "cat": "Канцелярія", + "quarter": "2016 Q2", + "sales": 34584 + }, + { + "cat": "Канцелярія", + "quarter": "2016 Q3", + "sales": 45148 + }, + { + "cat": "Канцелярія", + "quarter": "2016 Q4", + "sales": 74767 + }, + { + "cat": "Канцелярія", + "quarter": "2017 Q1", + "sales": 43232 + }, + { + "cat": "Канцелярія", + "quarter": "2017 Q2", + "sales": 45721 + }, + { + "cat": "Канцелярія", + "quarter": "2017 Q3", + "sales": 72197 + }, + { + "cat": "Канцелярія", + "quarter": "2017 Q4", + "sales": 84946 + }, + { + "cat": "Меблі", + "quarter": "2014 Q1", + "sales": 22656 + }, + { + "cat": "Меблі", + "quarter": "2014 Q2", + "sales": 28064 + }, + { + "cat": "Меблі", + "quarter": "2014 Q3", + "sales": 41958 + }, + { + "cat": "Меблі", + "quarter": "2014 Q4", + "sales": 64515 + }, + { + "cat": "Меблі", + "quarter": "2015 Q1", + "sales": 27374 + }, + { + "cat": "Меблі", + "quarter": "2015 Q2", + "sales": 27565 + }, + { + "cat": "Меблі", + "quarter": "2015 Q3", + "sales": 49586 + }, + { + "cat": "Меблі", + "quarter": "2015 Q4", + "sales": 65993 + }, + { + "cat": "Меблі", + "quarter": "2016 Q1", + "sales": 24349 + }, + { + "cat": "Меблі", + "quarter": "2016 Q2", + "sales": 41402 + }, + { + "cat": "Меблі", + "quarter": "2016 Q3", + "sales": 52815 + }, + { + "cat": "Меблі", + "quarter": "2016 Q4", + "sales": 80335 + }, + { + "cat": "Меблі", + "quarter": "2017 Q1", + "sales": 23724 + }, + { + "cat": "Меблі", + "quarter": "2017 Q2", + "sales": 45032 + }, + { + "cat": "Меблі", + "quarter": "2017 Q3", + "sales": 56283 + }, + { + "cat": "Меблі", + "quarter": "2017 Q4", + "sales": 90348 + }, + { + "cat": "Техніка", + "quarter": "2014 Q1", + "sales": 37263 + }, + { + "cat": "Техніка", + "quarter": "2014 Q2", + "sales": 27231 + }, + { + "cat": "Техніка", + "quarter": "2014 Q3", + "sales": 47751 + }, + { + "cat": "Техніка", + "quarter": "2014 Q4", + "sales": 63033 + }, + { + "cat": "Техніка", + "quarter": "2015 Q1", + "sales": 18418 + }, + { + "cat": "Техніка", + "quarter": "2015 Q2", + "sales": 29239 + }, + { + "cat": "Техніка", + "quarter": "2015 Q3", + "sales": 44913 + }, + { + "cat": "Техніка", + "quarter": "2015 Q4", + "sales": 70211 + }, + { + "cat": "Техніка", + "quarter": "2016 Q1", + "sales": 39447 + }, + { + "cat": "Техніка", + "quarter": "2016 Q2", + "sales": 60095 + }, + { + "cat": "Техніка", + "quarter": "2016 Q3", + "sales": 45825 + }, + { + "cat": "Техніка", + "quarter": "2016 Q4", + "sales": 80997 + }, + { + "cat": "Техніка", + "quarter": "2017 Q1", + "sales": 56189 + }, + { + "cat": "Техніка", + "quarter": "2017 Q2", + "sales": 43011 + }, + { + "cat": "Техніка", + "quarter": "2017 Q3", + "sales": 67772 + }, + { + "cat": "Техніка", + "quarter": "2017 Q4", + "sales": 104759 + } + ] + }, + "mark": { + "type": "rect", + "stroke": "#ffffff", + "strokeWidth": 1 + }, + "encoding": { + "x": { + "field": "quarter", + "type": "ordinal", + "title": null, + "axis": { + "labelAngle": -45, + "labelFontSize": 10 + } + }, + "y": { + "field": "cat", + "type": "nominal", + "title": null, + "sort": [ + "Меблі", + "Канцелярія", + "Техніка" + ] + }, + "color": { + "field": "sales", + "type": "quantitative", + "title": "продажі, $", + "scale": { + "scheme": "viridis" + } + }, + "tooltip": [ + { + "field": "cat", + "title": "категорія" + }, + { + "field": "quarter", + "title": "квартал" + }, + { + "field": "sales", + "title": "продажі, $", + "format": "," + } + ] + } +} diff --git a/specs/margin-before.png b/specs/margin-before.png new file mode 100644 index 0000000..5cf653a Binary files /dev/null and b/specs/margin-before.png differ diff --git a/specs/margin.vl.json b/specs/margin.vl.json new file mode 100644 index 0000000..1817a3a --- /dev/null +++ b/specs/margin.vl.json @@ -0,0 +1,287 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "Стало: обчислена маржа з нульовою віссю.", + "title": { + "text": "Маржа, % від продажів", + "subtitle": "Sample Superstore, 2014–2017", + "anchor": "start", + "fontSize": 16, + "subtitleFontSize": 12, + "subtitleColor": "#7a8894" + }, + "width": 600, + "height": 300, + "data": { + "values": [ + { + "month": "2014-01-01", + "margin": 17.21 + }, + { + "month": "2014-02-01", + "margin": 19.08 + }, + { + "month": "2014-03-01", + "margin": 0.9 + }, + { + "month": "2014-04-01", + "margin": 12.33 + }, + { + "month": "2014-05-01", + "margin": 11.58 + }, + { + "month": "2014-06-01", + "margin": 14.38 + }, + { + "month": "2014-07-01", + "margin": -2.48 + }, + { + "month": "2014-08-01", + "margin": 19.05 + }, + { + "month": "2014-09-01", + "margin": 10.18 + }, + { + "month": "2014-10-01", + "margin": 10.96 + }, + { + "month": "2014-11-01", + "margin": 11.82 + }, + { + "month": "2014-12-01", + "margin": 12.92 + }, + { + "month": "2015-01-01", + "margin": -18.05 + }, + { + "month": "2015-02-01", + "margin": 23.54 + }, + { + "month": "2015-03-01", + "margin": 25.13 + }, + { + "month": "2015-04-01", + "margin": 12.25 + }, + { + "month": "2015-05-01", + "margin": 15.49 + }, + { + "month": "2015-06-01", + "margin": 13.45 + }, + { + "month": "2015-07-01", + "margin": 11.43 + }, + { + "month": "2015-08-01", + "margin": 14.52 + }, + { + "month": "2015-09-01", + "margin": 12.71 + }, + { + "month": "2015-10-01", + "margin": 8.97 + }, + { + "month": "2015-11-01", + "margin": 16.42 + }, + { + "month": "2015-12-01", + "margin": 10.7 + }, + { + "month": "2016-01-01", + "margin": 15.23 + }, + { + "month": "2016-02-01", + "margin": 21.78 + }, + { + "month": "2016-03-01", + "margin": 6.98 + }, + { + "month": "2016-04-01", + "margin": 7.68 + }, + { + "month": "2016-05-01", + "margin": 15.2 + }, + { + "month": "2016-06-01", + "margin": 11.77 + }, + { + "month": "2016-07-01", + "margin": 11.29 + }, + { + "month": "2016-08-01", + "margin": 6.63 + }, + { + "month": "2016-09-01", + "margin": 12.71 + }, + { + "month": "2016-10-01", + "margin": 27.21 + }, + { + "month": "2016-11-01", + "margin": 5.05 + }, + { + "month": "2016-12-01", + "margin": 18.44 + }, + { + "month": "2017-01-01", + "margin": 16.24 + }, + { + "month": "2017-02-01", + "margin": 7.95 + }, + { + "month": "2017-03-01", + "margin": 25.06 + }, + { + "month": "2017-04-01", + "margin": 2.56 + }, + { + "month": "2017-05-01", + "margin": 14.33 + }, + { + "month": "2017-06-01", + "margin": 15.52 + }, + { + "month": "2017-07-01", + "margin": 15.36 + }, + { + "month": "2017-08-01", + "margin": 14.32 + }, + { + "month": "2017-09-01", + "margin": 12.51 + }, + { + "month": "2017-10-01", + "margin": 11.93 + }, + { + "month": "2017-11-01", + "margin": 8.18 + }, + { + "month": "2017-12-01", + "margin": 10.12 + } + ] + }, + "layer": [ + { + "mark": { + "type": "area", + "line": { + "color": "#003964", + "strokeWidth": 2 + }, + "color": "#cfe4ea", + "opacity": 0.9 + }, + "encoding": { + "x": { + "field": "month", + "type": "temporal", + "title": null, + "axis": { + "format": "%Y", + "tickCount": 4 + } + }, + "y": { + "field": "margin", + "type": "quantitative", + "title": null + } + } + }, + { + "transform": [ + { + "filter": "datum.margin < 0" + } + ], + "mark": { + "type": "point", + "filled": true, + "size": 70, + "color": "#f15b43" + }, + "encoding": { + "x": { + "field": "month", + "type": "temporal" + }, + "y": { + "field": "margin", + "type": "quantitative" + }, + "tooltip": [ + { + "field": "month", + "type": "temporal", + "title": "місяць", + "format": "%b %Y" + }, + { + "field": "margin", + "type": "quantitative", + "title": "маржа, %" + } + ] + } + }, + { + "mark": { + "type": "rule", + "color": "#8a97a3", + "strokeWidth": 1 + }, + "encoding": { + "y": { + "datum": 0 + } + } + } + ] +}