arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
jellyfin-tools/jellyfin-year-review/web/src/lib/RadarChart.svelte
jellyfin-year-review/web/src/lib/RadarChart.svelteRaw
 1<script>
 2  import { t, locale } from './i18n.js';
 3  import { radarGeometry } from './radar.js';
 4
 5  let { values, labels, metric = t('h') } = $props();
 6  const fmt = value => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
 7  let geometry = $derived(radarGeometry(values));
 8  let points = $derived(geometry.axes.slice(0, values.length).map(axis => `${280 + axis.valueX * 150},${240 + axis.valueY * 150}`).join(' '));
 9  let description = $derived(labels.map((label, index) => `${label}: ${fmt(values[index])} ${metric}`).join('; '));
10</script>
11
12<figure class="radar-chart">
13  <figcaption>{t('Genre radar chart')}</figcaption>
14  <div class="radar-plot">
15  <svg viewBox="0 0 560 480" role="img" aria-label={description}>
16    <title>{t('Genre radar chart')}</title>
17    {#each [0.25, 0.5, 0.75, 1] as level}
18      <polygon class="grid-ring" points={geometry.axes.map(axis => `${280 + axis.x * 150 * level},${240 + axis.y * 150 * level}`).join(' ')} />
19      <text class="scale" x="286" y={240 - 150 * level + 12}>{fmt(geometry.max * level)} {metric}</text>
20    {/each}
21    {#each geometry.axes as axis}
22      <line class="axis" x1="280" y1="240" x2={280 + axis.x * 150} y2={240 + axis.y * 150} />
23    {/each}
24    {#if values.length >= 3}
25      <polygon class="radar-area" {points} />
26    {:else}
27      <polyline class="radar-line" {points} />
28    {/if}
29    {#each values as value, index}
30      {@const axis = geometry.axes[index]}
31      <circle class="radar-point" cx={280 + axis.valueX * 150} cy={240 + axis.valueY * 150} r="4">
32        <title>{labels[index]}: {fmt(value)} {metric}</title>
33      </circle>
34    {/each}
35  </svg>
36  {#each values as value, index}
37    {@const axis = geometry.axes[index]}
38    <div class="axis-label" style:left="{50 + axis.x * 194 / 560 * 100}%" style:top="{50 + axis.y * 194 / 480 * 100}%" aria-hidden="true" title={labels[index]}>
39      <b>{labels[index]}</b><span>{fmt(value)} {metric}</span>
40    </div>
41  {/each}
42  </div>
43</figure>
44
45<style>
46  .radar-chart { width: 100%; max-width: 850px; margin: 36px 0 0; }
47  figcaption { font-size: 1rem; font-weight: 700; }
48  .radar-plot { position: relative; max-width: 650px; margin: 0 auto; }
49  svg { display: block; width: 100%; height: auto; }
50  .grid-ring { fill: none; stroke: currentColor; stroke-opacity: .2; }
51  .axis { stroke: currentColor; stroke-opacity: .15; }
52  .radar-area { fill: currentColor; fill-opacity: .18; stroke: currentColor; stroke-width: 2; stroke-linejoin: round; }
53  .radar-line { fill: none; stroke: currentColor; stroke-width: 2; }
54  .radar-point { fill: currentColor; }
55  .scale { fill: currentColor; font-size: 10px; opacity: .65; }
56  .axis-label { position: absolute; transform: translate(-50%, -50%); width: 23%; display: flex; flex-direction: column; gap: 3px; text-align: center; font-size: clamp(10px, 1.3vw, 14px); line-height: 1.2; overflow-wrap: anywhere; }
57  .axis-label b { display: -webkit-box; line-clamp: 2; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
58  .axis-label span { font-size: .85em; }
59  @media (max-width: 760px) { .scale { font-size: 18px; } }
60</style>