🏴☠️ Start
This commit is contained in:
commit
62d59fd06f
28
.editorconfig
Normal file
28
.editorconfig
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.js]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[package.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[{*.html,*.css,*.json}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[humans.txt]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
src/bootstrap*.*
|
||||||
|
src/chart*.js
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.md
|
10
.prettierrc.json
Normal file
10
.prettierrc.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 132,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"htmlWhitespaceSensitivity": "strict",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <http://unlicense.org>
|
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
## Assets
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.js
|
||||||
|
https://raw.githubusercontent.com/twbs/bootstrap/refs/tags/v5.3.3/dist/js/bootstrap.min.js
|
||||||
|
https://raw.githubusercontent.com/twbs/bootstrap/refs/tags/v5.3.3/dist/css/bootstrap.min.css
|
||||||
|
```
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* [ ] favicon
|
||||||
|
* [ ] theme color
|
||||||
|
* [ ] deploy script
|
27
package-lock.json
generated
Normal file
27
package-lock.json
generated
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "poppers-tracker",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "3.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "3.4.1"
|
||||||
|
}
|
||||||
|
}
|
233
src/app.js
Normal file
233
src/app.js
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
const data = { poppers: null, chart: null, default: { year: '2024', month: '12' } };
|
||||||
|
|
||||||
|
window.onload = async function () {
|
||||||
|
data.poppersData = await getData();
|
||||||
|
|
||||||
|
updateYears(data.poppersData);
|
||||||
|
|
||||||
|
/* Автоматическиая загрузка последнего года и месяца */
|
||||||
|
document.getElementById('yearSelect').value = data.default.year;
|
||||||
|
updateMonths(data.default.year);
|
||||||
|
document.getElementById('monthSelect').value = data.default.month;
|
||||||
|
showChart(data.default.month);
|
||||||
|
|
||||||
|
document.getElementById('yearSelect').addEventListener('change', (self) => {
|
||||||
|
const value = self.target.value;
|
||||||
|
if (value === 'Выбрать') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMonths(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('monthSelect').addEventListener('change', (self) => {
|
||||||
|
const value = self.target.value;
|
||||||
|
if (value === 'Выбрать') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showChart(self.target.value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = async () => {
|
||||||
|
const response = await fetch('data.json');
|
||||||
|
|
||||||
|
const posts = await response.json();
|
||||||
|
|
||||||
|
return posts;
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateYears() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const years = document.getElementById('yearSelect');
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data.poppersData)) {
|
||||||
|
if (key === 'update') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = document.createElement('option');
|
||||||
|
year.appendChild(document.createTextNode(key));
|
||||||
|
year.setAttribute('value', key);
|
||||||
|
years.appendChild(year);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMonths(year) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const months = document.getElementById('monthSelect');
|
||||||
|
|
||||||
|
/* Очистка элемента */
|
||||||
|
months.innerText = '';
|
||||||
|
const month = document.createElement('option');
|
||||||
|
month.appendChild(document.createTextNode('Выбрать'));
|
||||||
|
months.appendChild(month);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data.poppersData[year])) {
|
||||||
|
const month = document.createElement('option');
|
||||||
|
month.appendChild(document.createTextNode(key));
|
||||||
|
month.setAttribute('value', key);
|
||||||
|
months.appendChild(month);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showChart(selectedMonth) {
|
||||||
|
const year = document.getElementById('yearSelect').value;
|
||||||
|
const month = selectedMonth;
|
||||||
|
const ctx = document.getElementById('chart');
|
||||||
|
|
||||||
|
if (data.chart != null) {
|
||||||
|
data.chart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Object.keys(data.poppersData[year][month]);
|
||||||
|
const breathData = [];
|
||||||
|
for (const [key, value] of Object.entries(data.poppersData[year][month])) {
|
||||||
|
breathData.push(data.poppersData[year][month][key].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: days,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: ' Вдыханий',
|
||||||
|
data: breathData,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: `${month} месяц, ${year} год`,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
position: 'nearest',
|
||||||
|
external: externalTooltipHandler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Внешний HTML Tooltip | Потом изменить */
|
||||||
|
const getOrCreateTooltip = (chart) => {
|
||||||
|
let tooltipEl = data.chart.canvas.parentNode.querySelector('div');
|
||||||
|
|
||||||
|
if (!tooltipEl) {
|
||||||
|
tooltipEl = document.createElement('div');
|
||||||
|
tooltipEl.style.background = 'rgba(0, 0, 0, 0.4)';
|
||||||
|
tooltipEl.style.borderRadius = '3px';
|
||||||
|
tooltipEl.style.color = 'white';
|
||||||
|
tooltipEl.style.opacity = 1;
|
||||||
|
tooltipEl.style.pointerEvents = 'none';
|
||||||
|
tooltipEl.style.position = 'absolute';
|
||||||
|
tooltipEl.style.transform = 'translate(-50%, 0)';
|
||||||
|
tooltipEl.style.transition = 'all .1s ease';
|
||||||
|
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.style.margin = '0px';
|
||||||
|
|
||||||
|
tooltipEl.appendChild(table);
|
||||||
|
data.chart.canvas.parentNode.appendChild(tooltipEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltipEl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const externalTooltipHandler = (context) => {
|
||||||
|
// Tooltip Element
|
||||||
|
const { chart, tooltip } = context;
|
||||||
|
const tooltipEl = getOrCreateTooltip(data.chart);
|
||||||
|
|
||||||
|
// Hide if no tooltip
|
||||||
|
if (tooltip.opacity === 0) {
|
||||||
|
tooltipEl.style.opacity = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Text
|
||||||
|
if (tooltip.body) {
|
||||||
|
const titleLines = tooltip.title || [];
|
||||||
|
const bodyLines = tooltip.body.map((b) => b.lines);
|
||||||
|
|
||||||
|
const tableHead = document.createElement('thead');
|
||||||
|
|
||||||
|
titleLines.forEach((title) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.style.borderWidth = 0;
|
||||||
|
|
||||||
|
const th = document.createElement('th');
|
||||||
|
th.style.borderWidth = 0;
|
||||||
|
const text = document.createTextNode(title);
|
||||||
|
|
||||||
|
th.appendChild(text);
|
||||||
|
tr.appendChild(th);
|
||||||
|
tableHead.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableBody = document.createElement('tbody');
|
||||||
|
bodyLines.forEach((body, i) => {
|
||||||
|
const colors = tooltip.labelColors[i];
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.style.background = colors.backgroundColor;
|
||||||
|
span.style.borderColor = colors.borderColor;
|
||||||
|
span.style.borderWidth = '2px';
|
||||||
|
span.style.marginRight = '10px';
|
||||||
|
span.style.height = '10px';
|
||||||
|
span.style.width = '10px';
|
||||||
|
span.style.display = 'inline-block';
|
||||||
|
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.style.backgroundColor = 'inherit';
|
||||||
|
tr.style.borderWidth = 0;
|
||||||
|
|
||||||
|
const td = document.createElement('td');
|
||||||
|
td.style.borderWidth = 0;
|
||||||
|
|
||||||
|
const text = document.createTextNode(body);
|
||||||
|
|
||||||
|
td.appendChild(span);
|
||||||
|
td.appendChild(text);
|
||||||
|
tr.appendChild(td);
|
||||||
|
tableBody.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableRoot = tooltipEl.querySelector('table');
|
||||||
|
|
||||||
|
// Remove old children
|
||||||
|
while (tableRoot.firstChild) {
|
||||||
|
tableRoot.firstChild.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new children
|
||||||
|
tableRoot.appendChild(tableHead);
|
||||||
|
tableRoot.appendChild(tableBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { offsetLeft: positionX, offsetTop: positionY } = data.chart.canvas;
|
||||||
|
|
||||||
|
// Display, position, and set styles for font
|
||||||
|
tooltipEl.style.opacity = 1;
|
||||||
|
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
|
||||||
|
tooltipEl.style.top = positionY + tooltip.caretY + 'px';
|
||||||
|
tooltipEl.style.font = tooltip.options.bodyFont.string;
|
||||||
|
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
|
||||||
|
};
|
19
src/data.json
Normal file
19
src/data.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"update": 1733593642,
|
||||||
|
"2024": {
|
||||||
|
"12": {
|
||||||
|
"01": ["10.54pm"],
|
||||||
|
"03": ["12.47am", "1.40am", "11.56am", "12.22pm", "4.51pm", "5.33pm", "1.23pm", "2.20pm", "7.07pm", "8.39pm"],
|
||||||
|
"04": ["11.49am"],
|
||||||
|
"05": ["12.13am", "12.48am", "02.03am", "2.27am", "9.44pm", "9.55pm", "10.17pm", "10.41pm", "11.48pm"],
|
||||||
|
"06": ["12.37am", "02.01am"],
|
||||||
|
"07": ["2.38am", "2.59am", "3.05am", "4.06pm", "7.30pm"]
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"25": ["2.10pm", "2.30pm", "много"],
|
||||||
|
"27": ["2.33pm"],
|
||||||
|
"29": ["2.14am", "2.20am", "2.24am", "4.32am"],
|
||||||
|
"30": ["1.11am"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/index.html
Normal file
42
src/index.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="author" content="Alexander Popov <iiiypuk {at} fastmail.fm>" />
|
||||||
|
<title>Poppers Tracker</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />
|
||||||
|
<script src="chart-4.4.7.umd.js"></script>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="p-4 border-bottom">
|
||||||
|
<a href="/" class="link-body-emphasis text-decoration-none">
|
||||||
|
<span class="fs-4">🧪 Poppers Tracker 📊</span>
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="p-4">
|
||||||
|
<div class="d-grid gap-3" style="grid-template-columns: 1fr 3fr">
|
||||||
|
<div class="bg-body-tertiary border rounded-3 p-3">
|
||||||
|
<p class="fs-5 fw-bold text-center">График</p>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<label class="input-group-text" for="yearSelect">Год</label>
|
||||||
|
<select class="form-select" id="yearSelect">
|
||||||
|
<option selected>Выбрать</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<label class="input-group-text" for="monthSelect">Месяц</label>
|
||||||
|
<select class="form-select" id="monthSelect">
|
||||||
|
<option selected>Выбрать</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-body-tertiary border rounded-3 p-3">
|
||||||
|
<canvas id="chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user