273 lines
9.4 KiB
Vue
273 lines
9.4 KiB
Vue
<template>
|
|
<div id="heatmap" class="w-full">
|
|
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
|
|
<p class="mb-2 px-1 text-sm text-gray-200">{{ $getString('MessageListeningSessionsInTheLastYear', [Object.values(daysListening).length]) }}</p>
|
|
<div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
|
|
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
|
|
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
|
|
|
|
<div v-for="monthLabel in monthLabels" :key="monthLabel.id" :style="monthLabel.style" class="absolute top-0 left-0 text-gray-300">{{ monthLabel.label }}</div>
|
|
|
|
<div v-for="(block, index) in data" :key="block.dateString" :style="block.style" :data-index="index" class="absolute top-0 left-0 h-2.5 w-2.5 rounded-sm" />
|
|
|
|
<div class="flex py-2 px-4" :style="{ marginTop: innerHeight + 'px' }">
|
|
<div class="flex-grow" />
|
|
<p style="font-size: 10px; line-height: 10px" class="text-gray-400 px-1">{{ $strings.LabelLess }}</p>
|
|
<div v-for="block in legendBlocks" :key="block.id" :style="block.style" class="h-2.5 w-2.5 rounded-sm" style="margin-left: 1.5px; margin-right: 1.5px" />
|
|
<p style="font-size: 10px; line-height: 10px" class="text-gray-400 px-1">{{ $strings.LabelMore }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
props: {
|
|
daysListening: {
|
|
type: Object,
|
|
default: () => {}
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
contentWidth: 0,
|
|
maxInnerWidth: 0,
|
|
innerHeight: 13 * 7,
|
|
blockWidth: 13,
|
|
data: [],
|
|
monthLabels: [],
|
|
tooltipEl: null,
|
|
tooltipTextEl: null,
|
|
tooltipArrowEl: null,
|
|
showingTooltipIndex: -1,
|
|
outlineColors: ['rgba(27, 31, 35, 0.06)', 'rgba(255,255,255,0.03)'],
|
|
bgColors: ['rgb(45,45,45)', 'rgb(14, 68, 41)', 'rgb(0, 109, 50)', 'rgb(38, 166, 65)', 'rgb(57, 211, 83)']
|
|
// GH Colors
|
|
// outlineColors: ['rgba(27, 31, 35, 0.06)', 'rgba(255,255,255,0.05)'],
|
|
// bgColors: ['rgb(22, 27, 34)', 'rgb(14, 68, 41)', 'rgb(0, 109, 50)', 'rgb(38, 166, 65)', 'rgb(57, 211, 83)']
|
|
}
|
|
},
|
|
computed: {
|
|
weeksToShow() {
|
|
return Math.min(52, Math.floor(this.maxInnerWidth / this.blockWidth) - 1)
|
|
},
|
|
innerWidth() {
|
|
return (this.weeksToShow + 1) * 13
|
|
},
|
|
daysToShow() {
|
|
return this.weeksToShow * 7 + this.dayOfWeekToday
|
|
},
|
|
dayOfWeekToday() {
|
|
return new Date().getDay()
|
|
},
|
|
firstWeekStart() {
|
|
return this.$addDaysToToday(-this.daysToShow)
|
|
},
|
|
dayLabels() {
|
|
return [
|
|
{
|
|
label: 'Mon',
|
|
style: {
|
|
transform: `translate(${-25}px, ${13}px)`,
|
|
lineHeight: '10px',
|
|
fontSize: '10px'
|
|
}
|
|
},
|
|
{
|
|
label: 'Wed',
|
|
style: {
|
|
transform: `translate(${-25}px, ${13 * 3}px)`,
|
|
lineHeight: '10px',
|
|
fontSize: '10px'
|
|
}
|
|
},
|
|
{
|
|
label: 'Fri',
|
|
style: {
|
|
transform: `translate(${-25}px, ${13 * 5}px)`,
|
|
lineHeight: '10px',
|
|
fontSize: '10px'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
legendBlocks() {
|
|
return [
|
|
{
|
|
id: 'legend-0',
|
|
style: `background-color:${this.bgColors[0]};outline:1px solid ${this.outlineColors[0]};outline-offset:-1px;`
|
|
},
|
|
{
|
|
id: 'legend-1',
|
|
style: `background-color:${this.bgColors[1]};outline:1px solid ${this.outlineColors[1]};outline-offset:-1px;`
|
|
},
|
|
{
|
|
id: 'legend-2',
|
|
style: `background-color:${this.bgColors[2]};outline:1px solid ${this.outlineColors[1]};outline-offset:-1px;`
|
|
},
|
|
{
|
|
id: 'legend-3',
|
|
style: `background-color:${this.bgColors[3]};outline:1px solid ${this.outlineColors[1]};outline-offset:-1px;`
|
|
},
|
|
{
|
|
id: 'legend-4',
|
|
style: `background-color:${this.bgColors[4]};outline:1px solid ${this.outlineColors[1]};outline-offset:-1px;`
|
|
}
|
|
]
|
|
}
|
|
},
|
|
methods: {
|
|
destroyTooltip() {
|
|
if (this.tooltipEl) this.tooltipEl.remove()
|
|
this.tooltipEl = null
|
|
this.showingTooltipIndex = -1
|
|
},
|
|
createTooltip() {
|
|
const tooltip = document.createElement('div')
|
|
tooltip.className = 'absolute top-0 left-0 rounded bg-gray-500 text-white p-2 text-white max-w-xs pointer-events-none'
|
|
tooltip.style.display = 'none'
|
|
tooltip.id = 'heatmap-tooltip'
|
|
|
|
const tooltipText = document.createElement('p')
|
|
tooltipText.innerText = 'Tooltip'
|
|
tooltipText.style.fontSize = '10px'
|
|
tooltipText.style.lineHeight = '10px'
|
|
tooltip.appendChild(tooltipText)
|
|
|
|
const tooltipArrow = document.createElement('div')
|
|
tooltipArrow.className = 'text-gray-500 arrow-down-small absolute -bottom-1 left-0 right-0 mx-auto'
|
|
tooltip.appendChild(tooltipArrow)
|
|
|
|
this.tooltipEl = tooltip
|
|
this.tooltipTextEl = tooltipText
|
|
this.tooltipArrowEl = tooltipArrow
|
|
|
|
document.body.appendChild(this.tooltipEl)
|
|
},
|
|
showTooltip(index, block, rect) {
|
|
if (this.tooltipEl && this.showingTooltipIndex === index) return
|
|
if (!this.tooltipEl) {
|
|
this.createTooltip()
|
|
}
|
|
|
|
this.showingTooltipIndex = index
|
|
this.tooltipEl.style.display = 'block'
|
|
this.tooltipTextEl.innerHTML = block.value ? `<strong>${this.$elapsedPretty(block.value, true)} listening</strong> on ${block.datePretty}` : `No listening sessions on ${block.datePretty}`
|
|
|
|
const calculateRect = this.tooltipEl.getBoundingClientRect()
|
|
|
|
const w = calculateRect.width / 2
|
|
var left = rect.x - w
|
|
var offsetX = 0
|
|
if (left < 0) {
|
|
offsetX = Math.abs(left)
|
|
left = 0
|
|
} else if (rect.x + w > window.innerWidth - 10) {
|
|
offsetX = window.innerWidth - 10 - (rect.x + w)
|
|
left += offsetX
|
|
}
|
|
|
|
this.tooltipEl.style.transform = `translate(${left}px, ${rect.y - 32}px)`
|
|
this.tooltipArrowEl.style.transform = `translate(${5 - offsetX}px, 0px)`
|
|
},
|
|
hideTooltip() {
|
|
if (this.showingTooltipIndex >= 0 && this.tooltipEl) {
|
|
this.tooltipEl.style.display = 'none'
|
|
this.showingTooltipIndex = -1
|
|
}
|
|
},
|
|
mouseover(e) {
|
|
if (isNaN(e.target.dataset.index)) {
|
|
this.hideTooltip()
|
|
return
|
|
}
|
|
var block = this.data[e.target.dataset.index]
|
|
var rect = e.target.getBoundingClientRect()
|
|
this.showTooltip(e.target.dataset.index, block, rect)
|
|
},
|
|
mouseout(e) {
|
|
this.hideTooltip()
|
|
},
|
|
buildData() {
|
|
this.data = []
|
|
|
|
var maxValue = 0
|
|
var minValue = 0
|
|
Object.values(this.daysListening).forEach((val) => {
|
|
if (val > maxValue) maxValue = val
|
|
if (!minValue || val < minValue) minValue = val
|
|
})
|
|
const range = maxValue - minValue + 0.01
|
|
|
|
for (let i = 0; i < this.daysToShow + 1; i++) {
|
|
const col = Math.floor(i / 7)
|
|
const row = i % 7
|
|
|
|
const date = i === 0 ? this.firstWeekStart : this.$addDaysToDate(this.firstWeekStart, i)
|
|
const dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
|
|
const datePretty = this.$formatJsDate(date, 'MMM d, yyyy')
|
|
const monthString = this.$formatJsDate(date, 'MMM')
|
|
const value = this.daysListening[dateString] || 0
|
|
const x = col * 13
|
|
const y = row * 13
|
|
|
|
var bgColor = this.bgColors[0]
|
|
var outlineColor = this.outlineColors[0]
|
|
if (value) {
|
|
outlineColor = this.outlineColors[1]
|
|
var percentOfAvg = (value - minValue) / range
|
|
var bgIndex = Math.floor(percentOfAvg * 4) + 1
|
|
bgColor = this.bgColors[bgIndex] || 'red'
|
|
}
|
|
|
|
this.data.push({
|
|
date,
|
|
dateString,
|
|
datePretty,
|
|
monthString,
|
|
dayOfMonth: Number(dateString.split('-').pop()),
|
|
yearString: dateString.split('-').shift(),
|
|
value,
|
|
col,
|
|
row,
|
|
style: `transform:translate(${x}px,${y}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
|
|
})
|
|
}
|
|
console.log('Data', this.data)
|
|
|
|
this.monthLabels = []
|
|
var lastMonth = null
|
|
for (let i = 0; i < this.data.length; i++) {
|
|
if (this.data[i].monthString !== lastMonth) {
|
|
const weekOfMonth = Math.floor(this.data[i].dayOfMonth / 7)
|
|
if (weekOfMonth <= 2) {
|
|
this.monthLabels.push({
|
|
id: this.data[i].dateString + '-ml',
|
|
label: this.data[i].monthString,
|
|
style: {
|
|
transform: `translate(${this.data[i].col * 13}px, -15px)`,
|
|
lineHeight: '10px',
|
|
fontSize: '10px'
|
|
}
|
|
})
|
|
lastMonth = this.data[i].monthString
|
|
}
|
|
}
|
|
}
|
|
},
|
|
init() {
|
|
const heatmapEl = document.getElementById('heatmap')
|
|
this.contentWidth = heatmapEl.clientWidth
|
|
this.maxInnerWidth = this.contentWidth - 52
|
|
this.buildData()
|
|
}
|
|
},
|
|
updated() {},
|
|
mounted() {
|
|
this.init()
|
|
},
|
|
beforeDestroy() {}
|
|
}
|
|
</script> |