Created
December 28, 2021 05:22
-
-
Save russelllim22/00b621bd7bfbb2e18913de5d14a0c655 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script> | |
import {scaleBand, scaleLinear} from 'd3-scale'; | |
import {csvParse} from 'd3'; | |
import * as cloud from 'd3-cloud' | |
import { fly} from 'svelte/transition'; | |
let years = [], selectedYear = 2020, textFile = "", nameData = [], letters = [], letterNames = {}, letterCounts = []; | |
for(let y = 1880; y<=2020; y++){ | |
years.push(y); | |
} | |
const cat10colors = ["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"]; | |
let yScale, xScale, totalBabies; | |
let width = 0; // will be updated based on window.innerWidth | |
const showBars = (textFile)=> { | |
letters = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]; | |
letterNames = {"other": []}; | |
letterCounts = []; | |
letters.forEach((letter)=>{ | |
letterNames[letter] = []; | |
letterCounts.push({"letter": letter, "count": 0, "words": []}) | |
}) | |
nameData = csvParse("word,gender,count\n" + textFile).map(d => ({ | |
text: d.word, | |
size: +d.count | |
})).sort((a,b) => a.size > b.size ? 1 : 0) | |
nameData.forEach((d)=>{ | |
const letter = d.text.slice(0,1); | |
letterNames[letter].push(d); | |
letterCounts.find(d => d.letter === letter).count += d.size; | |
}) | |
letterCounts.sort((a,b)=> b.count - a.count) | |
totalBabies = letterCounts.reduce((a, b) => a + b.count, 0); | |
const smallCounts = {letter: "other", count: 0, otherLetters: "", words: []} | |
for(let i = 25; i>=0; i--){ | |
if(letterCounts[i].count < 0.015*totalBabies){ | |
smallCounts.otherLetters += `${letterCounts[i].letter},`; | |
smallCounts.count += letterCounts[i].count; | |
letterNames["other"] = letterNames["other"].concat(letterNames[letterCounts[i].letter]) | |
letterCounts.splice(i,1) | |
} | |
} | |
letterCounts.sort((a,b)=> b.count - a.count) | |
letterCounts.push(smallCounts); | |
yScale = scaleBand() | |
.domain(letterCounts.map(d => d.letter)) | |
.range([0, 2000]) | |
.paddingInner(0.1) | |
.paddingOuter(0.1); | |
xScale = scaleLinear([0,letterCounts[0].count],[0,1000 - 150]); | |
for(let i=0; i<letterCounts.length; i++){ | |
const d = letterCounts[i]; | |
const words = [...letterNames[d.letter]].filter(e=>e.size>99); | |
const layout = cloud() | |
.size([xScale(d.count) + 10, yScale.bandwidth()]) | |
.words(words) | |
.text(d => d.text) | |
.font("Nunito") | |
.padding(1) | |
.rotate(0) | |
.fontSize(d => 3 + 0.25*Math.sqrt(d.size)) | |
setTimeout(()=>{ | |
layout.on("end", (words)=>{ | |
letterCounts.find(e => e.letter === d.letter).words = words; | |
letterCounts = letterCounts; // re-assignment reminds Svelte to update DOM | |
console.log('letterCounts') | |
console.log(letterCounts) | |
}) | |
layout.start(); | |
},1000*i + 500) | |
} | |
} | |
</script> | |
<svelte:head> | |
<title>Baby Names Word Cloud Bar Chart</title> | |
<link rel="preconnect" href="https://fonts.googleapis.com"> | |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700&display=swap" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css2?family=Asap+Condensed:wght@400;500&display=swap" rel="stylesheet"> | |
</svelte:head> | |
<svelte:window bind:innerWidth={width}/> | |
<section> | |
<h1>Baby Names Word Cloud Bar Chart</h1> | |
<label for="year"> | |
Select a year: | |
<select id="year" selected="2020" bind:value={selectedYear} | |
on:change="{ | |
async () => { | |
letterCounts = []; | |
let response = await fetch(`names data/yob${selectedYear}.txt`); | |
let newFile = await response.text(); | |
showBars(newFile); | |
} | |
}"> | |
{#each years.reverse() as year} | |
<option>{year}</option> | |
{/each} | |
</select> | |
</label> | |
<svg id="chartSVG" viewBox="0,0,1000,2000"> | |
<image width="1000px" height="2000px" style="opacity:0.2;" href="/babies-pixabay.png"> | |
</image> | |
{#if letterCounts.length > 0} | |
{#each letterCounts as d, i} | |
<rect x = 0 y = {yScale(d.letter)} width = {xScale(d.count)} height = {yScale.bandwidth()} | |
in:fly="{{ x: -1000, duration: 1000, delay: 1000*i}}"></rect> | |
<text class="bar-label" x = {xScale(d.count) + 15} y = {yScale(d.letter) + yScale.bandwidth()/2} | |
in:fly="{{ x: -1000, duration: 1000, delay: 1000 * i }}"> | |
{d.letter}: ({Math.round(d.count / totalBabies * 100)}%) | |
</text> | |
<g width = {xScale(d.count)} height = {yScale.bandwidth()} | |
style="transform: translate({xScale(d.count)/2}px,{yScale(d.letter) + yScale.bandwidth()/2}px); text-anchor: middle"> | |
{#if d.words.length > 0} | |
{#each d.words as word} | |
<text x={word.x} y={word.y} style="transform: translate({word.x}, {word.y}); | |
font-size: {word.size}px; | |
fill: {cat10colors[Math.floor(Math.random()*10)]}"> | |
{word.text} | |
</text> | |
{/each} | |
{/if} | |
</g> | |
{/each} | |
{/if} | |
</svg> | |
</section> | |
<style> | |
section{ | |
font-family: Nunito, sans-serif; | |
} | |
select, label{ | |
font-size: 1.2em; | |
} | |
#chartSVG{ | |
max-width: 100%; | |
margin-top: 1em; | |
border: 1px solid black; | |
background: radial-gradient(ellipse at bottom right, rgba(137,207,240,1) 20%, rgba(244,194,194,1) 70%); | |
} | |
#chartSVG rect{ | |
stroke: black; | |
fill: white; | |
stroke-width: 2; | |
} | |
.bar-label{ | |
font-size:28px; | |
fill: rgba(0, 0, 0, 0.8); | |
alignment-baseline: middle; | |
} | |
</style> |
Yes the re-assignment is necessary for an array, Svelte won't re-render based on a mutation of the array. I agree it does seem like a bit of a hack. But there's other ways to do it too, see here: https://svelte.dev/tutorial/updating-arrays-and-objects
Regarding putting logic in the HTML vs using JS to render elements into an empty HTML container, I actually like having more logic in the HTML. To me it's easier to see what is being added to the page. Because the async function was quite short, I dont mind having it inline in the HTML, if it was a longer function I would probably put it in a script tag.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just started looking into Svelte, and I've worked with D3 before, so this was a helpful example for me, thanks! I do have a few questions for you, if you don't mind. I like the idea of Svelte, but I saw your line 72:
Is this really necessary, or just a just-to-be-sure kind of thing? Either way, it kind of worries me that there will be "gotchas" like this when working with the language. This looks like a hack to me. I've worked a lot with AngularJS, and there would be times where, if data was modified by something outside of AngularJS's "scope" (e.g. WebSocket data, 3rd party library, etc.), you'd have to wrap it in AngularJS's
$timeout(() => {}, 0);
call, to schedule digestion of any changes.Also, in my experience with D3, all of the logic for creating elements necessary for display have been contained in the JS (via
selection.append('...')
), with the HTML only having andiv#chartContainer
to render into. It caught me a bit off-guard to see HTML with a full-blownasync
method defined in an attribute. Is that just your personal preference, or is that recommended for some specific reason?