Skip to content

Commit

Permalink
Add chart
Browse files Browse the repository at this point in the history
  • Loading branch information
temnov98 committed Dec 6, 2023
1 parent 0784167 commit 7aa51cb
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 5 deletions.
2 changes: 2 additions & 0 deletions examples/tapme/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
<script src="./js/components/chart/chart-page.component.js"></script>
<script src="./js/components/tracker/tracker-page.component.js"></script>
<script src="./js/components/page.component.js"></script>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body onload="initPage('page-id', new PageComponent())">
<div id="page-id"></div>
Expand Down
300 changes: 298 additions & 2 deletions examples/tapme/js/components/chart/chart-page.component.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,304 @@
// TODO: это самый плохой код в моей жизни, но я хотел сделать быстро и чтобы работало. И я смог.
// TODO: refactor it

/**
* @param {any} value
* @return {string}
*/
function getTooltip(value) {
const { tags, seconds } = value;

const tagComponents = tags.map((tag) => `
<div class="tooltip-tag">${'#' + tag}</div>
`).join('');

return `
<div class="chart-js-custom-tooltip">
<div>${seconds.toFixed(2)}h</div>
${tagComponents}
</div>
`;
}

function generateRandomHexColor() {
const getHex = () => Math.floor(Math.random() * 256).toString(16).padStart(2, '0');

return `#${getHex()}${getHex()}${getHex()}`;
}

class TaskData {
constructor({ tags, seconds }) {
this.tags = (tags ?? ['No tag']).sort();
this.seconds = seconds;
}

static fromRaw(raw) {
return new TaskData({
tags: raw.tags,
seconds: raw.seconds,
});
}
}

class DayData {
constructor({ date, tasks }) {
this.date = date;
this.tasks = tasks;
}

static fromRaw(raw) {
return new DayData({
date: new Date(raw.date),
tasks: raw.tasks.map((task) => TaskData.fromRaw(task))
});
}
}

function realDataToDayData(date, data) {
return new DayData({
date,
tasks: data.tasks.map((task) => new TaskData({
tags: task.tags,
seconds: task.duration,
})),
});
}

let testData = [];
let allTags = [];
let activeTags = new Set();

function refreshConstants() {
testData.sort((left, right) => left.date.getTime() - right.date.getTime());
allTags = [...new Set(testData.map((day) => day.tasks.map((task) => task.tags)).flat(2))];
activeTags = new Set(allTags);
}

function saveToLocalStorage() {
localStorage.setItem('__chart-test-data', JSON.stringify(testData));
}

function readFromLocalStorage() {
const raw = JSON.parse(localStorage.getItem('__chart-test-data') ?? '[]');
testData = raw.map((item) => DayData.fromRaw(item));

refreshConstants();
}

readFromLocalStorage();

function filterTags(tags) {
return tags.filter((tag) => activeTags.has(tag));
}

