Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display more stats #86

Open
wants to merge 4 commits into
base: gdpr-chart-zoom-2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions client/css/spotify-gdpr-time-stats.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
* {
box-sizing: border-box;
}

#main-container {
display: flex;
flex-flow: column nowrap;
align-items: stretch;
width: 100%;
margin: 1rem 0;
}

#gdpr-time-stats-hero {
display: flex;
flex-flow: column nowrap;
align-items: center;
margin: 2rem 0;

font-weight: 700;
font-size: 1.5rem;
}

#gdpr-time-stats-hero p {
margin: 1rem 0;
}

#gdpr-time-stats-total {
color: var(--color-accent-primary-1);
font-size: 7rem;
font-family: 'Work Sans', sans-serif;
}

#gdpr-time-stats-breakdown {
display: grid;
grid-template-columns: 1fr 1fr 1fr;

font-weight: 900;
}

#gdpr-time-stats-breakdown section {
display: flex;
flex-flow: column nowrap;
align-items: center;
}

.gdpr-time-stats-breakdown-number {
color: var(--color-accent-primary-2);
font-size: 3rem;
font-family: 'Work Sans', sans-serif;
}

#gdpr-time-stats-artists {
margin: 1rem 0;
}

#gdpr-time-stats-artists-graph {
position: relative;
}

#gdpr-time-stats-artists-graph div {
margin: 0.5rem 0;
padding: 0.5rem;
overflow: visible;

background-image: linear-gradient(to right, var(--color-accent-primary-1), var(--color-accent-primary-2));
background-size: 100%;
background-attachment: fixed;
}

#gdpr-time-stats-artists-graph .artist-name {
font-weight: 700;
}
28 changes: 18 additions & 10 deletions client/js/gdpr/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import {showPicker} from '../file-upload/gdrive-picker.js';
import './chart.js';
import './table.js';
import './time-stats.js';

const {zip} = window;

Expand All @@ -23,6 +24,9 @@ const chart = document.getElementById('gdpr-chart');
/** @type {import('./table.js').GdprTable} */
const table = document.getElementById('gdpr-table');

/** @type {import('./time-stats.js').GdprTimeStats} */
const timeStats = document.getElementById('gdpr-time-stats');

const btnUploadFile =
/** @type {HTMLButtonElement} */
(document.getElementById('btn-data-upload'));
Expand All @@ -47,14 +51,7 @@ btnUploadFile.addEventListener('click', async () => {
const fileUpload = inputUpload.files.item(0);
if (!fileUpload) return;

const records = await getStreamingData(fileUpload);

const collatedRecords = collateStreamingData(records);

const rankings = getStreamingHistory(collatedRecords);

chart.load(rankings.history, rankings.dates);
table.load(rankings.history, rankings.dates);
await processUpload(fileUpload);
});

const btnPickFile =
Expand All @@ -64,12 +61,23 @@ const btnPickFile =
btnPickFile.addEventListener('click', async () => {
const fileUpload = await showPicker();

const records = await getStreamingData(fileUpload);
await processUpload(fileUpload);
});

/**
* Processes a GPDR data file uploaded by the user and displays the
* visualization.
*
* @param {Blob} blob The file that was uploaded.
*/
async function processUpload(blob) {
const records = await getStreamingData(blob);

const collatedRecords = collateStreamingData(records);

const rankings = getStreamingHistory(collatedRecords);

timeStats.load(records);
chart.load(rankings.history, rankings.dates);
table.load(rankings.history, rankings.dates);
});
}
128 changes: 128 additions & 0 deletions client/js/gdpr/time-stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const MS_PER_HOUR = 60 * 60 * 1000;

