Assume you have an SVG map like this below:
Above map shows the prefectures of Japan. But since all prefectures share the same color, it is not visible. Let's fill each prefecture with a different color:
It looks better now, doesn't it?
But we still don't know which color denotes which prefecture, so let's add a legend:
Okay, now we can see where all prefectures are located in Japan. But let's go one step further.
Instead of having a separate legend, which is lame and occupies a lot of space, let's add a hover function so that when you hover on a prefecture it's name is shown in a tooltip.
Much better right?
Note that the SVG map of Japan's prefecture were obtained from this GitHub repo. Adding tooltip/ working dynamically with SVG using Svelte was not as straightforward as I expected. I was assuming it'd take 30 mins to create above structures, but it took ~2 hours.
For example g elements inside SVG do not accept hover events. Also, I didn't want to duplicate the SVG in my file 3 times for the above maps, so I moved the SVG to a separate Svelte file, imported once, and cloned that SVG file when creating the maps. This makes the code much cleaner.
You can refer to the code below:
<script>
import { onMount } from 'svelte';
// Get the SVG file from GitHub repo: https://github.com/geolonia/japanese-prefectures/blob/master/map-full.svg
import Prefectures from './Prefectures.svelte';
function getUniqueColor(n) {
const rgb = [0, 0, 0];
for (let i = 0; i < 24; i++) {
rgb[i % 3] <<= 1;
rgb[i % 3] |= n & 0x01;
n >>= 1;
}
return '#' + rgb.reduce((a, c) => (c > 0x0f ? c.toString(16) : '0' + c.toString(16)) + a, '');
}
let container, svg, colorfulSvg, tooltipSvg, tooltip;
$: tooltip = null;
let prefectures = [];
let noOfPefectures = 47;
let randomColors = Array.from({ length: noOfPefectures }, (_, index) => index)
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => getUniqueColor(value));
onMount(() => {
svg = container.firstChild;
colorfulSvg = svg.cloneNode(true);
colorfulSvg.getElementById('prefectures').childNodes.forEach((node, index) => {
let color = randomColors[index];
let code = node.getAttribute('data-code');
let titleText = node.getElementsByTagName('title')[0].textContent;
let title = {
en: titleText.split('/')[1].trim(),
tr: titleText.split('/')[1].trim(),
jp: titleText.split('/')[0].trim()
};
prefectures = [...prefectures, { color, title, code }];
node.setAttribute('fill', randomColors[index]);
});
document.getElementById('colorfulContainer').appendChild(colorfulSvg);
tooltipSvg = colorfulSvg.cloneNode(true);
tooltipSvg.getElementById('prefectures').childNodes.forEach((node, index) => {
let span = document.createElement('span');
span.classList.add('tooltiptext');
span.innerHTML = prefectures[index].title[$language];
node.classList.add('tooltip');
node.appendChild(span);
});
document.getElementById('tooltipContainer').appendChild(tooltipSvg);
document.addEventListener('mouseover', function (e) {
if (!e.target.closest('#tooltip')) {
tooltip.style.visibility = 'hidden';
}
let target = e.target.closest('#tooltipContainer #prefectures g');
if (target) {
let elemRect = target.getBoundingClientRect();
let bodyRect = document.getElementById('tooltipContainer').getBoundingClientRect();
tooltip.style.top = `${elemRect.top - bodyRect.top}px`;
tooltip.style.left = `${elemRect.left - bodyRect.left}px`;
tooltip.style.visibility = 'visible';
let titleText = target.getElementsByTagName('title')[0].textContent;
tooltip.textContent =
$language == 'jp' ? titleText.split('/')[0].trim() : titleText.split('/')[1].trim();
}
});
});
</script>
<div id="mainContainer" class="flex gap-4 flex-col relative">
<p>Original MAP</p>
<div class="flex justify-center" bind:this={container}><Prefectures /></div>
<p>Colorful MAP</p>
<div class="flex justify-center" id="colorfulContainer"></div>
<p>Legend</p>
{#key prefectures}
<div class="flex flex-wrap gap-2">
{#each prefectures.sort((a, b) => a.code - b.code) as pref}
<span
class="text-sm text-white px-2 py-1 rounded-full"
style="background-color: {pref.color}">{pref.title[$language]}</span
>
{/each}
</div>
{/key}
<p>Tooltipped MAP</p>
<div class="flex justify-center relative" id="tooltipContainer">
<span
style="visiblity:hidden;"
bind:this={tooltip}
id="tooltip"
class="absolute px-2 py-1 rounded bg-white/[0.9]"
></span>
</div>
</div>
In the next blogpost, let's show some stats when you click on a prefecture.
Until then, happy hacking!