class ChartPageComponent extends Component {
constructor() {
super();

this.canvasId = getId();
}

onRender() {
const ctx = document.getElementById(this.canvasId);

const labels = testData.map((item) => item.date.toLocaleDateString());

const getGroup = (tags) => '#' + filterTags(tags).join('# ');

const groups = new Set();
const groupToTags = new Map();

for (const dayItem of testData) {
for (const taskItem of dayItem.tasks) {
const group = getGroup(filterTags(taskItem.tags));
groups.add(group);
groupToTags.set(group, filterTags(taskItem.tags));
}
}

const sumOfSeconds = (items) => items.reduce((accumulator, currentValue) => accumulator + currentValue, 0);

const datasets = [...groups].map((group) => ({
label: group,
data: [
...testData.map((item) => ({
date: item.date.toLocaleDateString(),
seconds: +(sumOfSeconds(
item.tasks
.filter((task) => {
if (!filterTags(task.tags).length) {
return false;
}

return getGroup(filterTags(task.tags)) === group;
})
.map((task) => task.seconds)
) / 1000 / 60 / 60).toFixed(2),
tags: filterTags(groupToTags.get(group)),
})),
],
parsing: {
xAxisKey: 'date',
yAxisKey: 'seconds',
},
backgroundColor: generateRandomHexColor(),
fill: true,
}));

new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets,
},
options: {
animations: false,
"plugins": {
legend: {
display: false,
},
"title": {
"display": true,
},
tooltip: {
enabled: false,
position: 'nearest',
external: function(context) {
// Tooltip Element
let tooltipEl = document.getElementById('chartjs-tooltip');

// Create element on first render
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = 'chartjs-tooltip';
tooltipEl.innerHTML = '<table></table>';
document.body.appendChild(tooltipEl);
}

// Hide if no tooltip
const tooltipModel = context.tooltip;
if (tooltipModel.opacity === 0) {
tooltipEl.style.opacity = 0; // todo: set to 0
return;
}

// Set Text
if (tooltipModel.body) {
const value = context.tooltip.dataPoints[0].raw;

const tableRoot = tooltipEl.querySelector('table');
tableRoot.innerHTML = getTooltip(value);
}

const position = context.chart.canvas.getBoundingClientRect();
const bodyFont = Chart.helpers.toFont(tooltipModel.options.bodyFont);

// Display, position, and set styles for font
tooltipEl.style.opacity = 1;
tooltipEl.style.position = 'absolute';
tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px';
tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px';
tooltipEl.style.font = bodyFont.string;
tooltipEl.style.padding = tooltipModel.padding + 'px ' + tooltipModel.padding + 'px';
tooltipEl.style.pointerEvents = 'none';
},
},
},
"responsive": true,
"scales": {
"x": {
"stacked": true,
},
"y": {
"stacked": true,
},
},
"barPercentage": 1,
"categoryPercentage": 0.98,
},
});
}

onClickTag(tag, checked) {
if (checked) {
activeTags.add(tag);
} else {
activeTags.delete(tag);
}

this.redraw();
}

async selectFiles() {
testData = [];

const handles = await window.showOpenFilePicker({
multiple: true,
types: [
{
description: 'JSON Files',
accept: {
'application/json': ['.json'],
},
},
],
});

const files = await Promise.all(handles.map((handle) => handle.getFile()));
const filesContents = await Promise.all(files.map((file) => file.text()));

for (let index = 0; index < files.length; index++) {
const file = files[index];
const content = filesContents[index];

const [day, month, year] = file.name.split(' ')[0].split('-');
testData.push(realDataToDayData(new Date(`${year}-${month}-${day}`), JSON.parse(content)));
}

refreshConstants();
saveToLocalStorage();

this.redraw();
}

clearChart() {
testData = [];

refreshConstants();
saveToLocalStorage();

this.redraw();
}

toHtml() {
setTimeout(() => this.onRender(), 0);

const tagsComponents = allTags.map((tag) => t`
<div style="float: left">
<input
type="checkbox"
onchange="${(event) => this.onClickTag(tag, event.target.checked)}"
${activeTags.has(tag) ? 'checked' : ''}
/>
${tag}
</div>
`);

return t`
<div class="chart-page-component">
<h1>Coming soon 😉</h1>
</div>`;
<div>
<button onclick="${() => this.selectFiles()}">Select files</button>
<button onclick="${() => this.clearChart()}">Clear</button>
</div>
<div class="chart-tags-panel">
${tagsComponents}
</div>
<canvas id="${this.canvasId}"></canvas>
${DebugComponent}
</div>
`;
}
}
31 changes: 28 additions & 3 deletions examples/tapme/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,32 @@ body:has(.page--dark) .tab-button:hover {
}

.chart-page-component {
font-size: 30pt;
padding-top: 300px;
text-align: center;
margin: 0 50px 0 110px;
}

.chart-js-custom-tooltip {
background-color: white;
font-size: 16pt;
padding: 15px;
}

.tooltip-tag {
padding-top: 10px;
}

.chart-tags-panel {
margin-top: 15px;
width: 100%;
font-size: 16pt;
}

.chart-page-component button {
font-size: 15pt;
padding: 15px;
}

.chart-tags-panel input {
width: 30px;
height: 30px;
}

0 comments on commit 7aa51cb

Please sign in to comment.