export class GdprTimeStats extends HTMLElement {
constructor() {
super();

this.attachShadow({mode: 'open'});

this.mainContainer = document.createElement('div');
this.mainContainer.id = 'main-container';

this.mainContainer.innerHTML = `
<div id="gdpr-time-stats-hero">
<p>You've spent</p>
<span id="gdpr-time-stats-total">?</span>
<p>hours listening to music on Spotify<p>
</div>
<div id="gdpr-time-stats-breakdown">
<section>
<span
id="gdpr-time-stats-this-year"
class="gdpr-time-stats-breakdown-number">
?
</span>
<p>hours this year so far</p>
</section>
<section>
<span
id="gdpr-time-stats-last-year"
class="gdpr-time-stats-breakdown-number">
?
</span>
<p>hours last year</p>
</section>
<section>
<span
id="gdpr-time-stats-old"
class="gdpr-time-stats-breakdown-number">
?
</span>
<p>hours before last year</p>
</section>
</div>
<div id="gdpr-time-stats-artists">
<h3>Breakdown by artist</h3>
<div id="gdpr-time-stats-artists-graph">
</div>
</div>
`;

const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/css/spotify-gdpr-time-stats.css';

this.shadowRoot.append(stylesheet, this.mainContainer);
}

/**
* Loads raw GDPR data into this time statistics control.
*
* @param {import("./analysis").GDPRRecord[]} records The GDPR data to load.
*/
load(records) {
const currentYear = new Date().getFullYear();

const thisYearRecords = records
.filter((record) => record.endTime.getFullYear() === currentYear);
const lastYearRecords = records
.filter((record) => record.endTime.getFullYear() === currentYear - 1);
const oldRecords = records
.filter((record) => record.endTime.getFullYear() < currentYear - 1);

const thisYearTime = thisYearRecords
.reduce((time, record) => time + record.msPlayed, 0);
const lastYearTime = lastYearRecords
.reduce((time, record) => time + record.msPlayed, 0);
const oldTime = oldRecords
.reduce((time, record) => time + record.msPlayed, 0);

const totalTime = thisYearTime + lastYearTime + oldTime;

const timeByArtist = records
.reduce((map, record) => {
map.set(
record.artistName,
(map.get(record.artistName) || 0) + record.msPlayed,
);
return map;
}, new Map());

const timeByArtistSorted = Array.from(timeByArtist)
.sort((entryA, entryB) => entryB[1] - entryA[1])
.slice(0, 50);

this.shadowRoot.getElementById('gdpr-time-stats-total').innerText =
(totalTime / MS_PER_HOUR).toFixed(0);

this.shadowRoot.getElementById('gdpr-time-stats-this-year').innerText =
(thisYearTime / MS_PER_HOUR).toFixed(0);

this.shadowRoot.getElementById('gdpr-time-stats-last-year').innerText =
(lastYearTime / MS_PER_HOUR).toFixed(0);

this.shadowRoot.getElementById('gdpr-time-stats-old').innerText =
(oldTime / MS_PER_HOUR).toFixed(0);

const artistGraph =
this.shadowRoot.getElementById('gdpr-time-stats-artists-graph');

for (const [artistName, msPlayed] of timeByArtistSorted) {
const bar = document.createElement('div');
// how much this artist has been played in comparison to first-place
// artist
const proportion = msPlayed / timeByArtistSorted[0][1];

const hoursPlayed = msPlayed / MS_PER_HOUR;

bar.style.width = `${proportion * 100}%`;
bar.innerHTML = `
<span class='artist-name'>${artistName}</span>
<span class='artist-time'>${hoursPlayed.toFixed(1)} hours</span>`;

artistGraph.appendChild(bar);
}
}
}

customElements.define('gdpr-time-stats', GdprTimeStats);
2 changes: 2 additions & 0 deletions client/spotify-gdpr.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ <h2 class="header">Get started</h2>
<gdpr-table id="gdpr-table" chart="gdpr-chart"></gdpr-table>
</section>

<gdpr-time-stats id="gdpr-time-stats"></gdpr-time-stats>

<h2 class="header">How it works</h2>
<p>
The GDPR is a European law that requires tech companies to let you download the data that
Expand Down