1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
|
+++
title = "Rockbox Stat Tracking"
date = "2025-09-02"
+++
<article>
<section>
<div class="text-section">
<p>In this post I talk about how I went about setting up a <a href="/rockstats" target="_blank">stat visualization page</a> for my rockbox mp3 player.</p>
</div>
<figure class="cover-image">
<svg width="100%" height="360" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 640 360">
<rect width="640" height="360" x="0" y="0" fill="rgb(0,0,0)" fill-opacity="0"></rect>
<polyline points="427.2 233.9 440.6 240.7 455.6 240.7" fill="transparent" stroke="#e67e80" class="zr0-cls-0"></polyline>
<polyline points="202.3 203.4 192.6 206.3 177.6 206.3" fill="transparent" stroke="#e69875" class="zr0-cls-0"></polyline>
<polyline points="220 113.6 197.5 135.5 182.5 135.5" fill="transparent" stroke="#dbbc7f" class="zr0-cls-0"></polyline>
<polyline points="273.2 69.5 190.3 117.4 190.3 117.4" fill="transparent" stroke="#a7c080" class="zr0-cls-0"></polyline>
<polyline points="304.6 61 201.7 99.3 201.7 99.3" fill="transparent" stroke="#83c092" class="zr0-cls-0"></polyline>
<polyline points="310.7 60.4 217.9 81.2 217.9 81.2" fill="transparent" stroke="#7fbbb3" class="zr0-cls-0"></polyline>
<polyline points="315.4 60.1 242.2 63.1 242.2 63.1" fill="transparent" stroke="#d699b6" class="zr0-cls-0"></polyline>
<polyline points="318.6 60 318.4 45 303.4 45" fill="transparent" stroke="#f85552" class="zr0-cls-0"></polyline>
<path d="M320 60A120 120 0 1 1 223.6 251.5L320 180Z" fill="#e67e80" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="0" ecmeta_ssr_type="chart" class="zr0-cls-1"></path>
<path d="M223.6 251.5A120 120 0 0 1 203.6 150.7L320 180Z" fill="#e69875" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="1" ecmeta_ssr_type="chart" class="zr0-cls-2"></path>
<path d="M203.6 150.7A120 120 0 0 1 247.9 84.1L320 180Z" fill="#dbbc7f" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="2" ecmeta_ssr_type="chart" class="zr0-cls-3"></path>
<path d="M247.9 84.1A120 120 0 0 1 301.3 61.5L320 180Z" fill="#a7c080" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="3" ecmeta_ssr_type="chart" class="zr0-cls-4"></path>
<path d="M301.3 61.5A120 120 0 0 1 307.8 60.6L320 180Z" fill="#83c092" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="4" ecmeta_ssr_type="chart" class="zr0-cls-5"></path>
<path d="M307.8 60.6A120 120 0 0 1 313.6 60.2L320 180Z" fill="#7fbbb3" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="5" ecmeta_ssr_type="chart" class="zr0-cls-6"></path>
<path d="M313.6 60.2A120 120 0 0 1 317.2 60L320 180Z" fill="#d699b6" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="6" ecmeta_ssr_type="chart" class="zr0-cls-7"></path>
<path d="M317.2 60A120 120 0 0 1 320 60L320 180Z" fill="#f85552" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="7" ecmeta_ssr_type="chart" class="zr0-cls-8"></path>
<text dominant-baseline="central" text-anchor="start" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(460.5907 240.6868)" fill="var(--fg)">Progressive Rock</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(172.5853 206.2935)" fill="var(--fg)">Alternative</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(177.5244 135.5094)" fill="var(--fg)">Post-Rock</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(185.3496 117.4094)" fill="var(--fg)">Post-Hardcore</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(196.7011 99.3094)" fill="var(--fg)">Post-Metal</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(212.8733 81.2094)" fill="var(--fg)">Rock</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(237.2337 63.1094)" fill="var(--fg)">Shoegaze</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(298.4082 45.0094)" fill="var(--fg)">Progressive Metal</text>
<style>
.zr0-cls-0:hover {
cursor:pointer;
}
.zr0-cls-1:hover {
cursor:pointer;
fill:rgba(253,138,140,1);
}
.zr0-cls-2:hover {
cursor:pointer;
fill:rgba(253,167,128,1);
}
.zr0-cls-3:hover {
cursor:pointer;
fill:rgba(240,206,139,1);
}
.zr0-cls-4:hover {
cursor:pointer;
fill:rgba(183,211,140,1);
}
.zr0-cls-5:hover {
cursor:pointer;
fill:rgba(144,211,160,1);
}
.zr0-cls-6:hover {
cursor:pointer;
fill:rgba(139,205,196,1);
}
.zr0-cls-7:hover {
cursor:pointer;
fill:rgba(235,168,200,1);
}
.zr0-cls-8:hover {
cursor:pointer;
fill:rgba(255,93,90,1);
}
</style>
</svg>
<figcaption>A static site generation experiment</figcaption>
</figure>
</section>
<section class="text-section">
<h2>Preamble: Digital Sovereignity & Rockbox</h2>
<p>
I've been building up a pretty sizeable collection of digital music
over the last couple of years. I think there's a lot of value in owning
the music I pay for and being able to choose how I listen to it.
Purchasing music also allows me to support artists in a more direct
and substantial way than the fractions of cents for using streaming services,
but that's more of a happy consequence than some moral obligation I feel.
</p>
<p>
Over the years, I've enjoyed listening to my music in a variety of ways.
For years I kept all of my music files on all of my devices and used
various local music clients depending on the platform, most notably mpd
and ncmpcpp on linux. Eventually, as I charged headlong into the glorious
world of self-hosting, I began using a central Jellyfin media server that
I stream music and video from. It's super convenient, and works on all of
my devices (including my TV!).
</p>
<p>
My media server is great, and it's been the primary way I listen to music
for a while now. But it has limitations. For example, I don't expose my media
server to the internet, so I'm unable to stream from it while I'm out and
about. And even if I could, the bandwidth requirements would be pretty high.
I figured I would need a dedicated music player if I wanted to take my music
library on the go, and settled on the HIFI Walker H2 after reading some
online recommendations. The ability to install <a href="https://rockbox.org" target="_blank">Rockbox</a>, an open-source firmware,
was a big factor in my decision. I couldn't tell you how the device works
out of the box, since I flashed the firmware pretty much immediately once I got it,
but I've been super impressed with how the device works while running Rockbox.
</p>
<p>
<figure class="fig fig-right">
<img src="player.bmp" alt="Screenshot of Rockbox player showing cool theme.">
<figcaption>I'm using a modified version of the <a
href="https://themes.rockbox.org/index.php?themeid=3266&target=aigoerosq"
target="_blank">InfoMatrix-v2</a> theme, which looks great.</figcaption>
</figure>
Rockbox comes with many codecs for common audio formats including FLAC and MP3. The
device boots extremely quickly, and the interface is snappy. Virtually every aspect
of the user experience is tweakable and customizable to a crazy degree. I've even begun
listening to music on my player even at home, since a device specifically for the
purpose provides less distraction while I'm trying to be productive.
</p>
<p>
All this to say I'm pretty much sold on Rockbox. But there's certain things I
still miss from my days of being a user of popular services like Spotify with
fancy APIs and data tracking. Things like Spotify wrapped or third-party apps
for visualizing playback statistics are a fun way to see what my listening history
looks like and could potentially be used to help find more music that I'd enjoy.
This is why when I noticed that Rockbock has a playback logging feature, a little
lightbulb lit up over my head.
</p>
</section>
<section class="text-section">
<h2>Generating and Parsing Logs</h2>
<p>
<figure class="fig fig-right">
<img src="log-setting.bmp" alt="Logging">
<figcaption>The logging feature can be accessed through the settings menu.</figcaption>
</figure>
Rockbox has a feature that logs playback information to a text file. This feature can
be enabled by setting <b>Playback Settings > Logging</b> to "On". With this setting enabled, a
new line gets added to the end of the <b>.rockbox/playback.log</b> file every time you play a track,
containing info about what you played and when.
</p>
<p>
The logging feature is actually already used by the LastFM scrobbler plugin that comes preloaded with
Rockbox, which is probably the simplest way to get insights into your playback. However,
I personally want to avoid using third-party services as much as possible, because it's more fun.
</p>
<p>
If I take a look at a logfile generated after a bit of listening, I'll see that I've wound up with
a series of lines that each look something like this:
<figure class="fig">
<pre><samp>1758478258:336689:336718:/<microSD0>/Music/This Is The Glasshouse/This Is The Glasshouse - 867/This Is The Glasshouse - 867 - 01 Streetlight By Streetlight.flac</samp></pre>
<figcaption>An example of a log entry for "Streetlight by Streetlight" by This is the Glasshouse.
</figcaption>
</figure>
</p>
<p>
I wasn't really able to find any information online about the format of these logs, but they appear
to be simple enough to figure out. From what I can tell, each event is broken up into 4 pieces:
<ol>
<li><b>Timestamp:</b> The number of milliseconds since the UNIX epoch.
<li><b>Playback Duration:</b> The amount of the song that was played, in milliseconds.
<li><b>Total Track Length:</b> The length of the played track, in milliseconds.
<li><b>File Path:</b> An absolute path to the file containing the track on the filesystem.
</ol>
All of this is enough to know what I was listening to and when. I can use the file path to check for
audio tags which can help glean even more information about my listening habits.
</p>
<p>Now that I have this information and know how to interpret it, I'm ready to start processing it!</p>
</section>
<section class="text-section">
<h2>Analyzing Playback History</h2>
<p>
In order to get some useful information out of my playback history, I think it's a good idea to start by
building
a database. I created a sqlite database with the following tables:
<div class="sunken-panel" style="width: min-content;">
<table class="schema-table">
<thead>
<tr>
<th colspan="3">songs</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>i64</td>
<td>PK</td>
</tr>
<tr>
<td>title</td>
<td>String</td>
<td></td>
</tr>
<tr>
<td>artists</td>
<td>JSON</td>
<td></td>
</tr>
<tr>
<td>album_id</td>
<td>i64?</td>
<td></td>
</tr>
<tr>
<td>genre</td>
<td>String?</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="sunken-panel" style="width: min-content;">
<table class="schema-table">
<thead>
<tr>
<th colspan="3">albums</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>i64</td>
<td>PK</td>
</tr>
<tr>
<td>title</td>
<td>String</td>
<td></td>
</tr>
<tr>
<td>artist</td>
<td>String</td>
<td></td>
</tr>
<tr>
<td>cover_art</td>
<td>Blob?</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="sunken-panel" style="width: min-content;">
<table class="schema-table">
<thead>
<tr>
<th colspan="3">history</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>i64</td>
<td>PK</td>
</tr>
<tr>
<td>timestamp</td>
<td>Datetime</td>
<td></td>
</tr>
<tr>
<td>duration</td>
<td>i64</td>
<td></td>
</tr>
<tr>
<td>song_id</td>
<td>i64</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<br>
I can add more columns later, but this is a good place to start.
</p>
<p>
Now, as I read through the logfile line-by-line, I can check if each album exists before
inserting it into the database:
<figure class="fig">
<pre><code>for line in log_file.lines().flatten() {
println!("{line}");
// Skip comments
if line.starts_with("#") {
continue;
}
let chunks = line.split(":").collect::<Vec<_>>();
let timestamp = DateTime::from_timestamp_secs(
i64::from_str_radix(chunks[0], 10).context("Failed to parse timestamp")?,
)
.context("Failed to convert timestamp")?;
// Load tags from file on device
let file_path = chunks[chunks.len() - 1][1..]
.split_once("/")
.context("Missing file")?
.1;
let tags = Tag::new()
.read_from_path(args.mount_point.join(file_path))
.context("Failed to read audio tags")?;
//...
}</code></pre>
<figcaption>Parsing log entry and loading audio metadata.</figcaption>
</figure>
<figure class="fig">
<pre><code>if let Some(existing_album) =
sqlx::query("SELECT id FROM albums WHERE title=$1 AND artist=$2")
.bind(album_title)
.bind(album_artist)
.fetch_optional(&mut *db)
.await
.context("Failed to execute query to find existing album")?
{
let album_id: i64 = existing_album.get("id");
info!("Album already exists, id {album_id}");
//...
} else {
info!("Inserting new album: {album_title} by {album_artist}");
//...
let result = sqlx::query(
"INSERT INTO albums (title, artist, cover_art) VALUES ($1, $2, $3);",
)
.bind(album_title)
.bind(album_artist)
.bind(cover)
.execute(&mut *db)
.await
.context("Failed to execute query to insert album into database")?;
//...
}</code></pre>
<figcaption>Checking for an album with matching artist and title before creating a new row in the
database.</figcaption>
</figure>
I did something similar with the <b>songs</b> and <b>history</b> tables, basically building up a cache
of history information and skipping anything that's already in the database on repeat runs.
</p>
<p>
With this database constructed, it's pretty easy to get a bunch of different information
about my listening. For example (forgive me if my SQL skills are kind of ass lol):
<figure class="fig">
<pre><code>SELECT
songs.title AS song_title,
songs.artists AS song_artists,
songs.genre AS song_genre,
albums.title AS album_title,
albums.artist AS album_artist,
history.timestamp AS timestamp,
history.duration AS duration
FROM history
CROSS JOIN songs ON songs.id = history.song_id
CROSS JOIN albums ON albums.id = songs.album_id
ORDER BY timestamp DESC;</code></pre>
<figcaption>Querying for a list of each history entry along with track metadata, sorted from most to
least recent.</figcaption>
</figure>
<figure class="fig">
<pre><code>SELECT
songs.genre,
SUM(history.duration) AS total_duration
FROM history
CROSS JOIN songs ON history.song_id = songs.id
GROUP BY genre
ORDER BY total_duration DESC
LIMIT 10; </code></pre>
<figcaption>Querying for the top 10 most listened genres by playtime.</figcaption>
</figure>
</p>
<p>
It's all well and good to be able to view this information using a database client,
but it would be really cool if I could visualize this data somehow.
</p>
</section>
<section class="text-section">
<h2>Visualizing this Data Somehow</h2>
<p>
I wanted to make this data available on my website for people to view, and for a bunch of mostly trivial
reasons I won't get into here, I have a couple of requirements for pages on this site:
<ol>
<li>Pages need to be static.
<li>Pages need to be JavaScript-free.
</ol>
This means any chart rendering needs to be done automatically at build time before
deploying. I don't currently use a static site generator for my site (just for fun),
so I'm basically going to need to write one specifically to generate this page.
</p>
<p>
I won't get too deep into the specifics of how I queried the database and generated each visualization
on
the page, but I can explain the visualizations I created using the queries from the previous section.
For the
listening history I wanted to generate a table displaying the information. To accomplish this, I first
used a combination of <a href="https://crates.io/crates/sqlx" target="_blank">sqlx</a>'s ability to convert a row to a struct and <a href="https://crates.io/crates/serde" target="_blank">serde</a> to serialize
the rows as JSON values.
<figure class="fig">
<pre><code>#[derive(Serialize, Deserialize, FromRow)]
struct HistoryEntry {
song_title: String,
song_artists: Value,
timestamp: DateTime<Utc>,
duration: i64,
album_title: String,
album_artist: Option<String>,
song_genre: Option<String>,
}
//...later
let history = sqlx::query_as::<_, HistoryEntry>(
/* SELECT... */
).fetch_all(&mut *db).await;
//...later still, tera context accepts
let mut context = tera::Context::new();
context.insert("history", &history);
</code></pre>
<figcaption>Struct definition for a history entry, allowing conversion from a sqlx row and
de/serialization from/to JSON.</figcaption>
</figure>
</p>
<p>
In order to keep the generation as painless as possible, I decided to use the <a href="https://keats.github.io/tera" target="_blank">Tera</a> template
engine, which allows me to define a template HTML file and substitute in values from
a context which I can define before rendering. In the case of the table, I can just generate a <code><tr></code>
matching the data for each item:
<figure class="fig">
<pre><code>{% macro history_table(history) %}
<h3>Playback History</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Played Duration</th>
<th>Title</th>
<th>Artists</th>
<th>Album</th>
<th>Genre</th>
</tr>
</thead>
<tbody>
{% for item in history %}<tr>
<td>{{ item.timestamp | date(format="%Y-%m-%d %H:%M:%S") }}</td>
<td>{{ item.duration | hms }}</td>
<td>{{ item.song_title }}</td>
<td>{{ item.song_artists }}</td>
<td>{{ item.album_title }}</td>
<td>{{ item.song_genre }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endmacro history_table %}</code></pre>
<figcaption>
A Tera macro for generating a table from a list of playback history items.
I used a macro so I can re-use this later if I want to add time range views.
(last month, year, etc.)
</figcaption>
</figure>
</p>
<p>
I wrote similar macros for each of the visualizations I wanted to create. Most are
easy, but for my top 10 genres I wanted to display a pie chart. I found a pretty decent
data visualization crate called <a href="https://crates.io/crates/charming" target="_blank">charming</a> that's able to render to html, however
the output contains javascript so it's a no-go for me. Luckily, it can also render to
an SVG which I can embed nicely within the page.
<figure class="fig">
<svg width="100%" height="360" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 640 360">
<rect width="640" height="360" x="0" y="0" fill="rgb(0,0,0)" fill-opacity="0"></rect>
<polyline points="427.2 233.9 440.6 240.7 455.6 240.7" fill="transparent" stroke="#e67e80" class="zr0-cls-0"></polyline>
<polyline points="202.3 203.4 192.6 206.3 177.6 206.3" fill="transparent" stroke="#e69875" class="zr0-cls-0"></polyline>
<polyline points="220 113.6 197.5 135.5 182.5 135.5" fill="transparent" stroke="#dbbc7f" class="zr0-cls-0"></polyline>
<polyline points="273.2 69.5 190.3 117.4 190.3 117.4" fill="transparent" stroke="#a7c080" class="zr0-cls-0"></polyline>
<polyline points="304.6 61 201.7 99.3 201.7 99.3" fill="transparent" stroke="#83c092" class="zr0-cls-0"></polyline>
<polyline points="310.7 60.4 217.9 81.2 217.9 81.2" fill="transparent" stroke="#7fbbb3" class="zr0-cls-0"></polyline>
<polyline points="315.4 60.1 242.2 63.1 242.2 63.1" fill="transparent" stroke="#d699b6" class="zr0-cls-0"></polyline>
<polyline points="318.6 60 318.4 45 303.4 45" fill="transparent" stroke="#f85552" class="zr0-cls-0"></polyline>
<path d="M320 60A120 120 0 1 1 223.6 251.5L320 180Z" fill="#e67e80" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="0" ecmeta_ssr_type="chart" class="zr0-cls-1"></path>
<path d="M223.6 251.5A120 120 0 0 1 203.6 150.7L320 180Z" fill="#e69875" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="1" ecmeta_ssr_type="chart" class="zr0-cls-2"></path>
<path d="M203.6 150.7A120 120 0 0 1 247.9 84.1L320 180Z" fill="#dbbc7f" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="2" ecmeta_ssr_type="chart" class="zr0-cls-3"></path>
<path d="M247.9 84.1A120 120 0 0 1 301.3 61.5L320 180Z" fill="#a7c080" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="3" ecmeta_ssr_type="chart" class="zr0-cls-4"></path>
<path d="M301.3 61.5A120 120 0 0 1 307.8 60.6L320 180Z" fill="#83c092" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="4" ecmeta_ssr_type="chart" class="zr0-cls-5"></path>
<path d="M307.8 60.6A120 120 0 0 1 313.6 60.2L320 180Z" fill="#7fbbb3" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="5" ecmeta_ssr_type="chart" class="zr0-cls-6"></path>
<path d="M313.6 60.2A120 120 0 0 1 317.2 60L320 180Z" fill="#d699b6" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="6" ecmeta_ssr_type="chart" class="zr0-cls-7"></path>
<path d="M317.2 60A120 120 0 0 1 320 60L320 180Z" fill="#f85552" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="7" ecmeta_ssr_type="chart" class="zr0-cls-8"></path>
<text dominant-baseline="central" text-anchor="start" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(460.5907 240.6868)" fill="var(--fg)">Progressive Rock</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(172.5853 206.2935)" fill="var(--fg)">Alternative</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(177.5244 135.5094)" fill="var(--fg)">Post-Rock</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(185.3496 117.4094)" fill="var(--fg)">Post-Hardcore</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(196.7011 99.3094)" fill="var(--fg)">Post-Metal</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(212.8733 81.2094)" fill="var(--fg)">Rock</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(237.2337 63.1094)" fill="var(--fg)">Shoegaze</text>
<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(298.4082 45.0094)" fill="var(--fg)">Progressive Metal</text>
<style>
.zr0-cls-0:hover {
cursor:pointer;
}
.zr0-cls-1:hover {
cursor:pointer;
fill:rgba(253,138,140,1);
}
.zr0-cls-2:hover {
cursor:pointer;
fill:rgba(253,167,128,1);
}
.zr0-cls-3:hover {
cursor:pointer;
fill:rgba(240,206,139,1);
}
.zr0-cls-4:hover {
cursor:pointer;
fill:rgba(183,211,140,1);
}
.zr0-cls-5:hover {
cursor:pointer;
fill:rgba(144,211,160,1);
}
.zr0-cls-6:hover {
cursor:pointer;
fill:rgba(139,205,196,1);
}
.zr0-cls-7:hover {
cursor:pointer;
fill:rgba(235,168,200,1);
}
.zr0-cls-8:hover {
cursor:pointer;
fill:rgba(255,93,90,1);
}
</style>
</svg>
<figcaption>Here's one I generated just now.</figcaption>
</figure>
</p>
<p>
And that's pretty much all there is to it! The finished thing can be found <a href="/rockstats" target="_blank">here</a>.
</p>
</section>
</article>
|