diff --git a/.gitignore b/.gitignore index 6fecae67..5b4779cf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,11 @@ npm-debug.log yarn.lock package.lock -source/msi-4096/scenarios.tsv \ No newline at end of file +source/dvs-privacy/membership.json +explorable-ml + +source/forecast-correlation/data/ +source/forecast-correlation/node_modules/ + + + diff --git a/README.md b/README.md index 031ec93f..7c041473 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [roadtolarissa.com](http://roadtolarissa.com/) ``` -yarn run start +yarn start ``` -Changes to JS/CSS in `source/` are injected without a reload. Edits to `source/_posts/*.md` trigger an incremental rebuild and hard reload client side. +Changes to JS/CSS in `source/` are [injected without a reload](http://roadtolarissa.com/hot-reload). Edits to `source/_posts/*.md` trigger [an incremental rebuild](https://roadtolarissa.com/literate-blogging/) and hard client side reload. diff --git a/package.json b/package.json index 0db82dd4..0e7baf14 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { - "name": "static-site", + "license": "MIT", "dependencies": { "chokidar": "^1.7.0", - "highlight.js": "^9.12.0", + "highlight.js": "11", "hot-server": "^0.0.16", "lit-node": "^0.0.1", "marked": "^0.3.6" }, "scripts": { - "start": "lit-node source/_posts/2017-11-12-literate-blogging.md --watch & cd public/ && hot-server", - "publish": "lit-node source/_posts/2017-11-12-literate-blogging.md index.js && rsync -a --omit-dir-times --no-perms public/ demo@roadtolarissa.com:../../usr/share/nginx/html/" + "start": "mkdir -p public && yarn lit-node source/_posts/2018-05-24-literate-blogging.md --watch & cd public/ && hot-server", + "pub": "mkdir -p public && yarn lit-node source/_posts/2018-05-24-literate-blogging.md && rsync -a --omit-dir-times --no-perms public/ demo@roadtolarissa.com:../../usr/share/nginx/html/", + "homepage": "source/homepage/update.sh" } } diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/source/_posts/2012-09-27-first-post.md b/source/_posts/2012-09-27-first-post.md index cf7b71de..c4622e83 100644 --- a/source/_posts/2012-09-27-first-post.md +++ b/source/_posts/2012-09-27-first-post.md @@ -1,6 +1,7 @@ --- title: First Post template: post.html +date: 2012-09-27 permalink: /first-post --- I've found that writing todo lists enormously increases my productivity while programming. Programming is all about breaking larger problems into smaller problems over and over until the problems are small enough to solve easily. Lists provide a map of the problems that need solving and make it possible to work on a smaller subset of them without having to simultaneously worry about the global structure of the program. @@ -107,7 +108,7 @@ https://news.ycombinator.com/item?id=9177469 ### Difference between playoff and regular season win rates - Cubs got swept in playoffs after sweeping during season -### [Remake trib baseball chart](https://bl.ocks.org/1wheel/0fe7b82d7c188c2d26a3) +### [Remake trib baseball chart](https://blocks.roadtolarissa.com/1wheel/0fe7b82d7c188c2d26a3) - https://twitter.com/waynekamidoi/status/657359312520638464 - https://twitter.com/kleinmatic/status/657400344394276864 @@ -130,6 +131,10 @@ https://news.ycombinator.com/item?id=9177469 ### Log scales are pretty great! - https://www.washingtonpost.com/national/americas-new-tobacco-crisis-the-rich-stopped-smoking-the-poor-didnt/2017/06/13/a63b42ba-4c8c-11e7-9669-250d0b15f83b_story.html?tid=ss_tw&utm_term=.aab13303a01e +- https://www.washingtonpost.com/business/2018/10/30/owning-your-own-home-doesnt-make-you-rich-owning-somebody-elses-does/?utm_term=.80dc77d29762 +- https://fivethirtyeight.com/features/which-countries-terrorist-attacks-are-ignored-by-the-u-s-media/ +- https://www.nytimes.com/2014/04/23/upshot/the-american-middle-class-is-no-longer-the-worlds-richest.html +- https://www.washingtonpost.com/graphics/2018/business/hq-trivia/?noredirect=on&utm_term=.4b8a7ca8e79c (percent remaining chart) ### Which team is the best at 2-1? - look at scoring at the end of the quarter @@ -153,8 +158,10 @@ https://news.ycombinator.com/item?id=9177469 - During a rare happy stretch of Philly's blowout loss to the Hawks on Friday, the Sixers' broadcast team pointed out that Philly was on "an 8-3 run." I know the Sixers aren't good enough to put together normal NBA runs, but an 8-3 scoring gap is not a run. It is a randomly occurring blip that happens every game. - Which players are the streakest/have the most improbable runs? www.espn.com/nba/story/_/page/presents-19573519/heating-fire-klay-thompson-truth-hot-hand-nba - Which players do the best/worst after hiting/missing a FT? Do some players always go 7/10 or do some go 5/10 and 10/10 more often than they would randomly. +- What's the closest game ever? Has any game stayed within 10 points? 5 points? +- Which game had the shortest scoring streak? -### [Curl states into each other](https://bl.ocks.org/1wheel/4c1658719cfec9ac352ff1e0edc51317) +### [Curl states into each other](https://blocks.roadtolarissa.com/1wheel/4c1658719cfec9ac352ff1e0edc51317) - https://medium.com/@zachlieberman/land-lines-e1f88c745847#.t1x3ozhdt - noah's stuff @@ -209,9 +216,10 @@ https://news.ycombinator.com/item?id=9177469 - http://www.wsj.com/graphics/build-your-own-trading-bot/?mod=e2twg - Scatter plot of buy after tk days of gains/sell after tk days of losses -### new hp +### [new hp](https://roadtolarissa.com) - some sort of 3d scroll zoom/rotate -- once saw a nice portfolio site with sum(1/2^n) = 1. they had done something with game of thrones but can't find +- once saw a nice portfolio site with sum(1/2^n) = 1. they had worked on http://www.themill.com/portfolio/3861/winter-is-here%3A-facebook-ar-effects, but I can't find their site. +- https://beta.observablehq.com/@mbostock/golden-mona-lisa ### Structure of fiction - http://twinegarden.tumblr.com/ @@ -243,7 +251,7 @@ https://news.ycombinator.com/item?id=9177469 ### Encoding line width - setting y0 and y1 doesn't work on a slope - https://www.axios.com/countries-gdp-this-century-2484484895.html -- https://bl.ocks.org/veltman/e45cc3a2670779a0bc942ba18163228f +- https://blocks.roadtolarissa.com/veltman/e45cc3a2670779a0bc942ba18163228f - https://www.washingtonpost.com/graphics/2017/politics/tax-breaks/ ### Hapax Legomena @@ -251,7 +259,7 @@ https://news.ycombinator.com/item?id=9177469 ### [Hacking Hot Reloading](https://roadtolarissa.com/hot-reload/) -### That blog post about how the blog is set up +### [That blog post about how the blog is set up](https://roadtolarissa.com/literate-blogging/) ### Flappy Beep @@ -261,7 +269,7 @@ https://news.ycombinator.com/item?id=9177469 - and an explanation layer ### delaunay-triangulation -- https://bl.ocks.org/1wheel/6cd8f2c2be0bd0bde0818fc8c0fb895a +- https://blocks.roadtolarissa.com/1wheel/6cd8f2c2be0bd0bde0818fc8c0fb895a ### Kelly betting - https://www.gwern.net/Coin-flip @@ -275,7 +283,9 @@ https://news.ycombinator.com/item?id=9177469 - "No one ever got fired for buying IBM" - The paper gets things I know about totally wrong, but I trust them on everything else From Michael Crichton? - “It is difficult to get a man to understand something, when his salary depends on his not understanding it.” - +- "little know" tips on reddit +- https://tinysubversions.com/stuff/known-places/ +- every "there's always a tweet", connections between original date and rt What does reddit repeat? @@ -283,26 +293,294 @@ What does reddit repeat? - Scroll down and see all the movies that have been the 'best selling' - https://www.axios.com/black-panther-box-office-titanic-top-3-north-america-avatar-star-wars-32a35770-59fc-4ccc-bd5b-3a85d7144266.html -### MSI 2018 +### [MSI 2018](https://roadtolarissa.com/msi-4096/) - https://www.nytimes.com/elections/2012/results/president/scenarios.html ### Spurs 50 win streak - every streak line of 50+ wins on some kind of scatter plot? - compare to other sports with win percent? +- https://cleaningtheglass.com/two-for-one-or-two-for-none/ +### [2018 chart diary](https://roadtolarissa.com/2018-chart-diary/) -### 2018 chart diary -### New homepage zoooom +### [New homepage zoooom](https://roadtolarissa.com) - https://beta.observablehq.com/@mbostock/golden-mona-lisa +### DISSOLVING THE FERMI PARADOX +- http://slatestarcodex.com/2018/07/03/ssc-journal-club-dissolving-the-fermi-paradox/ + + +### 3d animation thinger +- https://imgur.com/a/5nu25 +- https://www.youtube.com/watch?v=FGhr3kvaWcs +- https://www.reddit.com/r/place/comments/64d8vy/3d_visualization_timelapse_of_place/ +- "Microdisplacement in Cycles", "Displacement texture", "Animated textures in Cycles" +- https://www.reddit.com/r/dataisbeautiful/comments/8v0m98/oc_3d_animation_of_chinas_nitrogen_dioxide/ + + +### Winning by at least 17 after Q1 -> final margin of victory +- https://twitter.com/jon_bois/status/1000580952047931393 +- What's the biggest drop in win probabily at each minute of the game? +- Most lead changes? + + +### animated illusions +- https://vimeo.com/287093172 +- https://www.youtube.com/watch?v=DkVOIJAaWO0&index=120&list=WL&t=67s + + +### rotating vector field +- https://www.windy.com/?40.781,-73.977,5 +- want to smoothy animate a hurricane wind map + + +### zoom into a pixel looping forever +- https://twitter.com/jashkenas/status/1044370517522694144f + + +### most popular canceled show +- tuca and bertie + + +### continuity +- https://www.reddit.com/r/nba/comments/ch7kr5/oc_we_may_never_see_roster_continuity_like_we_saw/ +- want a box for each player, gets 1px higher each year + + +### [spotify webplayer winamp skin](https://roadtolarissa.com/winampify/) +- don't like looking at ads to listen to music! +- why? +- - new pornographer low density screenshot +- - https://www.stereogum.com/2105993/pavement-harness-your-hopes-spotify/columns/sounding-board/ + + +### weather charts +- https://weatherspark.com/ +- a map with showing them in a tooltip? +- two of them showing 5% hottest and 95% hottest. and you can adjust the width of the spread +- show the actual hourly values for a given year + + +### aita +- identify score with gpt2? +- see ratings that change over time (auto find edits) +- just show the distributions of all the scores for all posts in a year +- https://public.tableau.com/profile/farhan7#!/vizhome/AmITheAssholeApeakintoallDilemmas__/TopicClusters + + +### [wild fire indexing](https://blocks.roadtolarissa.com/1wheel/46874895034f5bded13c97097bf25a83) +- https://twitter.com/damiandn/status/1216002437137469441 + + +### year end slug consistency +- https://lisawaananenjones.com/noted/2020/01/03/end-of-year-graphics-lists/ +- 2015-year-in-graphics + + +### Bulls are 1-16 v teams with a winning record and in 9th seed +- Jan 2020 + + +### LeBron beat Kemba 28 times in a row + +### Lebron hit 5 threes in a few minutes +- fastest streak of shots? +- longest streak of no misses? +- longest streak of no one else shooting? + +### visual snakes and ladders + +- https://www.research.ibm.com/haifa/ponderthis/challenges/February2020.html + + + +### Best winning streak of a time period +- color by win rate or min wins a season +- spurs made playoffs / 50 wins forever + + +### hyper cube portfolio page +- https://www.youtube.com/watch?v=q5Qh2XpoCsY + + +### usable spiral portfolio +- add sidebar of things (maybe horizontal on mobile?) +- hover highlights square + + +### zoomable dorling cartogram +- https://www.nytimes.com/interactive/2020/02/03/us/elections/results-iowa-caucus-precinct-map.html +- construct topology and check angle + + +### are cow's actually better repped in the senate? +- https://www.washingtonpost.com/opinions/2020/01/16/are-cows-better-represented-senate-than-people/?arc404=true +- compare gini coeffient +- find closest plant or animal + + +### Airbnb lawsuits over time +- https://www.bloomberg.com/graphics/2020-airbnb-ipo-challenges/ +- the number of lawsuits looks like it dropped in 2020, but that's because it doesn't show ones that haven't been filed +- make animation showing what this looks like over time +- make alternative + + +### d3 learning +- update to v6 +- link to others? +- separate from projects + + +### chicago neighborhoods +- https://pair-code.github.io/interpretability/uncertainty-over-space/neighborhood/ +- svg2tiles lib to converty + + +### chart podcast audio +- https://www.adblockradio.com/blog/2018/11/15/designing-audio-ad-block-radio-podcast/ +- 1 px is about 5 sec. show intro, ads, outro and content in different colors +- click to play! +- programmatic ads might mess this up + +### chart TAL replays +- air date v show index scatter plot is obv, but swoopy lines would be fun too +- diff transcripts to show updates + + +### local observable +- store state is query str +- node interop +- https://github.com/asg017/unofficial-observablehq-compiler + + +### umap of amazon review for a product +- color by date +- "When we become aware of negative changes, we investigate and often retest to determine whether we need to update our recommendation. For example, staff writer Sarah Witman looks for patterns: If one review mentions an issue with an item’s handle, for example, she’ll search for other reviews with the keyword “handle” to see if the issue is isolated or if it seems to be part of a trend—a good strategy for reviewers and readers alike" +- https://thewirecutter.com/blog/why-some-picks-have-bad-reviews/ +- filter by score / length / fakespot + + +### codenames embeddings +- looking at CLS token delta might be better than comparing raw word embedding +- maybe look at early layer? + + +### every bitcoin explanation +- replies to jk rowling tweak, maybe BERT + UMAP +- [CLS embedding](https://huggingface.co/transformers/quickstart.html#bert-example) +- [scatter plot with a 100k points](https://observablehq.com/@bmschmidt/zoom-strategies-for-huge-scatterplots-with-three-js) + +### commute v. cost +- http://www.verysmallarray.com/best-new-york-city-neighborhoods-again/ +- mouseover map to update scatter plot + + +### hacker news reposts +- Network graph showing links to old discussions +- Which page has been submitted the most? (only include on per month or something like that...) +- https://news.ycombinator.com/item?id=23775707 + + +### scotus wars +- network diagram or grid +- https://scotuswars.gilslotd.com/ + + +### stock returns +- which stock has had the best return since the pandemic? +- http://benschmidt.org/mvp/#-Batting-oWAR-counting +- http://amandacox.github.io/img/big/returns.png +- Bottom triangle shows which have done the worst (what would have done the best if you shorted) +- scrolly intro building chart or live updating thing? +- [tidyquant](https://cran.r-project.org/web/packages/tidyquant/vignettes/TQ05-performance-analysis-with-tidyquant.html) for data + + +### Momentum transition +- https://blocks.roadtolarissa.com/1wheel/54f90245720d7e3210cea1edfa42089b +- it isn't a change in like absolute capacity, but i'm really interested in interfaces that don't gate interaction behind animation - don't think my version is there yet, but you can start using tools in totally different ways if there isn't latency. +- 538 game theory +- wsj stock picker +- propublica immigration slider + + +### Multi dimensional arraybuffers for js +- ndarray +- node write out npy files +- https://github.com/aplbrain/npyjs +- https://github.com/NicholasTancredi/read-npy-file +- hdfs +- apache arrow? +- https://google.github.io/flatbuffers/ +- https://github.com/GoogleChromeLabs/buffer-backed-object + +### forecast outliers + + +### track flair changes on subreddits +- https://www.reddit.com/r/nba/comments/ix0s3f/charania_paul_george_was_preaching_to_teammates/g63r5bm/ +- i think there was a NYT or 538 graphic about searches or something like that for 2014 world cup - after a country is elimated, who do their fans cheer for? + +### umap + oeis +- http://www.sspectra.com/math/MovieFrames/ + +### Animal pictures +- https://storage.googleapis.com/openimages/web/visualizer/index.html?set=train&type=segmentation&r=false&c=%2Fm%2F0gd36 +- bing api? + +### Basketball WP by margin +- trace out paths of games +- https://twitter.com/inpredict/status/1314734826696503298 +- https://roadtolarissa.com/golf-paths/ + +### NBA team changes +- [Zion is now the longest tenured Pelican](https://www.reddit.com/r/nba/comments/jyifrm/zion_is_now_the_longest_tenured_pelican/) +- Is there, like, a knicks team made up mostly of former suns players or something like that? +- Which team has had a player for the longest? Which team has had the lowest % returning players ever? + +### RCP polling +- Cut off dates for polls were selectively picked; make a chart that shows shifting day windows and how it always errors for GOP +- https://github.com/1wheel/scraping-2018/tree/master/rcp-avg +- https://www.nytimes.com/2020/11/17/us/politics/real-clear-politics.html + +### Citibike + +- Which stations have no bikes or too many bikes? +- How does this change throughout the day / over time +- https://www.citibikenyc.com/system-data + +### Distribution of gymnastics ages over time +- https://thecorrespondent.com/739/why-womens-gymnastics-is-legal-child-abuse/1740638383-e7ff4274 + +### weatherspark +- https://weatherspark.com/y/23912/Average-Weather-in-New-York-City-New-York-United-States-Year-Round +- show distribution instead of mean, facet by hour or month instead +- 3d? +- or change over years + +### Covid polarization but with bars + +- https://kieranhealy.org/blog/archives/2021/10/30/the-polarization-of-death/ + +### Rewrite NYT webheadline as a print +- fine tune t5 +### Has the NYT stopped reviewing as many off broadway shows? +### Custom tool to make diverging color scales +### LeBron Point Record +- Show every season as a grid games, color by points or other things +- NYT/WaPo did vertical lines which aren't readable (he's played 1421 games!) +### LeBron reddit nick names +### Smushed Voroni +> “One-dimensional” is a slight misnomer: the pointerX and pointerY transforms consider distance in both dimensions, but the distance along the non-dominant dimension is divided by 100. +https://observablehq.com/plot/interactions/pointer diff --git a/source/_posts/2012-09-29-maximin-connect-4-completed.md b/source/_posts/2012-09-29-maximin-connect-4-completed.md index d7364df5..d47d361f 100644 --- a/source/_posts/2012-09-29-maximin-connect-4-completed.md +++ b/source/_posts/2012-09-29-maximin-connect-4-completed.md @@ -1,6 +1,7 @@ --- title: Maximin Connect 4 Completed template: post.html +date: 2012-09-29 permalink: /maximin-connect-4-completed --- I've finished working on the Maximin Connect 4 program. I've set up a [page][1] where you can play around with the AI since I'm not sure how to embed HTML in these posts. Some general things I learned (I'll make a separate post with more Connect 4 specific ideas once I've run some more tests on the AI): diff --git a/source/_posts/2012-09-30-connect-4-ai-how-it-works.md b/source/_posts/2012-09-30-connect-4-ai-how-it-works.md index 74cfc201..b0e44f2b 100644 --- a/source/_posts/2012-09-30-connect-4-ai-how-it-works.md +++ b/source/_posts/2012-09-30-connect-4-ai-how-it-works.md @@ -1,6 +1,7 @@ --- title: Connect 4 AI: How it Works template: post.html +date: 2012-09-30 permalink: /connect-4-ai-how-it-works --- The [connect 4 playing program][1] uses a [minmax algorithm][2]. diff --git a/source/_posts/2012-10-06-speed-issues-with-gokos-online-dominion-implementation.md b/source/_posts/2012-10-06-speed-issues-with-gokos-online-dominion-implementation.md index 8b4fc118..64d7df8e 100644 --- a/source/_posts/2012-10-06-speed-issues-with-gokos-online-dominion-implementation.md +++ b/source/_posts/2012-10-06-speed-issues-with-gokos-online-dominion-implementation.md @@ -1,6 +1,7 @@ --- title: Speed Issues Goko Dominion template: post.html +date: 2012-10-06 permalink: /speed-issues-with-gokos-online-dominion-implementation --- This isn't very scientific, but to get a better sense of how slow playing [Dominion][1] on  [goko][2] is compared to [isotropic][3] and to find out exactly what is making it slower, I spent an hour on both and recorded how the time was spent: diff --git a/source/_posts/2012-10-10-yglesias-on-amazons-pe-ratio.md b/source/_posts/2012-10-10-yglesias-on-amazons-pe-ratio.md index df58bb9d..a51a7b05 100644 --- a/source/_posts/2012-10-10-yglesias-on-amazons-pe-ratio.md +++ b/source/_posts/2012-10-10-yglesias-on-amazons-pe-ratio.md @@ -1,6 +1,7 @@ --- title: Yglesias on Amazon's P/E Ratio template: post.html +date: 2012-10-10 permalink: /yglesias-on-amazons-pe-ratio --- Yglesias [wonders][1] why Amazon's P/E is so high: diff --git a/source/_posts/2012-10-19-reddit-comment-visualizer.md b/source/_posts/2012-10-19-reddit-comment-visualizer.md index bf8deb6f..15c011c2 100644 --- a/source/_posts/2012-10-19-reddit-comment-visualizer.md +++ b/source/_posts/2012-10-19-reddit-comment-visualizer.md @@ -1,6 +1,7 @@ --- title: Reddit Comment Visualizer template: post.html +date: 2012-10-19 permalink: /reddit-comment-visualizer --- I've spent the last few days working on a [visualizer for reddit comments][1].  Using reddit's API, the program downloads a user's comments and graphs them with [flot][2]. diff --git a/source/_posts/2012-11-17-redditgraphs-retrospective.md b/source/_posts/2012-11-17-redditgraphs-retrospective.md index 0194784b..bc83c332 100644 --- a/source/_posts/2012-11-17-redditgraphs-retrospective.md +++ b/source/_posts/2012-11-17-redditgraphs-retrospective.md @@ -1,6 +1,7 @@ --- title: Redditgraphs Retrospective template: post.html +date: 2012-11-17 permalink: /redditgraphs-retrospective --- It's been nearly a month since my last post, about a comment visualizer I created for reddit. Since then, I've mostly been polishing the application and trying to share it with people. After posting the basic demo on /r/javascript, I was encouraged make improvements and host the project on its own domain. Registering “[redditgraphs.com][1]” for a year only cost $5 and it seemed more memorable and easier to access than “roadtolarissa.com/javascript/reddit-comment-visualizer“. I spent another week adding functionality – hourly trends, weekly trends, direct linking to user names – and making the UI prettier. diff --git a/source/_posts/2012-11-20-next-project.md b/source/_posts/2012-11-20-next-project.md index 5e83e91a..01515100 100644 --- a/source/_posts/2012-11-20-next-project.md +++ b/source/_posts/2012-11-20-next-project.md @@ -1,6 +1,7 @@ --- title: Next Project template: post.html +date: 2012-11-20 permalink: /next-project --- Finished with redditgraphs, I have a couple of ideas about what I'd like to work on next; I'm posting them to clarify my own thoughts and to get feedback. diff --git a/source/_posts/2012-12-19-interactive-visualization-of-white-house-petition-signatures.md b/source/_posts/2012-12-19-interactive-visualization-of-white-house-petition-signatures.md index 9a2bbb0b..233867db 100644 --- a/source/_posts/2012-12-19-interactive-visualization-of-white-house-petition-signatures.md +++ b/source/_posts/2012-12-19-interactive-visualization-of-white-house-petition-signatures.md @@ -1,6 +1,7 @@ --- title: White House Petition Signatures template: post.html +date: 2012-12-19 permalink: /interactive-visualization-of-white-house-petition-signatures --- diff --git a/source/_posts/2012-12-21-zoomable-sierpinski-triangle-with-d3-js.md b/source/_posts/2012-12-21-zoomable-sierpinski-triangle-with-d3-js.md index 58066004..88ed8216 100644 --- a/source/_posts/2012-12-21-zoomable-sierpinski-triangle-with-d3-js.md +++ b/source/_posts/2012-12-21-zoomable-sierpinski-triangle-with-d3-js.md @@ -1,6 +1,7 @@ --- title: Zoomable Sierpinski Triangle template: post.html +date: 2012-12-21 permalink: /zoomable-sierpinski-triangle-with-d3-js ---

diff --git a/source/_posts/2013-01-21-unemployment-rates.md b/source/_posts/2013-01-21-unemployment-rates.md index cbfd640e..7dfa8c1a 100644 --- a/source/_posts/2013-01-21-unemployment-rates.md +++ b/source/_posts/2013-01-21-unemployment-rates.md @@ -1,6 +1,7 @@ --- title: Unemployment Rates template: post.html +date: 2013-01-21 permalink: /unemployment-rates --- [][1] diff --git a/source/_posts/2013-01-28-whale-words.md b/source/_posts/2013-01-28-whale-words.md index 1bd72659..1c569dd6 100644 --- a/source/_posts/2013-01-28-whale-words.md +++ b/source/_posts/2013-01-28-whale-words.md @@ -1,6 +1,7 @@ --- title: Whale Words template: post.html +date: 2013-01-28 permalink: /whale-words --- [][1] diff --git a/source/_posts/2013-05-02-film-strips-post.md b/source/_posts/2013-05-02-film-strips-post.md index d5e5887c..acb937ce 100644 --- a/source/_posts/2013-05-02-film-strips-post.md +++ b/source/_posts/2013-05-02-film-strips-post.md @@ -1,6 +1,7 @@ --- title: Film Strips template: post.html +date: 2013-05-02 permalink: /film-strips-post --- [filmstrips][1] diff --git a/source/_posts/2013-05-19-meteor-map.md b/source/_posts/2013-05-19-meteor-map.md index 712dc0ee..e0c5d12d 100644 --- a/source/_posts/2013-05-19-meteor-map.md +++ b/source/_posts/2013-05-19-meteor-map.md @@ -1,6 +1,7 @@ --- title: Meteor Map template: post.html +date: 2013-05-19 permalink: /meteor-map --- [meteor map][1] diff --git a/source/_posts/2013-06-16-nba_draft.md b/source/_posts/2013-06-16-nba_draft.md index 02237728..f3ed82ea 100644 --- a/source/_posts/2013-06-16-nba_draft.md +++ b/source/_posts/2013-06-16-nba_draft.md @@ -1,6 +1,7 @@ --- title: NBA Draft template: post.html +date: 2013-06-16 permalink: /nba-draft ---

diff --git a/source/_posts/2013-10-22-twisters-post.md b/source/_posts/2013-10-22-twisters-post.md index 96ba7657..c22b0bf8 100644 --- a/source/_posts/2013-10-22-twisters-post.md +++ b/source/_posts/2013-10-22-twisters-post.md @@ -1,6 +1,7 @@ --- title: Twisters template: post.html +date: 2013-10-22 permalink: /twisters-post ---

@@ -46,7 +47,7 @@ But! I haven’t posted anything for four months and I’m looking forwa [1]: http://www.roadtolarissa.com/wp-content/uploads/2013/10/ok.png [2]: https://github.com/mbostock/d3/wiki/Zoom-Behavior - [3]: http://bl.ocks.org/mbostock/4699541 + [3]: http://blocks.roadtolarissa.com/mbostock/4699541 [4]: https://github.com/1wheel/tornado-tuners/blob/master/matchStates.py [5]: http://www.roadtolarissa.com/wp-content/uploads/2013/10/arc.png [6]: http://www.roadtolarissa.com/zoomable-sierpinski-triangle-with-d3-js/ diff --git a/source/_posts/2014-01-15-synth-scales.markdown b/source/_posts/2014-01-15-synth-scales.markdown index ce3bd8e9..558070be 100644 --- a/source/_posts/2014-01-15-synth-scales.markdown +++ b/source/_posts/2014-01-15-synth-scales.markdown @@ -1,6 +1,7 @@ --- template: post.html title: Making Music with d3 +date: 2014-01-15 permalink: /synth --- diff --git a/source/_posts/2014-03-06-population-division.markdown b/source/_posts/2014-03-06-population-division.markdown index 7bca5a01..cc67c69a 100644 --- a/source/_posts/2014-03-06-population-division.markdown +++ b/source/_posts/2014-03-06-population-division.markdown @@ -1,6 +1,7 @@ --- template: post.html title: Population Division +date: 2014-03-06 permalink: /population-division --- diff --git a/source/_posts/2014-06-23-even-fewer-lamdas-with-d3.markdown b/source/_posts/2014-06-23-even-fewer-lamdas-with-d3.markdown index 31164539..3937f8bd 100644 --- a/source/_posts/2014-06-23-even-fewer-lamdas-with-d3.markdown +++ b/source/_posts/2014-06-23-even-fewer-lamdas-with-d3.markdown @@ -1,9 +1,10 @@ --- template: post.html title: Even Fewer Lambdas +date: 2014-06-23 permalink: /even-fewer-lamdas-with-d3 --- -Writing d3 typically involves writing lots of anonymous functions. The [scatter plot](http://bl.ocks.org/mbostock/3887118) example illustrates two typical use cases: scales and attributes. +Writing d3 typically involves writing lots of anonymous functions. The [scatter plot](http://blocks.roadtolarissa.com/mbostock/3887118) example illustrates two typical use cases: scales and attributes. ## Scale computations diff --git a/source/_posts/2014-08-05-215-teeth.md b/source/_posts/2014-08-05-215-teeth.md index 960a77fe..1ee5f7f3 100644 --- a/source/_posts/2014-08-05-215-teeth.md +++ b/source/_posts/2014-08-05-215-teeth.md @@ -1,6 +1,7 @@ --- template: post.html title: 215 teeth / 1008 beats +date: 2014-08-05 permalink: /215-teeth --- @@ -22,7 +23,7 @@ While (I think) this is nicely elegant exploitation of 7, 8 and 9's relative pri I'm getting to work on more straightforward visualizations of data at [my job](http://www.bloomberg.com/visual-data) now; hoping to post more experimental work with [algorithms](http://bost.ocks.org/mike/algorithms/) and/or [rhythm](http://www.pianophase.com/) here soon. -Code for [gears](https://github.com/1wheel/roadtolarissa/blob/master/source/javascripts/posts/synthComp/gears.js) (drawing heavily from [Bostock’s Epicyclic Gearing bl.ocks](http://bl.ocks.org/mbostock/1353700) and [audio](https://github.com/1wheel/roadtolarissa/blob/master/source/javascripts/posts/synthComp/audio.js)) is on github. +Code for [gears](https://github.com/1wheel/roadtolarissa/blob/master/source/javascripts/posts/synthComp/gears.js) (drawing heavily from [Bostock’s Epicyclic Gearing bl.ocks](http://blocks.roadtolarissa.com/mbostock/1353700) and [audio](https://github.com/1wheel/roadtolarissa/blob/master/source/javascripts/posts/synthComp/audio.js)) is on github. diff --git a/source/_posts/2014-08-19-drawdown.md b/source/_posts/2014-08-19-drawdown.md index ab3ad731..22da7c17 100644 --- a/source/_posts/2014-08-19-drawdown.md +++ b/source/_posts/2014-08-19-drawdown.md @@ -1,6 +1,7 @@ --- template: post.html title: Drawdown +date: 2014-08-19 permalink: /drawdown --- @@ -33,7 +34,7 @@ var peak = 0; var n = prices.length for (var i = 1; i < n; i++){ dif = prices[peak] - prices[i]; - peak = dif < 0 ? i : j; + peak = dif < 0 ? i : peak; maxDrawdown = maxDrawdown > dif ? maxDrawdown : dif; } ``` diff --git a/source/_posts/2014-10-04-golf-paths.markdown b/source/_posts/2014-10-04-golf-paths.markdown index 06d7ff84..7dff9604 100644 --- a/source/_posts/2014-10-04-golf-paths.markdown +++ b/source/_posts/2014-10-04-golf-paths.markdown @@ -1,5 +1,6 @@ --- template: post.html +date: 2014-10-04 permalink: /golf-paths title: Golf Paths --- diff --git a/source/_posts/2014-10-19-dragon-curve.markdown b/source/_posts/2014-10-19-dragon-curve.markdown index 48e67301..ce06eba6 100644 --- a/source/_posts/2014-10-19-dragon-curve.markdown +++ b/source/_posts/2014-10-19-dragon-curve.markdown @@ -1,5 +1,6 @@ --- template: post.html +date: 2014-10-19 permalink: /dragon-curve title: Dragon Curve --- diff --git a/source/_posts/2014-12-24-convex-hulls.markdown b/source/_posts/2014-12-24-convex-hulls.markdown index 0b4c03c2..8e18ba29 100644 --- a/source/_posts/2014-12-24-convex-hulls.markdown +++ b/source/_posts/2014-12-24-convex-hulls.markdown @@ -1,6 +1,7 @@ --- template: post.html title: Convex Hulls +date: 2014-12-24 permalink: /convex-hulls --- diff --git a/source/_posts/2015-01-04-coloring-maps-with-d3.markdown b/source/_posts/2015-01-04-coloring-maps-with-d3.markdown index 4867cb07..e93a3b4b 100644 --- a/source/_posts/2015-01-04-coloring-maps-with-d3.markdown +++ b/source/_posts/2015-01-04-coloring-maps-with-d3.markdown @@ -1,6 +1,7 @@ --- template: post.html title: Coloring Maps +date: 2015-01-04 permalink: /blog/2015/01/04/coloring-maps-with-d3 --- @@ -12,7 +13,7 @@ This post describes several [d3 quantitative scales](https://github.com/mbostock We start with an array of objects - `places` - representing the filled in areas on the right choropleth. Each has a `value` property equal to a number that we'll encode as a color using the `colorScale` defined in the center code snippet. The scatter plot on the left shows the distribution of values. -The code in the center uses a few libraries: `purples` an array of 5 [colorbrewer](http://bl.ocks.org/mbostock/5577023) purple shades, `_` [library](https://lodash.com/) of helper functions, `ss` [simple-statistics](http://www.macwright.org/simple-statistics/), `ƒ` a [field accessor](http://roadtolarissa.com/blog/2014/06/23/even-fewer-lamdas-with-d3/), and `d3` [itself](http://d3js.org). +The code in the center uses a few libraries: `purples` an array of 5 [colorbrewer](http://blocks.roadtolarissa.com/mbostock/5577023) purple shades, `_` [library](https://lodash.com/) of helper functions, `ss` [simple-statistics](http://www.macwright.org/simple-statistics/), `ƒ` a [field accessor](http://roadtolarissa.com/blog/2014/06/23/even-fewer-lamdas-with-d3/), and `d3` [itself](http://d3js.org). \ No newline at end of file diff --git a/source/_posts/2017-11-25-d3-mp4.md b/source/_posts/2017-11-25-d3-mp4.md index dd13cb45..a454922b 100644 --- a/source/_posts/2017-11-25-d3-mp4.md +++ b/source/_posts/2017-11-25-d3-mp4.md @@ -1,13 +1,14 @@ --- template: post.html title: D3 to MP4 +date: 2017-11-25 permalink: /d3-mp4 draft: false --- -Generating a high-resolution video from a d3 animation is tricky. [LICEcap](https://www.cockos.com/licecap/) and QuickTime screen recording work in a pinch, but they aren't scriptable and lose FPS without a beefy video card. +Generating a [high-resolution video](https://www.nytimes.com/interactive/2018/01/24/world/is-there-something-wrong-with-democracy.html) from a d3 animation is tricky. [LICEcap](https://www.cockos.com/licecap/) and QuickTime screen recording work in a pinch, but they aren't scriptable and lose FPS without a beefy video card. -Noah Veltman has [written about](https://github.com/veltman/gifs) and [presented](http://slides.com/veltman/d3unconf/#/) different techniques for exporting d3 graphics. The best way I've found of exporting video come from him and uses a delightful hack: [modifying time itself](https://bl.ocks.org/veltman/5de325668417b1d504dc). +Noah Veltman has [written about](https://github.com/veltman/gifs) and [presented](http://slides.com/veltman/d3unconf/#/) different techniques for exporting d3 graphics. The best way I've found of exporting video come from him and uses a delightful hack: [modifying time itself](https://blocks.roadtolarissa.com/veltman/5de325668417b1d504dc). ## Mutate Time @@ -17,18 +18,14 @@ Inside of your clientside code, overwrite [performance.now](https://developer.mo if (document.URL.includes('d3-video-recording')){ window.currentTime = 0 performance.now = () => currentTime - window.setTime = t => curTime = t - - var graphSel = d3.select('html') - .st({width: 1920, height: 1080, position: 'absolute'}) } ``` -This code only runs if the url contains `d3-video-recording`, making it easy to toggle between automatic and manual animations with a query string. It also sets the chart's dimensions to `1920x1080` and positions it at the upper-left corner of the screen so cropping will be simple. +This code only runs if the url contains `d3-video-recording`, making it easy to toggle between automatic and manual animations with a query string. ## Take Screenshots -[puppeteer](https://github.com/GoogleChrome/puppeteer) loads the page, moving time forward slowly and taking a screenshot over and over again. Even though each screenshot takes over half a second to render, controlling the browser's perception of time ensures no frames are dropped. +[puppeteer](https://github.com/GoogleChrome/puppeteer) loads the page, moving time forward slowly and taking a screenshot over and over again. Even though each screenshot takes over [half a second](https://bugs.chromium.org/p/chromium/issues/detail?id=741689&can=1&q=is%3Astarred%20&colspec=ID%20Pri%20M%20Stars%20ReleaseBlock%20Component%20Status%20Owner%20Summary%20OS%20Modified) to render, controlling the browser's perception of time ensures no frames are dropped. ```js const puppeteer = require('puppeteer') @@ -49,8 +46,10 @@ const d3 = require('d3') await sleep(50) let path = __dirname + '/png/' + d3.format('05')(frame) + '.png' - let clip = {x: 0, y: 0, width: 1920, height: 1080} - await page.screenshot({path, clip}) + + await page.setViewport({width: 1920, height: 1080, deviceScaleFactor: 2}) + const chartEl = await page.$('.chart') + await chartEl.screenshot({path}) } browser.close() diff --git a/source/_posts/2017-11-28-2017-chart-diary.md b/source/_posts/2017-11-28-2017-chart-diary.md index 5145e96a..52bfc8b7 100644 --- a/source/_posts/2017-11-28-2017-chart-diary.md +++ b/source/_posts/2017-11-28-2017-chart-diary.md @@ -1,6 +1,7 @@ --- template: post.html title: 2017 Chart Diary +date: 2017-11-28 permalink: /2017-chart-diary shareimg: http://roadtolarissa.com/images/posts/2017-chart-diary.png --- @@ -11,7 +12,7 @@ shareimg: http://roadtolarissa.com/images/posts/2017-chart-diary.png The idea is cribbed from an [Upshot piece](http://www.nytimes.com/interactive/2015/05/28/upshot/you-draw-it-how-family-income-affects-childrens-college-chances.html). We got a little crunched for time and weren't as fancy about customizing feedback for incorrectly drawn lines. Still, I think the core idea of making people put their assumptions down and overlaying reality over them works and increases [memorability/engagement](http://faculty.washington.edu/jhullman/explaining_the_gap.pdf). -I'm a little surprised this form hasn't been used more. The chart dragging code isn't super complex - just [60 lines of d3](http://bl.ocks.org/1wheel/07d9040c3422dac16bd5be741433ff1e) for an mpv. +I'm a little surprised this form hasn't been used more. The chart dragging code isn't super complex - just [60 lines of d3](http://blocks.roadtolarissa.com/1wheel/07d9040c3422dac16bd5be741433ff1e) for an mpv. One bit that we should have spent more time on before publishing: clearly labeling the years. We originally put the year labels [directly below](https://imgur.com/xUZurSS) the tick, but there was a lot of confusion over Obama getting credit/blame for Bush's last year in office. Moving the year labels to the [left of the tick](https://imgur.com/vIqA2Q1) clarified that the data points were for values at the end of the year. @@ -74,7 +75,7 @@ Jeremy got a hold of some interesting data this afternoon and we threw together I made the map with d3 and a couple of canvas tricks from an [old Bloomberg piece](https://imgur.com/XdCmjNR---https://www.bloomberg.com/graphics/2015-uk-election/messy.html). There are too many points (80,000+!) to animate with svg, so I used two canvas layers. The top one is cleared every frame and each moving point is redrawn. The bottom frame only has points drawn on it and is never cleared so it keeps a record of every location. -We briefly talked about showing time in different ways - a line chart or small multiple maps by hour - but there was a chunk of time missing. After publishing, I explored an alternative representation with [d3-contour](https://imgur.com/4pmDBhy---https://bl.ocks.org/1wheel/5d6990abfff925e6a37e0557f1de18e5) which clearly shows the higher rate of hacking in Eastern Europe and China. It's easier to use a nonlinear scale when you're programming at a higher level than drawing rectangles on top of each other. +We briefly talked about showing time in different ways - a line chart or small multiple maps by hour - but there was a chunk of time missing. After publishing, I explored an alternative representation with [d3-contour](https://imgur.com/4pmDBhy---https://blocks.roadtolarissa.com/1wheel/5d6990abfff925e6a37e0557f1de18e5) which clearly shows the higher rate of hacking in Eastern Europe and China. It's easier to use a nonlinear scale when you're programming at a higher level than drawing rectangles on top of each other. Of course, number of hacked IPs per square mile is not the most meaningful thing in the world to show. Perhaps some kind of binning to compare the amount of hacking to the number of computers in different regions of the world would have been a better approach. @@ -119,9 +120,9 @@ One of my favorite things about writing code in response to the news cycle is ge This came together quite a lot faster than the 2015 UK Election Results piece I did at Bloomberg. I think we only decided that we were going to purchase a live feed of election results from the Press Association about a week and a half ahead of the election night. -Not sure if we were actually going actually do anything, I played around trying to generate a [hex cartogram](https://imgur.com/zom9SuO---https://bl.ocks.org/1wheel/raw/b833c3490bacc377f9485c060f1c470a/). It was harder than I thought it would be (Greater London is quite dense); we used [Ben Flanagan's layout](https://imgur.com/6NZ8spP---https://www.arcgis.com/home/item.html?id=15baaa6fecd54aa4b7250780b6534682) instead. +Not sure if we were actually going actually do anything, I played around trying to generate a [hex cartogram](https://imgur.com/zom9SuO---https://blocks.roadtolarissa.com/1wheel/raw/b833c3490bacc377f9485c060f1c470a/). It was harder than I thought it would be (Greater London is quite dense); we used [Ben Flanagan's layout](https://imgur.com/6NZ8spP---https://www.arcgis.com/home/item.html?id=15baaa6fecd54aa4b7250780b6534682) instead. -In retrospect, spending so much time exploring cartograms wasn't a great idea. I think this [arrow chart](https://imgur.com/HauRsAm---https://bl.ocks.org/1wheel/c7e86b96a0f9c4cafa4218515ff163ef) showing the shift in UKIP's vote share along with the Labour/Conservative split had potential but there wasn't enough time to finish it. +In retrospect, spending so much time exploring cartograms wasn't a great idea. I think this [arrow chart](https://imgur.com/HauRsAm---https://blocks.roadtolarissa.com/1wheel/c7e86b96a0f9c4cafa4218515ff163ef) showing the shift in UKIP's vote share along with the Labour/Conservative split had potential but there wasn't enough time to finish it. ## [The Golden State Warriors’ Record-Setting Postseason](https://www.nytimes.com/interactive/2017/06/17/sports/basketball/golden-state-warriors-post-season.html) @@ -143,7 +144,7 @@ I had trouble squeezing all the [annotations](https://imgur.com/qsjUFmH) in. Tom ## [It’s Not Your Imagination. Summers Are Getting Hotter.](https://www.nytimes.com/interactive/2017/07/28/climate/more-frequent-extreme-summer-heat.html) -Nadja did almost all the work on this. The [original animation](https://imgur.com/Mognewr) had a single frame for each time period. I thought it'd be cool to transition between the time periods - not too hard if you know to use the [mask element](https://bl.ocks.org/1wheel/76a07ca0d23f616d29349f7dd7857ca5)! +Nadja did almost all the work on this. The [original animation](https://imgur.com/Mognewr) had a single frame for each time period. I thought it'd be cool to transition between the time periods - not too hard if you know to use the [mask element](https://blocks.roadtolarissa.com/1wheel/76a07ca0d23f616d29349f7dd7857ca5)! ## [Good, Evil, Ugly, Beautiful: Help Us Make a ‘Game of Thrones’ Chart](https://www.nytimes.com/interactive/2017/08/09/upshot/game-of-thrones-chart.html) @@ -174,7 +175,7 @@ This started as a follow-up to reports that the [DOJ was going to challenge affi The design of the top charts went through several iterations. We started out with [slope charts](http://imgur.com/QnZyGDP) showing how the student population of different demographics had changed at different types of schools over the last 35 years. Fitting the white percentages on common scales was [tricky](http://imgur.com/FkH1Pzq), so we switched to showing the [difference](http://imgur.com/buvzjrZ) between percent admissions and population. -I really wanted the gap charts to work - they show so many different stories with just a few lines! - so I spent some time [tweaking the layout](http://imgur.com/zfFGOQJ) to squeeze them in. Distinguishing between positive and negative gaps wasn't intuitive though ([even with particle animation](https://imgur.com/3ZkgDV9---https://bl.ocks.org/1wheel/4b9d34d74bd64a63d34028f160a71d7b)), so we ended up using an even more slimmed down version of the [slope charts](http://imgur.com/oAnVpuG). +I really wanted the gap charts to work - they show so many different stories with just a few lines! - so I spent some time [tweaking the layout](http://imgur.com/zfFGOQJ) to squeeze them in. Distinguishing between positive and negative gaps wasn't intuitive though ([even with particle animation](https://imgur.com/3ZkgDV9---https://blocks.roadtolarissa.com/1wheel/4b9d34d74bd64a63d34028f160a71d7b)), so we ended up using an even more slimmed down version of the [slope charts](http://imgur.com/oAnVpuG). If I had a little more time, I would have liked to try including more chart forms and alternative gap measurements (the ratio of percents isn't the same as the difference of percents!) by transitioning between them in a scrollytelling piece. That would have required a big rewrite of copy/code which didn't make sense to attempt while we were waiting for a break in the news to publish. Other things to explore: a wider selection of schools (we had a drop down that let you chart any of the ~4,000 colleges in the US, but weren't 100% confident in the data so it was cut) and graduation rates. @@ -219,7 +220,7 @@ To give his numbers a little bit of context, we started exploring different ways All of the 500+ lines of javascript that create the charts were written in 25 hours. This was probably a little too ambitious. Including all of the hurricanes looked great, but after running into performance issues on mobile and retina displays we decided to only including category 3 hurricanes. Coming at it fresh, a canvas rewrite would only have taken an hour or two (d3.line is super flexible!) but by the time that I realized we needed one I was too worn out to do it. -I took a couple days the week after to [rewrite in regl](https://imgur.com/fSrst3U---https://bl.ocks.org/1wheel/9c90f55041220c600162b85f84e807c5). Includes my right to left time scale (so the westward paths don't invert) and line to scatter transition that were just a little too confusing to publish. +I took a couple days the week after to [rewrite in regl](https://imgur.com/fSrst3U---https://blocks.roadtolarissa.com/1wheel/9c90f55041220c600162b85f84e807c5). Includes my right to left time scale (so the westward paths don't invert) and line to scatter transition that were just a little too confusing to publish. ## [We Charted Arctic Sea Ice for Nearly Every Day Since 1979. You’ll See a Trend.](https://www.nytimes.com/interactive/2017/09/22/climate/arctic-sea-ice-shrinking-trend-watch.html) @@ -250,7 +251,7 @@ Design based on one of my [favorite graphics](https://imgur.com/C2Aopfu---http:/ After getting a couple of requests for an update to the [2016 version](https://roadtolarissa.com/worlds-group), I grabbed this year's [data](http://lol.esportswikis.com/wiki/2017_Season_World_Championship/Main_Event#Schedule) and threw it into the charts. The [code](https://github.com/1wheel/roadtolarissa/blob/master/source/worlds-group-2017/script.js) wasn't quite as pretty as I remembered, but I think I've fixed the three-way tiebreaker bug that threw off the [MSI chart](https://imgur.com/FH7Mndt---https://roadtolarissa.com/msi-group/)—if not please let me know! -Hopefully next year I'll have a chance to explore another [representation](https://imgur.com/h5sCVoz---https://bl.ocks.org/1wheel/edb19f67a301bdd8f28abc70db7e869f) of this data. I'd like something that you can read top to bottom as matches progress. With our World Cup coverage [canceled](https://twitter.com/adamrpearce/status/917935378748837889) there should be plenty of time! +Hopefully next year I'll have a chance to explore another [representation](https://imgur.com/h5sCVoz---https://blocks.roadtolarissa.com/1wheel/edb19f67a301bdd8f28abc70db7e869f) of this data. I'd like something that you can read top to bottom as matches progress. With our World Cup coverage [canceled](https://twitter.com/adamrpearce/status/917935378748837889) there should be plenty of time! ## [Every Tax Cut and Tax Increase in the House G.O.P. Bill and What It Would Cost](https://www.nytimes.com/interactive/2017/11/15/us/politics/every-tax-cut-in-the-house-tax-bill.html) @@ -259,23 +260,23 @@ We wanted to enumerate everything in the tax bill while also providing a higher Some of the provisions in the bill had a comparatively small impact on revenue and weren't tall enough to see as stacked boxes. Since we were also trying to show everything in the bill, I stole an idea from a [Bloomberg piece](https://www.bloomberg.com/graphics/2016-who-marries-whom/) and laid out a short description of each provision in a grid. -We also briefly explored ways of showing numbers to represent a ten year window, making a [stacked area chart](https://imgur.com/LOwj9ji.png) and putting a chart of revenue impact over time in a tooltip. I thought it would be fun to transition from boxes to an area chart, but there wasn't much to say about the timing of different provisions. A [treemap alternative](https://i.imgur.com/r81uKKS.gif---https://bl.ocks.org/1wheel/45f3ed2f6931c286932fe34058c096c0) I played with wasn't quite ready. +We also briefly explored ways of showing numbers to represent a ten year window, making a [stacked area chart](https://imgur.com/LOwj9ji.png) and putting a chart of revenue impact over time in a tooltip. I thought it would be fun to transition from boxes to an area chart, but there wasn't much to say about the timing of different provisions. A [treemap alternative](https://i.imgur.com/r81uKKS.gif---https://blocks.roadtolarissa.com/1wheel/45f3ed2f6931c286932fe34058c096c0) I played with wasn't quite ready. One detail I'd explore if I was studying human perception: how people process `1.2 trillion v. 86 billion` compared to how they understand `1,215 billion v. 86 billion`. My intuition is that our brains don't actually divide by a thousand to compare a `trillion` to `billion`. Consensus within the department was that the comma was pretty confusing, so I might be out thinking myself. ## [Tax Bill Calculator: Will Your Taxes Go Up or Down?](https://www.nytimes.com/interactive/2017/12/17/upshot/tax-calculator.html) -Finally published something with WebGL! I started working on this last month when Bui and Ben realized they could model the impact of the tax bill on thousands of households by running CPS data through [an open source tax model](https://github.com/open-source-economics/Tax-Calculator). Thinking it'd be interesting to see how different demographics' tax bills would change, I set up a [crossfilterish](http://square.github.io/crossfilter/) [interface](https://imgur.com/1IiOWS4---https://bl.ocks.org/1wheel/raw/e051435d784dae23360cdddd6b832a16/) to explore. With only 25,000 data points, filtering on multiple dimensions made the chart pretty sparse. So Bui used [small multiples](https://imgur.com/3A0h16U---https://www.nytimes.com/interactive/2017/11/28/upshot/what-the-tax-bill-would-look-like-for-25000-middle-class-families.html) instead, which also don't [require interaction to compare](http://worrydream.com/MagicInk/#interactivity_considered_harmful). +Finally published something with WebGL! I started working on this last month when Bui and Ben realized they could model the impact of the tax bill on thousands of households by running CPS data through [an open source tax model](https://github.com/open-source-economics/Tax-Calculator). Thinking it'd be interesting to see how different demographics' tax bills would change, I set up a [crossfilterish](http://square.github.io/crossfilter/) [interface](https://imgur.com/1IiOWS4---https://blocks.roadtolarissa.com/1wheel/raw/e051435d784dae23360cdddd6b832a16/) to explore. With only 25,000 data points, filtering on multiple dimensions made the chart pretty sparse. So Bui used [small multiples](https://imgur.com/3A0h16U---https://www.nytimes.com/interactive/2017/11/28/upshot/what-the-tax-bill-would-look-like-for-25000-middle-class-families.html) instead, which also don't [require interaction to compare](http://worrydream.com/MagicInk/#interactivity_considered_harmful). While Bui's piece provided a good overview of the tax bill, it still didn't answer everyone's biggest question: how will this affect *my* taxes? To get more data points with information about what people actually paid in taxes, Bui started talking to the IRS. -Canvas can't animate hundreds of thousands of points, so we decided to rewrite in [regl](http://regl.party/). [Peter's](https://peterbeshai.com/beautifully-animate-points-with-webgl-and-regl.html) tutorial helped me start [animating the points](https://bl.ocks.org/1wheel/5f0913c88cf80c6a3b00afb7b4f832db), but I ran into difficulties pretty fast. Rich Harris showed me how to draw opaque points, but they didn't stack quite right; the areas with the highest densities weren't the [darkest]( https://bl.ocks.org/1wheel/2322a47e760dcdc378dae1cd89d635af). Totally stuck, we tried rewriting in canvas, but the [lack of zoom](https://imgur.com/fQNJHCw) was lame. I ended up asking for help in the regl chat and Ricky Reusser showed me how to fix it with [a white background color](https://codepen.io/rsreusser/pen/LOKOxZ?editors=1010). +Canvas can't animate hundreds of thousands of points, so we decided to rewrite in [regl](http://regl.party/). [Peter's](https://peterbeshai.com/beautifully-animate-points-with-webgl-and-regl.html) tutorial helped me start [animating the points](https://blocks.roadtolarissa.com/1wheel/5f0913c88cf80c6a3b00afb7b4f832db), but I ran into difficulties pretty fast. Rich Harris showed me how to draw opaque points, but they didn't stack quite right; the areas with the highest densities weren't the [darkest]( https://blocks.roadtolarissa.com/1wheel/2322a47e760dcdc378dae1cd89d635af). Totally stuck, we tried rewriting in canvas, but the [lack of zoom](https://imgur.com/fQNJHCw) was lame. I ended up asking for help in the regl chat and Ricky Reusser showed me how to fix it with [a white background color](https://codepen.io/rsreusser/pen/LOKOxZ?editors=1010). There were a couple of similar problems that were hard to debug - no inspect element like SVG has! On some computers, the points with 0 `gl_pointSize` were still [getting drawn](https://imgur.com/fQNJHCw). My hacky fix was to draw very small points off-screen. We also had difficulties getting all the data to the browser. At first we tried loading 20% of the rows at a time. This caused the chart to flicker on load and the successive redraws made the page laggy. So instead we loaded the data incrementally by column. Initially, just the income and tax change columns needed to position the points are loaded. Then the columns with the categorical data for the filters are loaded in the the order that they appear on the page. -Getting this done before the bill passed was challenging. Some of the functions I've added to [jetpack](https://github.com/gka/d3-jetpack) like tooltips and [layers](https://bl.ocks.org/1wheel/f9b9909f10ed0f01780c5338ad38bd50) made it a little easier. And Blacki actually made finishing possible, jumping in right after starting at the Times and doing a ton of work while I was suffering from the flu. +Getting this done before the bill passed was challenging. Some of the functions I've added to [jetpack](https://github.com/gka/d3-jetpack) like tooltips and [layers](https://blocks.roadtolarissa.com/1wheel/f9b9909f10ed0f01780c5338ad38bd50) made it a little easier. And Blacki actually made finishing possible, jumping in right after starting at the Times and doing a ton of work while I was suffering from the flu. For more on the collection of the data and the actual story, check out [Ben's writeup](https://www.nytimes.com/2017/12/19/insider/how-to-build-a-tax-calculator-thats-actually-useful.html). diff --git a/source/_posts/2018-01-20-aaronson-oracle.md b/source/_posts/2018-01-20-aaronson-oracle.md index 98b7988c..8ca41674 100644 --- a/source/_posts/2018-01-20-aaronson-oracle.md +++ b/source/_posts/2018-01-20-aaronson-oracle.md @@ -1,6 +1,7 @@ --- template: post.html title: Aaronson Oracle +date: 2018-01-20 permalink: /oracle shareimg: https://i.imgur.com/rfwNaUx.png --- diff --git a/source/_posts/2018-03-01-same-sex-legal.md b/source/_posts/2018-03-01-same-sex-legal.md index 69189e60..0bdeaf35 100644 --- a/source/_posts/2018-03-01-same-sex-legal.md +++ b/source/_posts/2018-03-01-same-sex-legal.md @@ -1,6 +1,7 @@ --- template: post.html title: The Rise and Fall of Same-Sex Marriage Bans +date: 2018-03-01 permalink: /same-sex-legal shareimg: https://i.imgur.com/YeoOlAC.png --- diff --git a/source/_posts/2018-03-30-stringline-scrape.md b/source/_posts/2018-03-30-stringline-scrape.md index 2a99670e..504324c6 100644 --- a/source/_posts/2018-03-30-stringline-scrape.md +++ b/source/_posts/2018-03-30-stringline-scrape.md @@ -1,6 +1,7 @@ --- template: post.html title: MTA Marey +date: 2018-03-30 permalink: /mta-marey draft: true shareimg: http://roadtolarissa.com/images/posts/tktk.png diff --git a/source/_posts/2018-04-15-top-3-movies.md b/source/_posts/2018-04-15-top-3-movies.md index 1818df75..b4428826 100644 --- a/source/_posts/2018-04-15-top-3-movies.md +++ b/source/_posts/2018-04-15-top-3-movies.md @@ -1,6 +1,7 @@ --- template: post.html title: Every Top Three Grossing Movie Over The Last 25 Years +date: 2018-04-15 permalink: /top-3-movies shareimg: https://i.imgur.com/0IADOwR.png --- diff --git a/source/_posts/2018-04-25-hot-reload-hack.md b/source/_posts/2018-04-25-hot-reload-hack.md index f47968b4..7ac598c5 100644 --- a/source/_posts/2018-04-25-hot-reload-hack.md +++ b/source/_posts/2018-04-25-hot-reload-hack.md @@ -1,6 +1,7 @@ --- template: post.html title: Hackable Hot Reloading +date: 2018-04-25 permalink: /hot-reload shareimg: https://i.imgur.com/ZNkXwEx.png --- @@ -10,7 +11,34 @@ shareimg: https://i.imgur.com/ZNkXwEx.png Ever since seeing Bret Victor rewire a platformer live on stage, I've wanted to write code more interactively. - + + + +

+ +
+ +
+ + + + I dabbled with programming languages that facilated this, like [clojure's REPL](https://clojure.org/guides/repl/introduction) and [R notebooks](https://rmarkdown.rstudio.com/r_notebooks.html). But most of my work is with javascript and I was stuck pressing `⌘+S ⌘+Tab ⌘+R` over and over again to save my changes, tab over to the browser and reload the page. diff --git a/source/_posts/2018-05-02-sell-strat.md b/source/_posts/2018-05-02-sell-strat.md index 96f34816..19f58ad1 100644 --- a/source/_posts/2018-05-02-sell-strat.md +++ b/source/_posts/2018-05-02-sell-strat.md @@ -3,6 +3,7 @@ template: post.html title: Buying High and Selling Low Can Beat the Market title2: What Should You Do When Stock Prices Drop? title3: Beat the Market by Selling Low and Buying High +date: 2018-05-02 permalink: /sell-strat shareimg: https://i.imgur.com/5z400QK.png --- @@ -34,7 +35,7 @@ Following momentum works if the market generally does what it previously did; if If, on the other hand, you think stocks act more like a random walk, then buying high and selling low is the exact opposite of what you should do. If we're not entering a sustained decline, which happened twice in the 2000s, these strategies will probably do worse than just staying in the market like they did in the 1990s and 2010s. -Regardless of how you think of markets, be aware that this model ignores the tax penalty from short term capital gains. And it instead of investing in T-bonds while not in stocks, it sits on yield-less cash. +Regardless of how you think of markets, be aware that this model ignores the tax penalty from short term capital gains. And instead of investing in T-bonds while not in stocks, it sits on yield-less cash. For me, committing to regularly monitoring the market, optimally managing all these transactions and filling out a longer Form 8949 sounds like entirely too much work so I'll be holding for now. diff --git a/source/_posts/2018-05-13-msi-4096.md b/source/_posts/2018-05-13-msi-4096.md index cab77e61..958bfbc8 100644 --- a/source/_posts/2018-05-13-msi-4096.md +++ b/source/_posts/2018-05-13-msi-4096.md @@ -1,6 +1,7 @@ --- template: post.html title: The 4096 Paths Into MSI +date: 2018-05-13 permalink: /msi-4096 shareimg: https://i.imgur.com/qCOkFzl.png --- diff --git a/source/_posts/2018-05-24-literate-blogging.md b/source/_posts/2018-05-24-literate-blogging.md new file mode 100644 index 00000000..3b03fbda --- /dev/null +++ b/source/_posts/2018-05-24-literate-blogging.md @@ -0,0 +1,161 @@ +--- +title: Incremental Rebuilds and Hot Reloading: 60 Lines of Literate Code for Static Blogging +template: post.html +date: 2018-05-24 +permalink: /literate-blogging +shareimg: https://i.imgur.com/3KDlIFQ.png +--- + +For five years, I was frustrated by every blogging engine I tried. + +WordPress made it difficult to embed inline interactive charts. Octopress's predefined css was hard to disable and pasting Stack Overflow instructions on installing gems without understanding what `renv` or `rvm` were eventually broke my ruby installation. Metalsmith was easier, but I never managed to successfully configure the rss plugin. + +And none of alternatives I looked at supported [hot reloading](https://roadtolarissa.com/hot-reload). + +Writing my own blogging software seemed like epitome of yak shaving. I thought it would be difficult, too, until I came across Jeremy Ashkenas's [Jorno](http://ashkenas.com/journo/docs/journo.html) and Rich Harris's [Svelte blog](https://github.com/sveltejs/svelte.technology/blob/1fc419a37aa47cc54eaa8e65661bd80894a653b0/scripts/prep/build-blog.js) last summer. Using their code as a starting point, I spent a lazy Sunday simplifying my setup. + +Now this site is built with just **60 lines of code**. And they're run directly off of this post. + +
+ +## How It Works + +Each post is a markdown file in the `source/_posts` folder. The posts get read in, parsed and written out to `public/` as an HTML file using one of templates from `source/_templates`. + +Static files that don't need preprocessing, like images or javascript, are copied directly from `source/` to `public/` with `rysnc` in preperation for publishing. + +```javascript +var fs = require('fs') +var {exec, execSync} = require('child_process') + +var public = `${__dirname}/../../public` +var source = `${__dirname}/../../source` + +function rsyncSource(){ + exec(`rsync -a --exclude _posts --exclude _templates ${source}/ ${public}/`) +} +rsyncSource() +``` + +Markdown is converted to HTML with [marked](https://github.com/markedjs/marked) and syntax highlighted by [highlight.js](). + +```javascript +var hljs = require('highlight.js') +var marked = require('marked') +marked.setOptions({ + highlight: (code, language) => hljs.highlight(code, {language}).value, + smartypants: true +}) +``` + +Files in the `_templates` directory, currently `rss.xml`, `sitemap.xml` and `post.html`, are ES6 template strings. `eval` turns them into functions that can be passed data. + +```javascript +var templates = {} +readdirAbs(`${source}/_templates`).forEach(path => { + var str = fs.readFileSync(path, 'utf8') + var templateName = path.split('_templates/')[1] + templates[templateName] = d => eval('`' + str + '`') +}) + +function readdirAbs(dir){ return fs.readdirSync(dir).map(d => dir + '/' + d) } +``` + +Each post file in the `source/_posts` folder is read in with `parsePost` and saved to the `posts` array. + +Instead of having to install and configure a plugin, I created an rss feed by passing the array of posts to the `rss.xml` template and writing out a file. + +```javascript +var posts = readdirAbs(`${source}/_posts`).map(parsePost) +fs.writeFileSync(public + '/rss.xml', templates['rss.xml'](posts)) +fs.writeFileSync(public + '/sitemap.xml', templates['sitemap.xml'](posts)) +``` + +Passed the path of a post, `parsePost` reads the title, url, date and publish status from [front matter](https://jekyllrb.com/docs/frontmatter/) at the top of the post. The markdown body is converted to an HTML fragment and an object representing the post is returned. + +```javascript +function parsePost(path){ + var [top, body] = fs.readFileSync(path, 'utf8') + .replace('---\n', '') + .split('\n---\n') + + var post = {html: marked(body)} + top.split('\n').forEach(line => { + var [key, val] = line.split(/: (.+)/) + post[key] = val + }) + + return post +} +``` + + +`writePost` takes a post object, creates a folder for it in `public/`, runs it through a template and writes out the post to `index.html`. + +```javascript +function writePost(post){ + var dir = public + post.permalink + if (!fs.existsSync(dir)) execSync(`mkdir -p ${dir}`) + fs.writeFileSync(`${dir}/index.html`, templates[post.template](post)) +} +posts.forEach(writePost) +``` + +And that's all the code that's needed to build the blog! + +To get it all on the internet `npm run publish` runs [lit-node](https://github.com/Rich-Harris/lit-node) on this post to regenerate everything locally, then uses `rsync` again to copy the `public` directory to a remote folder that's being statically served. + +```json +"scripts": { + "publish": "lit-node source/_posts/2018-05-24-literate-blogging.md && + rsync -a public/ demo@roadtolarissa.com:../../usr/share/nginx/html/", + "start": "lit-node source/_posts/2018-05-24-literate-blogging.md --watch & + cd public/ && hot-server" +} +``` + +`npm run start` runs [hot-server](https://github.com/1wheel/hot-server) in the `public` folder and runs this post with the `--watch` flag. Changes in the `source` directory rerun `rsyncSource`, which copies the the update file to `public`, triggering hot-server's file watch and passing the file to the browser along a websocket. A little Rube Goldberg, but still plenty fast and simpler than rewriting hot-server here. + +Edits to a post rebuild just that post, making hot-server trigger a page reload. + +```javascript +if (process.argv.includes('--watch')){ + require('chokidar').watch(source).on('change', path => { + rsyncSource() + if (path.includes('_posts/')) writePost(parsePost(path)) + }) +} +``` + +I don't spend much time looking at `sitemap.xml` or tweaking the templates, so they're not hooked up to automatically update. I've tried to only implement exactly what I need without any unnecessary abstractions to keep the code easy to work with. Writing both the code and content lets you aggressively cut corners. + +## Make Your Own + +I'm not totally sold on literate programming yet. I quite liked the having all the code fit on one screen and [⌘-B](https://www.sublimetext.com/docs/3/build_systems.html) doesn't work out of the box. But I've been asked a couple of times for advice on putting words and charts on the internet without Medium App Nag getting plastered all over it. Hopefully this post shows how far a little glue code can go when paired with a folder of markdown files, `rsync` and a static server. + +If you'd like to try it without futzing with the literate bits, there's a [javascript only](https://github.com/1wheel/roadtolarissa/blob/master/source/literate-blogging/index.js) version. + + + + + + + + + \ No newline at end of file diff --git a/source/_posts/2018-06-01-2018-chart-diary.md b/source/_posts/2018-06-01-2018-chart-diary.md index 0b5f1562..f1ece623 100644 --- a/source/_posts/2018-06-01-2018-chart-diary.md +++ b/source/_posts/2018-06-01-2018-chart-diary.md @@ -1,274 +1,190 @@ --- template: post.html title: 2018 Chart Diary +date: 2018-12-03 permalink: /2018-chart-diary -shareimg: http://roadtolarissa.com/images/posts/2017-chart-diary.png -draft: true +shareimg: https://roadtolarissa.com/imgur-down/2018-chart-diary-promo.png --- -Previously: [2016](https://roadtolarissa.com/2017-chart-diary/) [2017](https://roadtolarissa.com/2017-chart-diary/) +_Previously: [2016](https://roadtolarissa.com/2016-chart-diary/) [2017](https://roadtolarissa.com/2017-chart-diary/)_ ## [Is There Something Wrong with Democracy?](https://www.nytimes.com/interactive/2018/01/24/world/is-there-something-wrong-with-democracy.html) -Larry [sketched out](https://imgur.com/9gl9sPf) a Hans Rosling style connected scatter plot about democracy and I turned it into an [unstyled animation](https://imgur.com/XiKMDYi) with a bit of d3. +Larry [sketched out](https://imgur.com/9gl9sPf) a Hans Rosling-style connected scatter plot about democracy and I turned it into an [unstyled animation](https://imgur.com/XiKMDYi) with a bit of d3. The tricky bit: he was producing an actual video, not a webpage. I found an objectively bad way of [exporting](https://roadtolarissa.com/d3-mp4/) a d3 animation as a video by taking a bunch of screenshots with a headless browser and stitching them together. I wish there was an easier way of doing this. [Chart Party](https://imgur.com/tEN3bkf---https://www.youtube.com/watch?v=t_SsIKgwvz4) does incredibly creative work and I think some of that's because video lets you walk people through complex forms in a way that's even more friendly than scrolling. I'd like to try, but learning a whole new way of working, especially with something that requires sharing more of yourself, is [daunting](https://imgur.com/zgthTOM---https://www.youtube.com/user/jonbois/videos). - - ## [The Rise and Fall of Same-Sex Marriage Bans](https://roadtolarissa.com/same-sex-legal/) Basically a stacked bar chart with two twists. -First, labeling the states lets you follow an individual state over time. States that change are highlighted with bolding and tweaking the sort order so you can easily count how many states switched into each group. This doesn't do great job highlighting which group the state [switched from](https://imgur.com/a/gWSUy), but that's not too important here since the progression is always No Law → Ban → Legal. +First, labeling the states lets you follow an individual state over time. States that change are highlighted with bolding and tweaking the sort order so you can easily count how many states switched into each group. This doesn't do great job highlighting which group the state [switched from](https://imgur.com/a/gWSUy---https://imgur.com/fVuvDJJ), but that's not too important here since the progression is always No Law → Ban → Legal. -Second, the baseline is adjusted so that No Law always sits in middle of the chart. This puts more emphasis on the total number of places with bans. - -TKTK imgs, baseline switch +Second, the baseline is adjusted to puts more emphasis on the total number of places with bans, giving the whole shape a fun outline. ## [Rich White Boys Stay Rich. Black Boys Don’t](https://www.nytimes.com/interactive/2018/03/19/upshot/race-class-white-and-black-men.html) -Kevin did most of the charting work on this piece - I just got pulled in as an expert dot animator. - -We started out with a [SVG animation](https://bl.ocks.org/1wheel/7ddedc637c07104886f6909215a41b7f), tried out Elijah's [canvas sankey particles](https://bl.ocks.org/emeeks/e9d64d27f286e61493c9) and ended up rewriting in regl to get more dots on the screen. +Amanda and Kevin did most of the charting work on this piece - I just got pulled in as an expert dot animator. -The final version gets better performance by passing in [array of attributes](https://bl.ocks.org/1wheel/9b3bcc4ce8266913c0a0ddd4120a41de) to the vertex shader - after publishing the very helpful [regl chat](gitter.im/mikolalysenko/regl) showed me how to fix. +We started out with a [SVG animation](https://blocks.roadtolarissa.com/1wheel/7ddedc637c07104886f6909215a41b7f---https://roadtolarissa.com/imgur-down/income-svg.gif), tried out Elijah's [canvas sankey particles](https://blocks.roadtolarissa.com/emeeks/e9d64d27f286e61493c9---https://roadtolarissa.com/imgur-down/income-meeks.gif) and ended up rewriting in [regl](https://blocks.roadtolarissa.com/1wheel/9b3bcc4ce8266913c0a0ddd4120a41de---https://roadtolarissa.com/imgur-down/income-regl.gif) to get more dots on the screen. -This piece probably has the worst data to ink ratio of anything I've made; 10,000 dots to show 10 data points. I think it would have been interesting to try show more of distributions across race/sex/parent income at once, but when you've got such important numbers going on big a couple of them can work. +This piece probably has the worst data to ink ratio of anything I've made; 10,000 dots to show 10 data points. I think it would have been interesting to try show more of distributions across race/sex/parent income at once, but when you've got such important numbers going big on a couple of them can work. -I got a reader email complaining how long it took watch the animation. I made a [static version](https://bl.ocks.org/1wheel/1629f9dbc0d48137ac3a8cb395e5ec4c) incorporating some of his suggestions]. Definitely more information dense, but it doesn't have quite the same impact. +I got a reader email complaining how long it took watch the animation, and made a [static version](https://blocks.roadtolarissa.com/1wheel/1629f9dbc0d48137ac3a8cb395e5ec4c---https://roadtolarissa.com/imgur-down/income-boring.png) incorporating some of his suggestions. Definitely more information-dense, but it doesn't have quite the same impact. ## [Every Top Three Grossing Movie Over The Last 25 Years](https://roadtolarissa.com/top-3-movies/) -After seeing Axios's [piece](https://www.axios.com/black-panther-box-office-titanic-top-3-north-america-avatar-star-wars-32a35770-59fc-4ccc-bd5b-3a85d7144266.html) on Black Panther becoming one of the highest (non-inflation adjusted) grossing movies of all time, I was curious how many other movies had done so. Originally, I copied the step interpolation - it looks cool! - [but Lisa's post](https://blog.datawrapper.de/weekly-chart-altitude/) convinced me that it was a little misleading for cumulative data. +After seeing Axios's [piece](https://www.axios.com/black-panther-box-office-titanic-top-3-north-america-avatar-star-wars-32a35770-59fc-4ccc-bd5b-3a85d7144266.html) on Black Panther becoming one of the highest (non-inflation adjusted) grossing movies of all time, I was curious how many other movies had done so. Originally, I copied the step interpolation - it looks cool! - [but Lisa's post](https://imgur.com/3NiBsQL---https://blog.datawrapper.de/weekly-chart-altitude/) convinced me that it was a little misleading for continuous cumulative data. -I like the scrubbability of seeing the scale suddenly increase when there's a new record, but some of the drama gets lost if you scroll too quickly. Updating the y scale elastically (like this [this path](https://www.bloomberg.com/politics/graphics/2015-redistricting/) or [this zoom](https://www.nytimes.com/interactive/2015/10/27/world/greenland-is-melting-away.html)) instead of instantly might have worked better. +I like the scrubbability of seeing the scale suddenly increase when there's a new record, but some of the drama gets lost if you scroll too quickly. Updating the y scale elastically (like [this path](https://www.bloomberg.com/politics/graphics/2015-redistricting/) or [this zoom](https://www.nytimes.com/interactive/2015/10/27/world/greenland-is-melting-away.html)) instead of instantly might have worked better. Grabbing data from Axios instead of having to find it, scrape it and check for errors is the best. In theory, I should have updated with more recent data (Infinity War!) and gotten a better sample of top grossing movies in the 80s... ## [Selling Low and Buying High Can Beat the Market](https://roadtolarissa.com/sell-strat/) -I got the idea for this piece after Feburary dip in stock prices and seeing comments like ["I like to look at it as stocks going on sale. Time to buy!"](https://www.reddit.com/r/personalfinance/comments/7vngon/dont_sell_the_stocks_in_your_retirement_portfolio/dttomu6/). Is it actually better to buy after a dip in prices? Or should you sell? +I got the idea for this piece after the Feburary dip in stock prices and seeing comments like ["I like to look at it as stocks going on sale. Time to buy!"](https://www.reddit.com/r/personalfinance/comments/7vngon/dont_sell_the_stocks_in_your_retirement_portfolio/dttomu6/). Is it actually better to buy after a dip in prices? Or should you sell? + +[Elliot Bently](http://ejb.github.io/2017/06/25/trading-bot-sketches.html) made a [trading simulator](https://www.wsj.com/graphics/build-your-own-trading-bot/) for the WSJ last year that came close to answering my question, but trying to understand the impact of tweaking the trading thresholds required [fiddling](https://i.imgur.com/uFl91hM.png) with a text field and waiting for an animation to play. [Animations](https://fivethirtyeight.com/features/how-to-win-a-trade-war/) are good for introducing a concept, but they start to get frustrating when they block interaction. + +Besides disabling the animation, I took a step and a half up the [ladder of abstraction](http://worrydream.com/LadderOfAbstraction/) and showed the entire space of different trading thresholds with a series of [heatmaps](https://i.imgur.com/zPehFto.png). People don't click on buttons as much as we'd like to imagine and mobile is a huge design constraint, but I don't think graphics desks are exploiting the possiblies of the medium when we don't give readers a chance to explore and examine our models without pulling down an IPython Notebook. + +Making something like this is more work; the [rough draft](https://blocks.roadtolarissa.com/1wheel/raw/18b49093b0a41888d4ff45281cb66f66/0b4c9ee45c6d625c7fdd48101963cdb70de190fa/) got [banged out](https://i.imgur.com/zNzmoCq.png) in an evening, but all the polishing touches took [more time](https://i.imgur.com/zPehFto.png) to get right and the [code](https://github.com/1wheel/roadtolarissa/blob/master/source/sell-strat/_script.js) ended up pretty ugly (didn't want to prematurally optimize, but dragging the date slider was laggy so I hacked in caching without cleanly rewriting, thinking the finish line was close… it wasn't). + +## [The 4096 Paths Into MSI](https://roadtolarissa.com/msi-4096/) + +My brother helped me update the [rules for three-way ties](http://esports-assets.s3.amazonaws.com/production/files/rules/MSI-Ruleset-2018-25APR.pdf) after they changed this year. The grid design didn’t change much from the first sketch. To stuff in more at-glance information, I added [bars showing high impact matches](https://imgur.com/oEOvnTc) and a final [small multiples slide](https://imgur.com/Jw4vqTR). + +Since this was for my blog I took a [bike ride](https://bertspaan.nl/dutch-farmhouses-of-brooklyn/) yesterday instead of making a mobile version `¯\_(ツ)_/¯` + +## [How 2 M.T.A. Decisions Pushed the Subway Into Crisis](https://www.nytimes.com/interactive/2018/05/09/nyregion/subway-crisis-mta-decisions-signals-rules.html) + +[Hard to sum this one up...](https://twitter.com/adamrpearce/status/1008340664315236352) + +## [Energized Democrats Are Voting in Competitive Primaries in Droves](https://www.nytimes.com/interactive/2018/06/25/us/politics/midterm-primaries-voter-turnout.html) + +About 30 seconds after walking back into the office post-paternity leave, Tom grabbed me: "Do you have a minute? We want to look at Democratic primary turnout." + +I put my bag down and started pulling party turnout numbers from secretary of state websites. The mechanics of party registration vary state to state, and states that did breakdown turnout by party were slow to post results. So we tried counting voters by just summing votes for Democratic and Republican candidates. House races didn't work for this--lots of seats don't have a primary--but we could compare turnout in states with a governor or senate race in 2014 and 2018. It [looked interesting](https://imgur.com/Ylxufyt), but there wasn't quite enough there to hang a story on. + +So we decided to zoom in and compare turnout in house races. My first attempt was a [log-scaled scatter plot](https://imgur.com/0qk98KV) showing how turnout shifted for both parties. Nice and dense! After [annonations](https://imgur.com/XRal9cV) didn't increase readabilty we started to worry it might be too complex. I still think it might have been doable with some [rotation](https://imgur.com/p12xa7D---https://roadtolarissa.com/nba-win-loss/) and quadrant labeling. + +Larry suggested trying out a less dense form, so I sketched the [distrubution](https://imgur.com/zp7AoeP) of how turnout shifted for each party in each race. This suggested a simpler metric: instead of trying to show how both parties' turnout changed, just use the change in [partisan vote share](https://imgur.com/y6ratvF). Haeyoun was curious if the Democratic share was up more in competive races, so I made [slope charts](https://imgur.com/NGSnPvr) faceted by the race's Cook rating. Since we didn't have data on a ton of races (the AP only reports vote totals for contested primaries and we needed vote totals for both parties in both years; a surprising number of primaries go uncontested), I switched over to [small multiples slope charts](https://imgur.com/5cj2z2F) which needed even less explanation. To emphasize the direction of the slope even more, we put all the races that increased their Democratic turnout on the left. Pulling the competive races, all of which had increased Democratic vote share, to the top kept the breakdown that Haeyoun wanted and enlarging them created a key. + +As we got closer to publication, Alex pointed out that we were missing lots of races in which readers would be interested. I drug my feet, not wanting to go back to dozens of secretary of state websites before realizing that I'd just need to copy/paste a single candidate's votes to get the vote total for each uncontested race, not several candidates as I had when collecting senate turnout. This gave us a ton of races so I shrunk the slope charts down to fit 10 in a column, essentially making the whole thing a [bar chart](https://imgur.com/Fyp5tec). + +The design got postpublication pushback: "Readers don't know where or what GA-4 is, you need to offer some human context." There's some truth to this. My reporting on this piece was done with spreadsheets and I didn't have much to add about individual races. I know Ocasio-Cortez won my district, but couldn't tell you my district number. Still, I didn't want to add a section with photos of candidates and details about every single race. That's been done dozens of times already! And turnout shifts in one race can be explained by its particulars; zooming out shows that there's actually a trend. Matthew whipped up some [maps](https://imgur.com/Y1SGZap) with [mapshaper](https://imgur.com/F95ukMB---http://mapshaper.org/) to provide some of that context without overwelming the rest of the piece. + +## [LeBron James Is Carrying the Cavaliers in a Historic Way](https://www.nytimes.com/interactive/2018/06/08/sports/basketball/lebron-nba-finals.html) + +Missed my chance to make something about the Spurs' 50 win streak, but still got my yearly LeBron chart in. He's pretty good! + +The design on the main chart didn't change too much from my [first sketch](https://imgur.com/Ylk9OuM---https://blocks.roadtolarissa.com/1wheel/raw/23837f1a9f5734a3e9e7a694b32a6aa3/). The gap between players each year ends up a little too emphasized with the lines, but the connection really helps recall each year. Plus it looks nice. Kevin suggested a bar chart version with space for all the players' names, but year encoding gets a little messy and including the 0 baseline makes the chart less dramatic. + +I added [labels](https://i.imgur.com/pklAgmR.png) for a few players and was thinking about using the little stat cards as a tooltip. Joe suggested laying out the cards by player so you could see their details [without interacting](http://worrydream.com/MagicInk/#interactivity_considered_harmful) (it makes a little bar chart [in print](https://i.imgur.com/jYHrPqF.png)!). Without anything to put in the tooltip, I tried just showing the [name](https://i.imgur.com/ayXUpjr) on hover. + +That was super frustrating to use though; it takes so much mousing to read the chart. So I shrunk the circles and printed all the names directly on the chart. + +I had planned on making a game-by-game stats share for every finals team, like I did for [total playoff points](https://www.nytimes.com/interactive/2017/05/25/sports/basketball/lebron-career-playoff-points-record.html) last year. The cards used the historic small multiples space, so I put a bigger 2018 game-by-game chart on top. Was originally going to stack them on mobile, but Archie pointed out that you really want to see them side by side so I squeeze them together, dropping the player labels and [reusing the y-axis](https://i.imgur.com/8ol1aZo.png). Wish I could have gotten some annotations in, but couldn't find space. : / + +The top still wasn't simple so we put in a 2018 LeBron stat card to introduce the piece. + +Also pitched for the 2018 playoffs: a [histogram of histograms.](https://imgur.com/RaNPhXX---https://blocks.roadtolarissa.com/1wheel/raw/e880b577009cf0bc5ca2a0d24c4c01b2/) + +## [These 20 Representatives Have Not Had a Primary Challenger for at Least a Decade](https://www.nytimes.com/interactive/2018/06/30/us/elections/representatives-running-unopposed-uncontested-primaries.html) + +When Crowley got primaried after not facing a primary opponent for over decade, Troy wondered how unusual it was to have a streak of uncontested primaries. I had House primary data sitting around from the turnout project and whipped up a chart showing how long it had been since each incumbent had been [challenged in a primary](https://imgur.com/tEBGPC4). + +Trying to turn the piece around before the news cycle moved on, Troy started writing and I started checking the data. It was way messier than I had hoped. The FEC collects [election results](https://transition.fec.gov/pubrec/electionresults.shtml) from states, but the names and FECIDs aren't 100% consistent between years. We ended up spending a day checking everything. + +I had big dreams of [remaking the NFL streak chart](http://www.nytimes.com/newsgraphics/2013/09/28/eli-manning-milestone/index.html---https://imgur.com/oFJhwPR), but the FEC didn't start posting spreadsheets till 2000. So we stuck to current incumbents. Going back farther, the results degrade from [HTML in 1998](https://transition.fec.gov/pubrec/fe1998/sch.htm---https://imgur.com/HDL7OSc) to [PDF in 1994](https://transition.fec.gov/pubrec/fe1994/federalelections94.pdf---https://imgur.com/EeMLOks) to [not proving primary results in 1992](https://transition.fec.gov/pubrec/fe1992/federalelections92.pdf---https://imgur.com/1xyW3fR). DeLauro and Lowey still had active unopposed streaks, so I called their offices to find out when they had their last primary--the person who picked up the phone for Lowey asked around for me and no one knew! + +To provide more historical context, I made a [line chart](https://imgur.com/hZjU3JK) that didn't relay on streaks. I wanted to get a little more information in it, so I switched to two stacked area charts showing [the number of candidates](https://imgur.com/IDvwR6K) in each House primary. To keep the piece focused on incumbents, I kept that form but switched to showing [how often they were challenged and how often they won](https://imgur.com/GOQvzXb). If I had seen Boatright's chart showing [competitive challenges earlier](https://imgur.com/h7bHtY0---https://www.washingtonpost.com/news/monkey-cage/wp/2018/06/05/heres-what-weve-learned-from-the-u-s-congressional-primaries-so-far/?utm_term=.c607cbf9096b), I would have added another layer showing the percentage of incumbents facing non-competitive primaries. -[Elliot Bently](http://ejb.github.io/2017/06/25/trading-bot-sketches.html) made a [trading simulator](https://www.wsj.com/graphics/build-your-own-trading-bot/) for the WSJ last year that came close to answering my question, but getting trying to understand impact of tweaking the trading thresholds required [fiddling](https://i.imgur.com/uFl91hM.png) with a text field and waiting for an animation to play. [Animations](https://fivethirtyeight.com/features/how-to-win-a-trade-war/) are good for introducing a concept, but they start to get frustrating when they block interaction. +Troy did all the styling and fixes in illustrator, like making time go left to right (whoops). I wish there was more time to report out _why_ incumbents aren't challenged: the [Queens machine](https://www.nytimes.com/2018/06/28/nyregion/joseph-crowley-party-boss-queens.html) and [New York ballot access](http://www.nydailynews.com/new-york/queens/ballot-frustrating-inefficient-article-1.3289020) create some formidable barriers. But between Kennedy retiring and a mass shooting, we had already missed the beat of the news cycle. -Besides disabling the animation, I took a step and a half up the [ladder of abstraction](http://worrydream.com/LadderOfAbstraction/) and showed the entire space of different trading thresholds with a series of [heatmaps](https://i.imgur.com/zPehFto.png). People don't click on buttons as much as we'd like to image and mobile is a huge design constraint, but I don't think graphics desks are exploiting the possiblies of the medium when we don't give readers a chance to explore and examine our models without pulling down an IPython Notebook. +## [DeMarcus Cousins Gives the Warriors a Fifth All-Star](https://www.nytimes.com/2018/07/02/sports/demarcus-cousins-warriors.html) -Making something like this is more work; the [rough draft](https://bl.ocks.org/1wheel/raw/18b49093b0a41888d4ff45281cb66f66/0b4c9ee45c6d625c7fdd48101963cdb70de190fa/) got [banged out](https://i.imgur.com/zNzmoCq.png) in a evening, but all the polishing touches took [more time](https://i.imgur.com/zPehFto.png) to get right and the [code](https://github.com/1wheel/roadtolarissa/blob/master/source/sell-strat/_script.js) ended up pretty ugly (didn't want to prematurally optimize, but dragging the date slider was laggy so I hacked in caching without cleanly rewriting thinking the finish line was close… it wasn't). +I [tweeted](https://imgur.com/9GOPDkW) this out after the news broke. Getting it inside of our [CMS](https://imgur.com/Caisl1H) took longer than making the chart! And I think it looks worse; taking off the monospace makes the columns less bar-like (typography is an object) and requiring the title on top stopped it from directly labeling the x-axis. +A reddit user suggested that I learn a data vis tool besides d3: +> You could have made this in Microsoft Paint. Why go through the additional effort. -TK TK animation gifs. rough draft chart -exploiting sentence needs work +## [Mapping Florence’s Impact](https://www.nytimes.com/interactive/2018/09/13/us/hurricane-florence-impact-damage-map.html) +Sure, the [Post](https://twitter.com/driven_by_data/status/1039698579466670082) had the best hurricane tracker maps. But they use my [tutorial](https://roadtolarissa.com/hurricane) to make [one of them](https://imgur.com/uEOCMez)! + +With the data file getting bigger and bigger during the storm, the Post reduced the temporal resoultion of their animation. I didn’t. Keeping the data hourly really shows the movement of the storm. And if loading a 10 MB file on the page was a bad idea, why is there a 50 MB video ad a little farther down? (On mobile, where bandwidth matters more, I decreased the spatial resolution). + +## [See Flood Waters Rise Across the Carolinas After Hurricane Florence](https://www.nytimes.com/interactive/2018/09/18/us/hurricane-florence-flooding.html) + +While sticking the rainfall map at the bottom, I noticed I was having a hard time [parsing](https://imgur.com/0zHzrwl) the height of the bars when they stacked on top of each other. I added a [gradient and a little curve](https://imgur.com/KAK41t0) so the color and angle of the curve would also encode the flooding height. Also they looked kind of like rain or water drops! + +I’m not sure why we stuck with the bars; it got rushed to publication before we got a chance to hash it out. + +## [Live Polling](https://www.nytimes.com/interactive/2018/upshot/elections-polls.html) + +My favorite chart from this project was for an internal dashboard. Call centers don’t usually provide real time results of polls; the best we could get them to do was 15 minute dumps of all the calls they had done that night, along with the start time of the call. + +The timeseries and call stream animation assumed they recived the calls in order of start time; when they started showing on the front end unordered they caused lots of hard to track down bugs. I thought there was an error with our data processing, but by [charting](https://imgur.com/NWM3UoP) the call center timestamp against the upload time found that high call volumes led to a [bottle neck](https://imgur.com/GIXN8eC). We ended up randomly distributing the new calls we recived over the next 15 minutes—not as magical as seeing them show up as they happened or exactly replayed, but the best we could do. + + I didn’t end up watching too many polls come in as they happend. I really wanted to make the stream animation a little slicker and less laggy, but got [bogged down](https://twitter.com/Rich_Harris/status/1037903141960867840) coming up with awful hacks to cram our sleek 100kb svelte app inside of the NYT’s react app. Kids, always make sure everyone is on board with your publishing pipeline *before* you start working. + +## [2018 Worlds Group Advancement](https://roadtolarissa.com/worlds-group-2018/) + +Didn't think I would have time this year, but the [fan art](https://www.reddit.com/r/leagueoflegends/comments/9nxhmw/worlds_simulation_site/---https://imgur.com/r6RMB4f) inspired me. + +## [Live Forecast: Who Will Win the House?](https://www.nytimes.com/interactive/2018/11/06/us/elections/results-house-forecast.html) + +I tried lots of ideas for showing our model's margin of uncertainty for house races. A [lava lamp](https://blocks.roadtolarissa.com/1wheel/raw/37344ee99e7d23dee2122bdd08c84d92/---https://roadtolarissa.com/imgur-down/lava-lamp.gif), a [bouncing histogram](https://blocks.roadtolarissa.com/1wheel/raw/c806ed704adee9ec82e580dff7763388/---https://roadtolarissa.com/imgur-down/bouncing-histogram.gif), a [grid of needles](https://blocks.roadtolarissa.com/1wheel/3d13f6f7399ce3458984ca031081be7e---https://roadtolarissa.com/imgur-down/dial-grid.gif), a grid of [every house race](https://blocks.roadtolarissa.com/1wheel/raw/c8d5a69a194de0ff69192f7951b3d514/---https://roadtolarissa.com/imgur-down/house-0.png) + +To clearly show the balance of power, I [stacked](https://roadtolarissa.com/imgur-down/house-1.png) races each party was favored to win. This was a little tall, so I squeezed each house race down to a [single vertical pixel](https://roadtolarissa.com/imgur-down/house-4.png) and drew their range of possible outcomes. Too dense; with the lines adjacent it wasn't clear they represented individual seats. And the closest races were stuck in the muddy middle of the chart. + +Looking to create space between the lines without increasing the height of the chart, I layed the races out in an [arc](https://blocks.roadtolarissa.com/1wheel/raw/692e7bb028b822a74a5517daf8d946c5/---https://roadtolarissa.com/imgur-down/space-arc.png). This also pulled the closest races to the top of the chart, encoded the balance of power with the angle of the lines and also kinda looked like the needle at the top of the page. I thought it was great, but there were too many bewildered looks internally and we cut it. + +## [Republicans Dominate State Politics. But Democrats Made a Dent This Year.](https://www.nytimes.com/interactive/2018/11/10/upshot/republicans-dominate-state-politics-but-democrats-made-a-dent.html) + +Last spring I took a look at state and local [election results](https://i.imgur.com/LYxA4bF.png), wondering if more Republican state legislators would lead to more [pre-emption](https://www.nytimes.com/2017/07/06/upshot/blue-cities-want-to-make-their-own-rules-red-states-wont-let-them.html), The National Conference of State Legislatures tracks the number of legislators over time, so I tried [plotting all of them](https://imgur.com/1Kqyuvj) (a [line chart](https://imgur.com/SqDAfKI) is probably a better way to show this information). + +The raw number of representives also isn't that meaningful (New Hampshire's lower chamber has [400 members](https://imgur.com/7SSSmWa)!), so I looked at control of upper and lower chambers in each state over the last hundred years with a [cartogram](https://imgur.com/s1Egu2U). National trends are hard to pick out with this form; state boxes are only 70 pixels wide so each election is only a pixel wide. + +To pull those trends out, I made a [stacked "area" chart](https://imgur.com/X4azqdJ) out of the state's intitals. Changes in control are highlighted and the states with split chambers are vertically centered so the outline of the chart shows changes in control. I was happy with this form--it'd make a great poster!--but didn't have a story to attach it to so it sat untouched till Bui and Emily started working on a post-midterms state legislative piece. + +Bui built similar charts for governors and overall state control, then dropped the [baseline](https://imgur.com/NnIwauh) to make them denser. I thought the shape of the outlines looked nice, but with three catorgies it is totally unnessary. After publishing, Kevin pointed the Times had previously published [both](https://imgur.com/HSLBNSx) [types](https://imgur.com/8joPL2f). + +## [See How Close the Results Are in Arizona, Georgia and Florida](https://www.nytimes.com/interactive/2018/11/10/us/elections/2018-possible-midterm-recounts-georgia-florida-arizona.html) + +Troy did a [mockup](https://imgur.com/UtXrZ5T) with fun lines. When I hooked up the [real data](https://imgur.com/u95Wsba) and dropped the election night oscillations it wasn’t quite as interesting. + +We tried using [yellow](https://imgur.com/ULOkNHe) to highlight recount zones, but it made the chart really noisy. Archie suggested cross hatching the zone and emphasizing the date ticks with a black background. And once we were closer to publishing, the real data was [more exciting](https://imgur.com/XY3O9Xg)! + +I knew this would be the last breaking news thing I’d work on so I tried to get the little touches right, sticking in a [hover+opacity](https://www.nytimes.com/interactive/2016/upshot/presidential-polls-forecast.html---https://imgur.com/NmGA7MO) effect lifted from Gregor. + +## [7 Train Signal Upgrades Complete After Years of Delays. Up Next: The Rest of the Subway.](https://roadtolarissa.com/flushing-cbtc-finished/) + +I moved to Sunnyside in late 2016, shortly before work was supposed to be finishing up on the 7 train's signal modernizations. Work was just completed yesterday. I was curious what caused the delay and also will take any excuse to duel encode time. + +Hoping to do more train charts, but getting people to answer your questions is a lot harder when you can’t say “I’m calling from the New York Times.” + + + + - - - + + diff --git a/source/_posts/2018-10-13-worlds-group.md b/source/_posts/2018-10-13-worlds-group.md new file mode 100644 index 00000000..47925a92 --- /dev/null +++ b/source/_posts/2018-10-13-worlds-group.md @@ -0,0 +1,36 @@ +--- +template: post.html +title: 2018 Worlds Group Advancement +date: 2018-10-13 +permalink: /worlds-group-2018 +shareimg: http://roadtolarissa.com/images/posts/worlds-group-2018.png +--- + +The second half of League of Legends' World Group Stage starts tomorrow morning! Each team will play the other three teams in their group once more. The best two teams in each group advance to the quarterfinals. + +The charts below show how each team could advance. With six games left in each group, there are 2^6 = 64 ways for group play to end. Each of these 64 outcomes is represented by a circle. Green circles show ways a team could advance and red circles show elimination scenarios. Yellow circles indicate scenarios with a tiebreaker match. + +With rough 1-2 starts, all three North American teams need at least two wins to advance. + +

Group A

+
+

Group B

+
+

Group C

+
+

Group D

+
+ + +[code](https://github.com/1wheel/roadtolarissa/blob/master/source/worlds-group-2017/script.js) +[2018 MSI](https://roadtolarissa.com/msi-4096/) +[2017 Worlds](https://roadtolarissa.com/worlds-group-2017) +[2017 MSI](https://roadtolarissa.com/msi-group/) +[2016 Worlds](https://roadtolarissa.com/worlds-group) + + + + + + + \ No newline at end of file diff --git a/source/_posts/2018-11-20-7-cbtc-completed.md b/source/_posts/2018-11-20-7-cbtc-completed.md new file mode 100644 index 00000000..d91f67ce --- /dev/null +++ b/source/_posts/2018-11-20-7-cbtc-completed.md @@ -0,0 +1,112 @@ +--- +template: post.html +title: Can the Rest of the System's Signals Modernized Without Years of Delays? +title: Computerized Signals on the 7 are Finally Actived. Will Upgrading the Whole System Also take Years of Delay. +title: 7 Train Signal Upgrades Fininished After Years of Delays +title: 7 Train Signal Upgrades Complete After Years of Delays. Up Next: The Rest of the Subway. +date: 2018-11-26 +permalink: /flushing-cbtc-finished +shareimg: https://i.imgur.com/i5njD1m.png +--- + +After years of weekend closures, the MTA announced the completion of the 7 train's new, [computerized signal system](https://www.nytimes.com/2017/05/01/nyregion/new-york-subway-signals.html) yesterday. + +Replacing finicky pneumatic electromechanical devices installed in the 50s, the modernized signals increase the number of trains the MTA can run on the 7 line, reduce "signal problems ahead," and lower maintenance costs. Over the next 10 years, the MTA plans to spend $20 billion upgrading every line’s signals. + +Past experience suggests this be will difficult. + +## The L and 7 Signal Upgrades Were Late and Over Budget + + + +
+ +In their efforts to fix the subway, the MTA has addressed some of the issues that caused problems in the past. They’re [buying](https://twitter.com/ModernSignaling/status/1053394646892720128) train cars that work with computer signals without retrofitting, a previous source of delays. The new head of New York City Transit, Andy Byford, has proposed shutting down all weekend and night travel for years while a line’s signals are upgraded; the intermittent shutdowns on the 7 meant that additional days of necessary [trackwork](https://www.wsj.com/articles/nyc-subway-chief-faces-reality-check-in-push-for-upgrades-1540044000?redirect=amp#click=https://t.co/VSchTsaExm) could add a month to the project timeline. + +But there are still problems. + +## How the 7 Signal Completion Date Slipped Over Time + +
+ +Plagued by software issues, the last two years of work on the 7 line took four years to finish. This isn’t a new problem for the MTA. On a [previous signal project](https://www.theatlantic.com/technology/archive/2015/11/why-dont-we-know-where-all-the-trains-are/415152/), ad hoc deployment errors caused interruptions to train service. A vaguely specified voice communications system failed in the field. UIs that had only been prototyped in PowerPoint were hard to use and had to be redone. + +Byford says he wants to buy "[off-the-shelf](https://www.nytimes.com/2018/06/11/nyregion/subway-signal-upgrade-plan.html)” to cut costs and reduce delays. His previous job as CEO of the Toronto Transit Commission provides a clue about what that’ll entail. + +## Combining Old and New Signaling Systems Isn’t Easy + +When Toronto decided to update its [50 year old signals](https://stevemunro.ca/2015/03/30/the-evolution-of-ttc-signaling-contracts/), they kept the old electromechanical signals in place and overlaid the computerized signals. This is hard to do: the computerized signals need heavily customized software that simulates how the old signals and interlocks work. And the amount of track work increases; instead of just putting down beacons that let the train car know its exact position, the old signals also require extensive modifications. + +Faced with mounting delays, Byford [simplified](https://ttc.ca/About_the_TTC/Commission_reports_and_information/Commission_meetings/2015/March_26/Reports/5_2_Staff_Report_%26_Attachment.pdf). Instead of keeping the old signals, almost everything was computerized. + +In its signal modernization efforts, the MTA is still overlaying rebuilt parts of the old signal system as a failsafe. Overlaying old signals accounts for a significant chunk of the project timeline on the signal modernization project currently underway on the Queens Boulevard Line: + +
+
+ ![](cbtc-queens.png) +
+ +
+ QBL CBTC Installation RFQ via Philippe Vibien +
+
+ +A Federal Transit Administration [report](https://www.transit.dot.gov/sites/fta.dot.gov/files/docs/FTA_REPORT_No._0045.pdf) found that keeping the old signals isn't necessary and increased capital costs by at least 30%. The safety benefit is limited. In use, two sets of signals can actually decrease reliability because service is interpreted if one of them fails. Keeping the old signals working also requires regularly sending workers onto the tracks. Many of the upsides of computerized signals are negated. + +## Things Will Get Worse Before They Get Better + +Under the [current plan](https://fastforward.mta.info/transform-the-subway), each line will require years of weekend and overnight closures to overlay the computerized signals on the old signals. Even if the project stays within its scoped budget, this signal modernization will cost a huge amount of money. Already overloaded with [debt](https://www.nytimes.com/2017/11/18/nyregion/new-york-subway-system-failure-delays.html) and weighing [service cuts and fare hikes](https://www.nytimes.com/2018/11/15/nyregion/mta-fare-hike-nyc.html), the MTA has limited fiscal room to maneuver if there are steep cost overruns. + +Will Byford continue to overlay two sets of signals or will he decide to simplify? The activation of the new signals on the 7 yesterday could impact the decision. There were extensive delays during the evening commute after a component of the old signal system [failed](https://www.nydailynews.com/new-york/ny-metro-7-train-signals-20181126-story.html). + +*Nov. 27, 2018* + + + + + + + + + + + \ No newline at end of file diff --git a/source/_posts/2019-02-26-paper-reading.md b/source/_posts/2019-02-26-paper-reading.md new file mode 100644 index 00000000..870f9c24 --- /dev/null +++ b/source/_posts/2019-02-26-paper-reading.md @@ -0,0 +1,18 @@ +--- +template: post.html +title: Paper Reading +date: 2019-02-26 +permalink: /paper-reading +shareimg: http://roadtolarissa.com/images/posts/paper-reading.png +draft: true +--- + + +
+ + + + + + + \ No newline at end of file diff --git a/source/_posts/2019-03-21-dvs-privacy.md b/source/_posts/2019-03-21-dvs-privacy.md new file mode 100644 index 00000000..9010c5a4 --- /dev/null +++ b/source/_posts/2019-03-21-dvs-privacy.md @@ -0,0 +1,19 @@ +--- +template: post.html +title: DVS, SVGs and Privacy +tweet: my data visualization society contest entry uses force directed pies to illustrate how the badge SVGs and contest reveal more about some members than intended +date: 2019-05-25 +permalink: /dvs-privacy +shareimg: https://i.imgur.com/9XHjYgq.png +--- + + +
+
+
+
+ + + + + diff --git a/source/_posts/2019-07-21-scan-sorted.md b/source/_posts/2019-07-21-scan-sorted.md new file mode 100644 index 00000000..5b6b2918 --- /dev/null +++ b/source/_posts/2019-07-21-scan-sorted.md @@ -0,0 +1,93 @@ +--- +template: post.html +title: Faster Tooltips for Canvas +date: 2019-03-21 +permalink: /scan-sorted +shareimg: https://i.imgur.com/cfjlTI9.png +--- + +Canvas and WebGL can efficiently draw hundreds of thousands of points. Because these points aren’t DOM nodes, they don’t have `click` or `mouseover` events; adding interaction or tooltips to them requires calculating the closest point to the mouse. + +[Voronoi diagrams](https://blocks.roadtolarissa.com/mbostock/8033015) have been recommended, but their initialization time is slow: ~1,500 ms with a million points. With several zoom levels to compute, that locked up the browser for this [tax calculator](https://www.nytimes.com/interactive/2017/12/17/upshot/tax-calculator.html) that I worked on. Instead, we just looped over every point in the data array and found the one closest to the mouse. + + +```js +canvasSel + .call(d3.attachTooltip) + .on('mousemove', function(){ + var [px, py] = d3.mouse(this) + + var minPoint = d3.least(data, d => { + var dx = d.px - px + var dy = d.py - py + + return dx*dx + dy*dy + }) + + // update tooltip text +}) +``` + +There's no initialization time and checking the distance of a million points takes about [15 ms](https://blocks.roadtolarissa.com/1wheel/da6c526602c05a5a77390620a6be3040)—good enough for a tooltip. + +If you’re doing additional compution, like running an animation or calculating some value, something faster would be handy to avoid dropping frames. Precomputing the voronoi diagram could avoid intensive client side computation, but I’m not sure how you'd compactly serialize the data structure (update: Vladimir Agafonkin’s [flatbrush](https://github.com/mourner/flatbush) uses an array buffer for the index. It also handles rectangles, indexing a million in less than 300 ms!). + +Robert Monfera [suggested](https://twitter.com/monfera/status/1150784849206267906) a simple precomputation: sort the data along the x-axis. + +
+ +With sorted data, we can find the point with the x-position closest to the mouse’s x-position in `O(log n)` time using a binary search. Keeping track of the nearest point, we can keep looking at points to the left and right until we’re futher along the x-axis than the distance to the nearest point. + +This is way [faster](https://blocks.roadtolarissa.com/1wheel/77c660a764ab55a496c4e37623be9069)! On a square, uniform grid scanning in two directions takes `O(sqrt n)` comparisons; quite an improvement over checking every point `O(n)`, but not nearly as fast as a voroni’s `O(1)` or a quadtree’s `O(log n)` lookups. + + +The code for this isn't too complicated: + +```js +var bisect = d3.bisector(d => d.px) + +canvasSel + .call(d3.attachTooltip) + .on('mousemove', function(){ + var [px, py] = d3.mouse(this) + var index = bisect.left(data, px) + + var minPoint = null + var minDist = Infinity + var lxDist = 0 + var rxDist = 0 + var i = 0 + while (lxDist < minDist && rxDist < minDist){ + lxDist = checkPoint(data[index - i]) + rxDist = checkPoint(data[index + i]) + i++ + } + + function checkPoint(d){ + if (!d) return Infinity + + var dx = d.px - px + var dy = d.py - py + var dist = Math.sqrt(dx*dx + dy*dy) + + if (dist < minDist){ + minDist = dist + minPoint = d + } + + return Math.abs(px - d.px) + } + + // update tooltip text + }) +``` + +Still more complicated than simply checking every point though, so I'll probably stick with that unless I’m really trying to push the performance envelope or working with more complex [polygons](https://blocks.roadtolarissa.com/veltman/f539d97e922b918d47e2b2d1a8bcd2dd). + + + + + + + + diff --git a/source/_posts/2019-10-28-winamp-spotify.md b/source/_posts/2019-10-28-winamp-spotify.md new file mode 100644 index 00000000..3cadb16e --- /dev/null +++ b/source/_posts/2019-10-28-winamp-spotify.md @@ -0,0 +1,70 @@ +--- +template: post.html +title: Winampify +date: 2019-10-28 +permalink: /winampify +draft: true +--- + + + + + +
+ +
+ +
+
+
+ +
+
+ + + +Spotify has strict rate limits. To view your own songs checkout the [code](https://github.com/chart-code/winampify). + + + + + + + + + + diff --git a/source/_posts/2019-12-14-pitchfork.md b/source/_posts/2019-12-14-pitchfork.md new file mode 100644 index 00000000..85132007 --- /dev/null +++ b/source/_posts/2019-12-14-pitchfork.md @@ -0,0 +1,36 @@ +--- +template: post.html +title: Pitchfork's Best of the Year, Best of the Decade +title: Pitchfork's Best of the Year Over the Decade +date: 2019-12-31 +permalink: /pitchfork +shareimg: https://i.imgur.com/1TRa5SY.png +--- + + +Every December, Pitchfork ranks their top 50 albums of the year. This year they also picked the [top 200 albums of the 2010s](https://pitchfork.com/features/lists-and-guides/the-200-best-albums-of-the-2010s/). + +
+
+
+ +_Fame Monster_ and _Red_ made the best of the decade, but not the best of the year. + +Some previously well-regarded artists didn't make the cut. James Blake and How to Dress Well both had three best of the year albums and nothing in best of the decade; their later work that scored a 5.8 and 6.8 respectively might have removed some of the previous luster. Others, like Sun Kil Moon and Ariel Pink, dropped off after on-stage misogyny. + +[Brookyln Vegan](http://www.brooklynvegan.com/albums-pitchfork-liked-less-over-time-according-to-their-decade-list/) has a qualitative analysis of who is up and who is down. + +[chart code](https://github.com/1wheel/roadtolarissa/blob/master/source/pitchfork/script.js) // [scraping code](https://github.com/1wheel/scraping-2018/tree/master/pitchfork) // scatter plot and slope chart [sketches](https://blocks.roadtolarissa.com/1wheel/raw/5ec32afde3419ef4f741bccd7405f53b/index.html) + + + + + + + + + + + + + diff --git a/source/_posts/2020-06-13-regression-discontinuity.md b/source/_posts/2020-06-13-regression-discontinuity.md new file mode 100644 index 00000000..e53c028b --- /dev/null +++ b/source/_posts/2020-06-13-regression-discontinuity.md @@ -0,0 +1,51 @@ +--- +template: post.html +title: You Regress It: How Effective Are Face Masks? +title: You Regress It: Have Masks Prevented 66,000 Infections in New York City? +date: 2020-06-14 +permalink: /regression-discontinuity +shareimg: https://i.imgur.com/FL2zUAs.png +--- + + + +A [paper](https://www.pnas.org/content/early/2020/06/10/2009637117/) published last week claiming mask usage prevented 66,000 coronavirus cases in NYC has received [widespread media coverage](https://www.google.com/search?biw=1296&bih=1121&tbm=nws&sxsrf=ALeKk01Enaskz9I8eHTE29TOyN_z3ZhA-g%3A1592334789691&ei=xRnpXtzfKfCRwbkP1JWukAg&q=zhang+pnas+mask&oq=zhang+pnas+mask&gs_l=psy-ab.3...5208.5208.0.5833.1.1.0.0.0.0.72.72.1.1.0....0...1c.1.64.psy-ab..0.0.0....0.MZW-_TMFfIU). It has also been [heavily criticized](https://twitter.com/KateGrabowski/status/1271542361244352514) for, among other things, a tenuous analysis of a [regression discontinuity](https://statmodeling.stat.columbia.edu/2019/06/25/another-regression-discontinuity-disaster-and-what-can-we-learn-from-it/). + +Fitting lines to the number of new cases each day in New York City before and after face masks were mandatory, the authors attribute the steeper decline in cases to face masks: new cases were falling by 39 per day before the order and 106 after, so face masks probably sped up the rate of decrease by about 67 cases a day. + +
![](https://i.imgur.com/YUF5FPU.png)
+ +There are some bold claims along the way ("After April 3, the only difference in the regulatory measures between NYC and the United States lies in face covering on April 17 in NYC"), but let's take a closer look at mechanics of this regression. + +With large day-of-week patterns, the regression is sensitive to the exact start and end dates — can you tweak them to make a chart recommending _against_ mask usage? + +
+ +These small adjustments aren't unreasonable. [Mobility](https://www.google.com/covid19/mobility/) decreased before the lockdown order; the mask order had a [three-day grace period](https://www.nytimes.com/2020/04/15/nyregion/coronavirus-face-masks-andrew-cuomo.html). And there's a variable lag between infections and positive tests that the paper doesn't engage with. Add a seven day lag to account for incubation, testing and reporting to the paper's dates and the regression makes it look like there's a strong case for banning masks! + +With fuzzy boundaries a [local regression](https://en.wikipedia.org/wiki/Local_regression) might be more appropriate. But it's not really possible to do [causal inference](https://twitter.com/NoahHaber/status/1271578680922267649) with case counts from just three regions like this paper tries to do, especially if you're fitting straight lines to exponential infection curves. + +A [growing body](https://www.preprints.org/manuscript/202004.0203/v2/download) of [evidence](https://apps.who.int/iris/rest/bitstreams/1279750/retrieve) supports mask usage. Shoddy statistics published in PNAS (with disconcertingly positive [initial expert reactions](https://www.sciencemediacentre.org/expert-reaction-to-a-study-looking-at-mandatory-face-masks-and-number-of-covid-19-infections-in-new-york-wuhan-and-italy/) and [continued public reference](https://twitter.com/search?q=https%3A%2F%2Fwww.pnas.org%2Fcontent%2Fearly%2F2020%2F06%2F10%2F2009637117&src=typed_query&f=live)) will make it harder to [communicate](https://twitter.com/jeremyfaust/status/1271572240010809347) the results of future research, especially after masks have become politicized and recommendations have shifted. The paper's abstract puts it well: "sound science is essential in decision-making for the current and future public health pandemics." + +
+

[NYC case data](https://github.com/nychealth/coronavirus-data/blob/master/tests.csv) // [chart code](https://github.com/1wheel/roadtolarissa/blob/master/source/regression-discontinuity/script.js) + +

The chart from the paper has been lightly edited for clarity. + +

The top line 66,000 number comes from a similarly suspect regression on cumulative cases. The discontinuous regression was more interesting to illustrate and the results are in the same ballpark. + + +

+ + + + + + + + + + + + + diff --git a/source/_posts/2020-09-02-playoff-probabilities.md b/source/_posts/2020-09-02-playoff-probabilities.md new file mode 100644 index 00000000..7d0b7d78 --- /dev/null +++ b/source/_posts/2020-09-02-playoff-probabilities.md @@ -0,0 +1,44 @@ +--- +template: post.html +title: NBA Playoff Probabilities +date: 2020-09-03 +permalink: /playoff-probabilities +shareimg: https://i.imgur.com/LiSk3Sd.png +--- + + + +
+
+
+ +
+
+
+ +How have NBA playoff probabilities shifted since the tournament started? More straightforward than tracing out changes, we could look at each team's championship chances over time: + +
+
+
+ +But that misses all the stories in each individual series—Orlando suddenly having a shot after stealing their first game, Denver making a comeback and Brooklyn getting crushed. + +Rachael Dottle's [How France And Croatia Made It To The World Cup Final](https://fivethirtyeight.com/features/how-france-and-croatia-made-it-to-the-world-cup-final-in-one-chart/) does an even better job of bringing the drama of a tournament into a chart by incorporating in-game win probability. You can see individual goals! + +
+

[538 2019-20 NBA Predictions](https://projects.fivethirtyeight.com/2020-nba-predictions) // [Chart Code](https://github.com/1wheel/roadtolarissa/blob/master/source/playoff-probabilities/script.js) +

+ + + + + + + + + + + + + diff --git a/source/_posts/2020-10-26-forecast-correlation.md b/source/_posts/2020-10-26-forecast-correlation.md new file mode 100644 index 00000000..74045f5e --- /dev/null +++ b/source/_posts/2020-10-26-forecast-correlation.md @@ -0,0 +1,50 @@ +--- +template: post.html +title: Correlations Between States in Presidential Election Forecasts +title: Election Forecast Correlations +date: 2020-10-25 +permalink: /forecast-correlation +shareimg: https://i.imgur.com/MqUH9IT.png +--- + + + + +[538](https://projects.fivethirtyeight.com/2020-election-forecast/) and the [Economist](https://projects.economist.com/us-2020-forecast/president) have both released detailed data from their election forecasts, listing how each state votes in 40,000 simulations of the presidential election. To understand some unusual scenarios from the 538 model, like every state [voting for Biden but New Jersey](https://twitter.com/gelliottmorris/status/1300480869082292225), Andrew Gelman [examined the correlation](https://statmodeling.stat.columbia.edu/2020/10/24/reverse-engineering-the-problematic-tail-behavior-of-the-fivethirtyeight-presidential-election-forecast/) in the Trump vote share between pairs of several states. + +I was curious what the whole universe of pairwise correlations looked like; you can click on a grid cell below to see voting patterns in two states in more detail along with the electoral maps from individual simulation scenarios. In the 538 model, 242 pairs of states are negatively correlated! + +
+ +Mousing around the edges of the scatter plot pulls out more unusual scenarios, like Trump losing everywhere but `CA-HI-VT`. + +On the correlation matrices, it appears that both models have identified similar groups of states. A scatter plot shows this directly: + +
+ +Outside of the `CA-DC-VT-WA` and `LA-MS-ND-KY` clusters, where the 538 correlation dips below 0, the models are mostly aligned. Glancing over the outliers, it looks like the Economist might not have an equivalent to 538's regional regression that groups states in the same geographic region together; the Economist has `HI` at 0.2 correlation with `WA` & `OR` while 538 has it around 0.7. + +Stepping back from the funky correlation charts, comparing the Trump vote share state by state clearly shows a bigger difference between the models: the Economist model considers really surprising outcomes, like Trump decisively winning `CA`, less likely than the 538 model. + +
+ +I haven't followed the extensive discussion around election modeling closely enough to have a strong opinion on what all of this means, but it does look like the 538 model is allowing for the [possibility](https://twitter.com/Nate_Cohn/status/1320043524771991560) of a broad [realignment in politics](https://twitter.com/NateSilver538/status/1300825856072454145)--something you'd want to incorporate when modeling 2024 today, but not plausible for an election next week with [sixty million ballots](https://www.nytimes.com/interactive/2020/us/elections/absentee-ballot-early-voting.html) already cast. + + +
+

Only 5,000 scenarios are shown on the scatter plots; the scenarios are a snapshot from 2020-10-25 and not updated (looking at the correlations over time might be interesting though!). The rendered electoral college scenarios ignore the possibility of `NE` or `ME` spliting their votes. + +

The correlations matrix orders states by clustering on 538's correlations. Sorting using the Economist correlations [splits up](https://i.imgur.com/JH9FC8I.png) the negative 538 correlations. + +

chart code + +

+ + + + + + + + + diff --git a/source/_posts/2020-11-03-fox-probabilities.md b/source/_posts/2020-11-03-fox-probabilities.md new file mode 100644 index 00000000..5a12c1a8 --- /dev/null +++ b/source/_posts/2020-11-03-fox-probabilities.md @@ -0,0 +1,27 @@ +--- +template: post.html +title: Fox Forecasts Over Time +date: 2020-10-25 +permalink: /fox-forecast +shareimg: https://i.imgur.com/wKV6kcf.png +--- + + + +The [Fox win probability dials](https://www.foxnews.com/elections/2020/general-results/probability-dials) only show their current win probabilities. I noticed the Florida forecast [jumping around](https://twitter.com/adamrpearce/status/1323794382391304194) and wanted to see what the odds looked like over time. + +As of 9:50 PM, Florida has settled down but Fox still shows Biden as having a much better chance in Georgia and North Carolina than the [NYT](https://www.nytimes.com/interactive/2020/11/03/us/elections/forecast-president.html). Nate Cohn [thinks](https://twitter.com/Nate_Cohn/status/1323820129197740040) this might be a result of the Fox model not be taking into account the type of vote outstanding. + +
+ +
+ +

scraping code // chart code + +

+ + + + + + diff --git a/source/_posts/2021-05-04-static-rss.md b/source/_posts/2021-05-04-static-rss.md new file mode 100644 index 00000000..d539956a --- /dev/null +++ b/source/_posts/2021-05-04-static-rss.md @@ -0,0 +1,68 @@ +--- +template: post.html +title: Static RSS Reader +date: 2021-05-06 +permalink: /static-rss +draft: false +--- + + + + + + + + +Ever since the certificate on [Kouio](http://kouio.com/) expired, I've been looking for a replacement RSS reader without fiddly swipping or distracting sticky elements. + +Hosting my own application and database seemed like too much work to just to display some text in a column until I saw [osmosfeed](https://github.com/osmoscraft/osmosfeed) this weekend. It's essentially a script that pulls down RSS feeds and bakes them out into a static page once a day—much less maintenance! + +osmosfeed's UI wasn't quite what I was looking for either, but the idea of working with static files made things simple enough to bang out a prototype, shown below, in [200 lines of code](https://github.com/chart-code/static-rss). + +No database imposes significant limitations (read status doesn't sync 😔), but as a [home-cooked app](https://www.robinsloan.com/notes/home-cooked-app/) it's possible to tweak until everything is just right and then trust it won't change for years. + + + +
+ + + + + + + + diff --git a/source/_posts/2022-02-07-box-office-hits.md b/source/_posts/2022-02-07-box-office-hits.md new file mode 100644 index 00000000..fb6c1384 --- /dev/null +++ b/source/_posts/2022-02-07-box-office-hits.md @@ -0,0 +1,101 @@ +--- +template: post.html +title: Why The Box Office Hits +title: Why Best Picture Winners Aren't Hits Anymore +date: 2022-03-27 +permalink: /box-office-hits +shareimg: https://roadtolarissa.com/box-office-hits/share.png +--- + + +Last year, I read a [statistic](https://www.nytimes.com/2021/12/26/business/movies-stars-hollywood.html) in the NYT that was so surprising I thought it was a typo: + +> “Spider-Man: No Way Home” collected $260 million in the United States and Canada on its opening weekend. Total ticket sales for the two countries totaled $283 million, according to Comscore. **That means “No Way Home” made up 92 percent of the market.** “Nightmare Alley,” which was released on the same weekend, played to virtually empty auditoriums. It took in $2.7 million. + +Did all the other movies in theaters really split just 8% of the audience? Scraping domestic box office numbers, it turns out to be both surprising and true: + + + ### “No Way Home” Is the First to Collect Over 90% of a Weekend Box Office + +
+ +This continued beyond opening weekend: over the next three weekends, “No Way Home” continued to take in over half of the box office. + +Looking at the superhero sequels at the top of the scatter plot, I initially thought this trend was being driven by Hollywood trying to engineer bigger returns from the biggest blockbusters. + +But the pattern is a little more complicated: + +### Yearly Distribution of Domestic Box Office Receipts by Year + +
+
+ +While the top grossing movies have taken a larger share of ticket sales of the last decade, there hasn't been a smooth increase in top-heaviness over the last 30 years. In 2021, the top five grossing movies took more than a quarter of the year's total box office — the first time that's happened since the 80s, when it happened regularly. + +What has changed more smoothly that's caused more weekends to be dominated by a single movie? Extended runs have slowly stopped earning a significant amount of money. + +### The Biggest Movies Are Making More of Their Money Opening Week + +
+ +There's been a bigger change in how media is consumed, beyond the control of Hollywood execs' green-lighting decisions. Television, streaming and TikTok are increasingly good [substitutes](https://www.nytimes.com/2022/03/25/opinion/oscars-movies-end.html) for theater going; people don't "go the movies" anymore, they go to see a specific movie. Superhero movies — which are engineered to create urgency around opening weekend with their franchises, familiar faces, spoilers and massive marketing budgets — have the only formula that can consistently still fill seats. + +This change in consumption has also created a best picture Oscar winner sized hole in the bottom right of the chart (adjust the minimum gross slider to see more recent winners). + +With other genres playing to mostly empty theaters, there's no longer a broad audience that can build up word of mouth momentum for an original movie over several weeks. Best picture winners are still slow burning, but they're no longer hits; even on [streaming](https://www.nytimes.com/2022/03/26/business/media/academy-awards-streaming-services.html), Oscar-favorite CODA has been viewed less than a [million times](https://deadline.com/2022/03/oscar-best-picture-nomiees-box-office-boost-streaming-viewership-1234985202/). + + + + + + + + + + + + + +
+
+ +

[Inflation adjusted](https://help.imdb.com/article/imdbpro/industry-research/box-office-mojo-by-imdbpro-faq/GCWTV4MQKGWRAUAP?ref_=mojo_cso_md#inflation) data [scraped](https://github.com/1wheel/scraping-2018/tree/master/box-office-mojo) from Box Office Mojo // [chart code](https://github.com/1wheel/roadtolarissa/tree/master/source/box-office-hits) + +

+ + + + + + + + + + \ No newline at end of file diff --git a/source/_posts/2022-11-08-2022-live-forecast.md b/source/_posts/2022-11-08-2022-live-forecast.md new file mode 100644 index 00000000..21d82729 --- /dev/null +++ b/source/_posts/2022-11-08-2022-live-forecast.md @@ -0,0 +1,39 @@ +--- +template: post.html +title: 2022 Live Forecasts +date: 2022-08-11 +permalink: /live-forecast-2022 +shareimg: https://roadtolarissa.com/live-forecast-2022/share.png +--- + + +[NYT](https://www.nytimes.com/interactive/2022/11/08/us/elections/results-needle-forecast.html) confidence intervals show the 5%, 25%, 50%, 75% and 95% percentiles of predicted vote margins. + +The [Washington Post](https://www.washingtonpost.com/election-results/2022/senate/) doesn't directly calculate confidence intervals for vote margins. The displayed bands are calculated using the upper vote total estimate from one party and the lower vote total estimate from the other party: + +`dem['upper_0.9'] - gop['lower_0.9']/(dem['upper_0.9'] - gop['lower_0.9'])` + + + + + +
+ + + +
+ +
+ + + +
+ + +
+
+

[scraping code](https://github.com/1wheel/scraping-2018/tree/master/2022-wp) // [chart code](https://github.com/1wheel/roadtolarissa/tree/master/source/live-forecast-2022) +

+ + + diff --git a/source/_posts/2023-02-02-nyc-feed.md b/source/_posts/2023-02-02-nyc-feed.md new file mode 100644 index 00000000..b750f90c --- /dev/null +++ b/source/_posts/2023-02-02-nyc-feed.md @@ -0,0 +1,62 @@ +--- +template: post.html +title: NYC Feed +date: 2021-05-06 +permalink: /nyc-feed +draft: false +--- + + + + + + + + +[sites](https://docs.google.com/spreadsheets/d/1zzrLPJQqxatd5CNhyjcAnjlbGM2haqDL2YY3SCgJ1PA/edit#gid=0) +// +[code](https://github.com/chart-code/static-rss) + +
+ + + + + + + + diff --git a/source/_posts/2024-11-05-2024-live-forecast.md b/source/_posts/2024-11-05-2024-live-forecast.md new file mode 100644 index 00000000..17a3e2af --- /dev/null +++ b/source/_posts/2024-11-05-2024-live-forecast.md @@ -0,0 +1,39 @@ +--- +template: post.html +title: 2024 Live Forecasts +date: 2024-11-05 +permalink: /live-forecast-2024 +shareimg: https://roadtolarissa.com/live-forecast-2024/share.png +--- + + +[NYT](https://www.nytimes.com/interactive/2024/11/05/us/elections/results-president-forecast-needle.html) confidence intervals show the 5%, 25%, 50%, 75% and 95% percentiles of predicted vote margins. + +The [Washington Post](https://www.washingtonpost.com/elections/results/2024/11/05/president/) doesn't directly calculate confidence intervals for vote margins. The displayed bands are calculated using the upper vote total estimate from one party and the lower vote total estimate from the other party: + +`dem['upper_0.9'] - gop['lower_0.9']/(dem['upper_0.9'] - gop['lower_0.9'])` + + + + + +
+ + + +
+ +
+ + + +
+ + +
+
+

[scraping code](https://github.com/1wheel/scraping-2018/tree/master/2024-wp) // [chart code](https://github.com/1wheel/roadtolarissa/tree/master/source/live-forecast-2024) +

+ + + diff --git a/source/_posts/2024-11-10-central-park-rain.md b/source/_posts/2024-11-10-central-park-rain.md new file mode 100644 index 00000000..d84feb3b --- /dev/null +++ b/source/_posts/2024-11-10-central-park-rain.md @@ -0,0 +1,52 @@ +--- +template: post.html +title: 100 Years of Rain +date: 2024-11-10 +permalink: /central-park-rain +shareimg: https://roadtolarissa.com/central-park-rain/share.png +draft: true +--- + + + + +Where's all the rain? + + +For day n, here's how long to reach n inches of precipitation: + +
+
+ + +On a longer scale, we're not in much of drought thanks to heavy rainfall in TK. + +
+
+ + + +Total rainfall by month + +
+ +- with .01" of rain, October 2024 had the least precipitation of any month since record keeping began in 1869. +- Summer 2021 was also unusual with three consecutive months with more than 10" of rain. I have no memory of this. + + +
+
+

[Data from NOAA](https://www.ncei.noaa.gov/cdo-web/) // [chart code](https://github.com/1wheel/roadtolarissa/tree/master/source/central-park-rain) +

+ + + + + +
+ + + + + + diff --git a/source/_posts/2024-12-25-advent-of-code.md b/source/_posts/2024-12-25-advent-of-code.md new file mode 100644 index 00000000..9010306b --- /dev/null +++ b/source/_posts/2024-12-25-advent-of-code.md @@ -0,0 +1,39 @@ +--- +template: post.html +title: Advent of Code Solve Times +date: 2024-12-27 +permalink: /advent-of-code +shareimg: https://roadtolarissa.com/share/advent-of-code.png +--- + +
+
+
+ +For the last decade, [Advent of Code](https://adventofcode.com/) has posted daily programming problems at midnight during the month of December. The first 100 people to solve part 1 and part 2 of the problem are enshrined on the day's leaderboard. + +In 2024 solve times decreased drastically. Before 2023 the five fastest part 1 solves were 23s, 26s, 28s, 28s, and [29s](https://youtu.be/Vl1w7kWRtDg?si=WqSkv5kJ6qbbmQN1&t=145). This year there were forty solves under 20s. Still, some of the trickier problems require human assistance. + +Using Claude to avoid drudgery is great — they wrote the css for this post! — but having an LLM complete this contest on its own feels unsporting. + + + +
+
+

[scraping code](https://github.com/1wheel/scraping-2018/tree/master/2024-advent-of-code) // [chart code](https://github.com/1wheel/roadtolarissa/tree/master/source/advent-of-code) +

+ + + + + +
+ + + + + + + + + diff --git a/source/_templates/post.html b/source/_templates/post.html index 6483b168..5dec47d1 100644 --- a/source/_templates/post.html +++ b/source/_templates/post.html @@ -1,21 +1,24 @@ - + - - + + + - ${d.meta.title} - - - - ${d.meta.shareimg ? `` : ''} - ${d.meta.shareimg ? `` : ''} + + + ${d.title} + + + + ${d.shareimg ? `` : ''} + ${d.shareimg ? `` : ''} - + - +
@@ -25,21 +28,21 @@
Adam Pearce - + github - + twitter - + email - + rss
-

${d.meta.title}

+

${d.title}

${d.html} diff --git a/source/_templates/rss.xml b/source/_templates/rss.xml index 937684d3..eed36615 100644 --- a/source/_templates/rss.xml +++ b/source/_templates/rss.xml @@ -9,20 +9,20 @@ roadtolarissa https://roadtolarissa.com - ${d.filter(post => post.meta.draft != 'true').reverse().map(post => + ${d.filter(post => post.draft != 'true').reverse().map(post => ` - ${post.meta.title} - ${post.meta.shareimg ? ` + ${post.title} + ${post.shareimg ? ` - + + ]]> ` : ''} - https://roadtolarissa.com${post.meta.permalink} + https://roadtolarissa.com${post.permalink} ${(new Date(post.date)).toUTCString()} ` diff --git a/source/_templates/sitemap.xml b/source/_templates/sitemap.xml index f997bbc8..74d4a6f3 100644 --- a/source/_templates/sitemap.xml +++ b/source/_templates/sitemap.xml @@ -1,10 +1,10 @@ - ${d.filter(post => post.meta.draft != 'true').map(post => + ${d.filter(post => post.draft != 'true').map(post => ` 0.5 - https://roadtolarissa.com${post.meta.permalink} + https://roadtolarissa.com${post.permalink} ${post.date} ` diff --git a/source/advent-of-code/TODO.md b/source/advent-of-code/TODO.md new file mode 100644 index 00000000..c9f8097d --- /dev/null +++ b/source/advent-of-code/TODO.md @@ -0,0 +1,41 @@ +x annotatins +x hover +x all user select + +x links +x circles +- slope (too dense) +x click to open problem (or leaderboard?) + +# annotations +- data break +- shortest solve +- 2024 has all the +- biggest/smallest percent/time gap +- day 25 part 2 auto solves +- lowest part1 part2 overlap +- day 11 2016 — Slowest solve after 2015 +- denest solve time/widest +- 1.7988505747126438 ratiop1 '2020_12' + +'2023_10' p1 fastest was 4x faster than the closest person + + + + + +https://old.reddit.com/r/adventofcode/comments/zc27zb/2022_day_4_placing_1st_with_gpt3/ + +https://github.com/ostwilkens/aoc2022 +https://old.reddit.com/r/adventofcode/comments/zb8tdv/2022_day_3_part_1_openai_solved_part_1_in_10/ + +https://web.archive.org/web/20241205061833/https://github.com/hugoromerorico/advent-of-code-24/blob/main/5/to_claude.txt + + +quickest pre 2022 solve + + + + + + diff --git a/source/advent-of-code/annotations.js b/source/advent-of-code/annotations.js new file mode 100644 index 00000000..3e03d0da --- /dev/null +++ b/source/advent-of-code/annotations.js @@ -0,0 +1,143 @@ +window.annotations = [ + { + "parent": ".day-1", + "year": 2024, + "seconds": 4, + "path": "M -42,-22 A 23.783 23.783 0 0 0 -3,-12", + "textOffset": [ + -120, + -32 + ], + "st": { + "width": 100 + }, + "html": "Part 1 four second solve" + }, + { + "parent": ".day-1", + "year": 2020.5, + "seconds": 465, + "path": "M 16,-50 A 17.054 17.054 0 0 1 11,-17", + "textOffset": [ + -54, + -56 + ], + "st": { + "width": 100 + }, + "textx": "Leaderboard server OOMs", + "html": "Leaderboard server OOM" + }, + { + "parent": ".day-3", + "year": 2022, + "seconds": 10, + "path": "M -42,-19 A 20.92 20.92 0 0 0 -4,-10", + "textOffset": [ + -76, + -31 + ], + "st": { + "width": 80 + }, + "isDraggable": 0, + "textx": "First LLM solves in 2022", + "html": "First LLM solves in 2022" + }, + { + "parent": ".day-5", + "year": 2023.5, + "seconds": 1600, + "path": "M 5,-40 A 12.135 12.135 0 0 1 5,-17", + "textOffset": [ + -135, + -31 + ], + "st": { + "width": 140 + }, + "isDraggable": 0, + "html": "100 part 2 solves on the leaderboard..." + }, + { + "parent": ".day-5", + "year": 2023, + "seconds": 95, + "path": "M -16,13 A 15.654 15.654 0 0 0 1,-8", + "textOffset": [ + -110, + 13 + ], + "st": { + "width": 180 + }, + "class": "align-right", + "isDraggable": 0, + "html": "...and 100
part 1 solves" + }, + { + "parent": ".day-10", + "year": 2023, + "seconds": 65, + "path": "M -15,5 A 13.71 13.71 0 0 0 0,-10", + "textOffset": [ + -149, + 13 + ], + "st": { + "width": 130 + }, + "class": "align-right", + "isDraggable": 0, + "html": "The winner solved 4× faster than the runner up" + }, + { + "parent": ".day-12", + "year": 2024, + "seconds": 14, + "path": "M -21,1 A 12.573 12.573 0 0 0 0,-10", + "textOffset": [ + -140, + -30 + ], + "st": { + "width": 130 + }, + "class": "", + "isDraggable": 0, + "html": "Only 3 people made
both leaderboards on day 12, 2024 — the average is 64" + }, + { + "parent": ".day-17", + "year": 2018, + "seconds": 1964, + "path": "M 15,65 A 93.378 93.378 0 0 1 -1,-10", + "textOffset": [ + -45, + 89 + ], + "st": { + "width": 160 + }, + "isDraggable": 0, + "textx": "First LLM solves in 2022", + "html": "A very straightforward part 2 — 95% leaderboard overlap with part 1" + }, + { + "parent": ".day-25", + "year": 2018, + "seconds": 1964, + "path": "M 0 0", + "textOffset": [ + -45, + 105 + ], + "st": { + "width": 180 + }, + "isDraggable": 0, + "html": "On Christmas, part 2 just requires clicking a button" + } +] + +window.init?.() \ No newline at end of file diff --git a/source/advent-of-code/init-swoopy.js b/source/advent-of-code/init-swoopy.js new file mode 100644 index 00000000..2b93a686 --- /dev/null +++ b/source/advent-of-code/init-swoopy.js @@ -0,0 +1,61 @@ +/* +to update arrows: + 1. set isDraggable = true + 2. tweaks arrows by dragging them + 3. run this in the dev tools: + copy('window.annotations = ' + JSON.stringify(window.annotations, null, 2) + '\n\nwindow.init?.()') + 4. paste in the init file +*/ + + +window.initSwoopy = function(annotations, c){ + // d3.selectAll('.annotation-container').remove() + + annotations.forEach(d => { + var isDraggable = !!annotations.isDraggable || d.isDraggable || 0 + + var sel = d3.select(d.parent) + .append('div.annotation-container') + .classed('is-draggable', isDraggable) + .html('') + .st(d.st) + .translate([c.x(d.year), c.y(d.seconds)]) + + if (d.class) d.class.split(' ').forEach(str => sel.classed(str, 1)) + + if (d.minWidth && d.minWidth > window.innerWidth){ + sel.st({display: 'none'}) + } + + var htmlSel = sel.append('div').html(d.html).translate(d.textOffset) + + var swoopy = d3.swoopyDrag() + .x(d => 0).y(d => 0) + .draggable(isDraggable) + .annotations([d]) + + sel.append('svg').at({width: 1, height: 1, 'textAlign': 'left'}).call(swoopy) + }) + + + d3.select('body').selectAppend('svg.arrow-svg').html('') + .st({height: 0}) + .append('marker') + .attr('id', 'arrow') + .attr('viewBox', '-10 -10 20 20') + .attr('markerWidth', 20) + .attr('markerHeight', 20) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M-10,-10 L 0,0 L -10,10') + .st({stroke: '#fff', fill: 'none', }) + + d3.selectAll('.annotation-container path') + .at({ + markerEnd: 'url(#arrow)', + strokeWidth: .5, + opacity: d => d.path == 'M 0 0' ? 0 : '', + }) +} + +window.init?.() diff --git a/source/advent-of-code/init.js b/source/advent-of-code/init.js new file mode 100644 index 00000000..48be04f8 --- /dev/null +++ b/source/advent-of-code/init.js @@ -0,0 +1,197 @@ +window.visState = window.visState || { +} + +var ttSel = d3.select('.tooltip') + + +window.init = function(){ + console.clear() + + window.byDay = d3.nestBy(tidy, d => d.day) + + var prevInnerWidth = -1 + function drawDays(){ + if (prevInnerWidth == window.innerWidth) return + prevInnerWidth = window.innerWidth + + d3.select('.graph').html('') + .appendMany('div.day', byDay) + .each(drawDate) + + byDay.forEach(day => day.setName(util.params.get('name'))) + window.initSwoopy(window.annotations, byDay[0].c) + } + drawDays() + d3.select(window).on('resize', _.throttle(drawDays, 200)) + + + // stats + console.log('<30s & year < 22', tidy.filter(d => d.seconds < 30 && d.part == 1 && d.year < 2022).length) + console.log('<30s & year = 24', tidy.filter(d => d.seconds < 30 && d.part == 1 && d.year == 2024).length) + console.log('<20s & year = 24', tidy.filter(d => d.seconds < 20 && d.part == 1 && d.year == 2024).length) + console.log('<10s & year = 24', tidy.filter(d => d.seconds < 10 && d.part == 1 && d.year == 2024).length) + + console.log('part 1 solves faster than 30s:') + console.table(tidy.filter(d => d.seconds < 30 && d.part == 1)) + + // '2024_12' 3 numBoth — avg is 64.3 + var byProblem = d3.nestBy(tidy, d => d.year + '_' + d.day) + byProblem.forEach(problem => { + problem.numBoth = d3.nestBy(problem, d => d.name).filter(d => d.length == 2).length + + var p1 = problem.filter(d => d.part == 1) + var p2 = problem.filter(d => d.part == 2) + problem.ratio = d3.max(problem, d => d.seconds)/d3.min(problem, d => d.seconds) + problem.ratiop1 = d3.max(p1, d => d.seconds)/d3.min(p1, d => d.seconds) + problem.ratiop2 = d3.max(p2, d => d.seconds)/d3.min(p2, d => d.seconds) + problem.diffp1 = d3.max(p1, d => d.seconds) - d3.min(p1, d => d.seconds) + problem.ratiop1_12 = p1[1].seconds/p1[0].seconds + problem.diffp1_12 = p1[1].seconds - p1[0].seconds + }) + console.log('daily summary stats: ') + console.table(byProblem.map(({key, numBoth, ratio, ratiop1, ratiop2, diffp1, ratiop1_12, diffp1_12}) => ({key, numBoth, ratio, ratiop1, ratiop2, diffp1, ratiop1_12, diffp1_12}))) + // console.log(d3.mean(byProblem, d => d.numBoth)) + console.log('Reloaded with the dev tools open to see more data tables') + // http://localhost:3989/advent-of-code/?name=Adam%2520Pearce +} + +function drawDate(dayData){ + var c = d3.conventions({ + sel: d3.select(this).append('div').classed('day-' + dayData.key, 1), + height: 200, + layers: 'scs', + margin: {left: 0, right: 0, top: 0, bottom: 0} + }) + + c.x.domain([2014.9, 2024.9]).interpolate(d3.interpolateRound) + c.y = d3.scaleLog().domain([3, 4*60*60]).range([c.height, 0]).interpolate(d3.interpolateRound) + + c.xAxis.tickFormat(d => d % 2 ? '' : "'" + (d % 100)) + c.yAxis = d3.axisLeft(c.y) + .tickValues([6, 60, 6*60, 60*60]) + .tickFormat(d => { + if (d < 60) return d + 's'; + if (d < 60*60) return d/60 + 'm'; + return d/(60*60) + 'h'; + }) + + d3.drawAxis(c) + util.ggPlot(c) + c.svg.select('.x').translate([.5, c.height]) + + c.x.interpolate(d3.interpolate) + c.y.interpolate(d3.interpolate) + + c.svg.append('text.day-label').text('Day ' + dayData.key) + .at({fill: '#999', fontSize: 9, x: c.width - 5, textAnchor: 'end', dy: 14}) + + d3.nestBy(dayData, d => d.part + '_' + d.year) + .forEach(part => part.forEach((d, i) => d.rank = i)) + + var yw = c.x(2016) - c.x(2015) + var s = 1.1 + dayData.forEach(d => { + d.px = c.x(d.year) + d.rank/100*yw/2 + d.py = c.y(d.seconds) + }) + + var ctx = c.layers[1] + // d3.nestBy(dayData, d => d.name + '_' + d.year) + // .filter(d => d.length == 2) + // .forEach(([a, b]) => { + // ctx.beginPath() + // ctx.strokeStyle = 'rgba(255,255,255,.04)' + // ctx.strokeWidth = .01 + // ctx.moveTo(a.px, a.py) + // ctx.lineTo(b.px, b.py) + // ctx.stroke() + // }) + + dayData.forEach(d =>{ + ctx.beginPath() + ctx.fillStyle = d.part == 1 ? '#9999cc' : '#ffff66' + ctx.moveTo(d.px + s, d.py) + ctx.arc(d.px, d.py, s, 0, 2 * Math.PI) + ctx.fill() + }) + + c.sel.select('canvas').st({pointerEvents: 'none'}) + c.layers[2].parent().st({pointerEvents: 'none'}) + + var nameSel = c.layers[2].appendMany('circle.glow', d3.range(20)) + .st({r: 3, stroke: '#0c0', fill: 'none', display: 'none'}) + + function findMatch([px, py]){ + function calcDist(d){ + var dx = d.px - px + var dy = d.py - py + return dx*dx + dy*dy + } + + var match = _.minBy(dayData, calcDist) + var dist = calcDist(match) + return {match, dist} + } + + c.svg + .st({cursor: 'pointer'}) + .on('mousemove', function(){ + if (d3.event.shiftKey) return + var {match, dist} = findMatch(d3.mouse(this)) + setActive(dist < 400 ? match : null) + }) + .on('click', function(){ + if (d3.event.shiftKey) return + var {match, dist} = findMatch(d3.mouse(this)) + window.open(`https://adventofcode.com/${match.year}/day/${match.day}`, '_blank') + }) + .on('mouseleave', () => { + if (d3.event.shiftKey) return + setActive(null) + }) + .call(d3.attachTooltip) + .on('mouseover.attachTooltip', _ => null) + + function setActive(d){ + byDay.forEach(day => day.setName(d?.name)) + util.params.set('name', d?.name) + + if (!d) return ttSel.classed('tooltip-hidden', 1) + // console.log(d) + ttSel.classed('tooltip-hidden', 0) + .html(` +
${d.year} Day ${d.day} Part ${d.part}
+
solved in ${d.seconds}s by
+
${d.name}
+ `) + } + + dayData.setName = name => { + var zeros = d3.range(20).map(_ => 0) + var nameData = dayData.filter(d => d.name == name).concat(zeros).slice(0, 20) + nameSel.data(nameData) + .st({display: d => d ? '' : 'none'}) + .filter(d => d) + .translate(d => [d.px, d.py]) + } + dayData.c = c +} + + + +if (!window.tidy){ + d3.loadData('https://roadtolarissa.com/data/2024-advent-of-code-tidy.tsv', (err, res) => { + window.tidy = res[0] + + tidy.forEach(d => { + d.part = +d.part + d.seconds = +d.seconds + d.year = +d.year + d.day = +d.day + }) + init() + }) +} else{ + init() +} + diff --git a/source/advent-of-code/style.css b/source/advent-of-code/style.css new file mode 100644 index 00000000..aceab74e --- /dev/null +++ b/source/advent-of-code/style.css @@ -0,0 +1,192 @@ +a, .header{ + color: #fff; + font-family: monospace; +} +.header a img{ + filter: invert(100%); +} + +h1, p{ + font-family: monospace; +} + + + +.tooltip { + top: -1000px; + position: fixed; + padding: 7px; + background: rgba(0, 0, 0, .8); + border: .1px solid rgba(255, 255, 255, .3); + pointer-events: none; + width: 130px; + font: 10px monospace; + line-height: 1.3em; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +.tooltip text{ + text-shadow: none; + fill: #000 !important; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.axis{ + stroke-width: .5; +} +.domain{ + display: none; +} + +text{ +/* pointer-events: none;*/ +/* text-shadow: 0 1px 0 #eee, 1px 0 0 #eee, 0 -1px 0 #eee, -1px 0 0 #eee;*/ +} + +text.day-label{ + text-shadow: 0 2px 0 #0F0F22, 2px 0 0 #0F0F22, 0 -2px 0 #0F0F22, -2px 0 0 #0F0F22; +} + +.axis text{ + fill: #777; + font: 9px monospace; +} + +#notes{ + font-family: monospace; + font-size: 12px; + opacity: .7; + color: #fff; +} + + + +html{ + background: #000; + color: #fff; + min-width: 1024px; + font-family: monospace; +} + +.full-bleed { + margin-left: calc(50% - max(50vw, 512px)); + margin-right: calc(50% - max(50vw, 512px)); + width: max(100vw, 1024px); +} + +.graph { + max-width: 1440px; + padding: 0 20px; + margin: 0 auto; + margin-bottom: 60px; + + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 30px 35px; + font-family: monospace; + min-height: 1000px; + overflow: hidden; +} + + + +:root { + --green: #0c0; + --silver: #9999cc; + --gold: #ffff66; +} + +.glow{ + filter: drop-shadow(0 0 2px var(--green)) + drop-shadow(0 0 8px var(--green)); + color: var(--green); + stroke: var(--green); +} + +.part-1{ + filter: drop-shadow(0 0 1.5px var(--silver)) + drop-shadow(0 0 8px var(--silver)); + color: var(--silver); +} + +.part-2 { + filter: drop-shadow(0 0 1.5px var(--gold)) + drop-shadow(0 0 8px var(--gold)); + color: var(--gold); +} + + + + + + +.annotation-container{ + position: absolute; + pointer-events: none; + font-family: monospace; + font-size: 11px; + +/* opacity: .5;*/ + + div{ + position: absolute; + line-height: 1.1em; + } +} +.annotation-container.is-draggable{ + pointer-events: all; +} + +.annotation-container path{ + stroke: #fff; + fill: none; + +} +.annotation-container a{ + pointer-events: all; +} +.annotation-container.align-right div{ + text-align: right; +} +.annotation-container svg{ + position: relative; + text{ + fill: #fff; + } +} + +.annotation-container div ni{ + display: inline-block; + font-family: sans-serif; + font-style: normal; + font-size: 10px; +} + + +.annotation-container div, .overlay-chart-label{ +/* text-shadow: 0 1px 0 #0F0F22, 1px 0 0 #0F0F22, 0 -1px 0 #0F0F22, -1px 0 0 #0F0F22;*/ + user-select: none; +} + + +* > *{ +/* outline: 1px solid red;*/ +} diff --git a/source/advent-of-code/util.js b/source/advent-of-code/util.js new file mode 100644 index 00000000..7c6c4cf5 --- /dev/null +++ b/source/advent-of-code/util.js @@ -0,0 +1,68 @@ +window.util = (function(){ + var params = (function(){ + var url = new URL(window.location) + var searchParams = new URLSearchParams(url.search) + + return { + set: (k, v) => { + v === null || v === undefined ? searchParams.delete(k) : searchParams.set(k, encodeURIComponent(v)) + url.search = searchParams.toString() + history.replaceState(null, '', url) + }, + get: k => { + var str = searchParams.get(k) + return str && decodeURIComponent(str) + }, + getAll: () => { + var values = {} + for (const [k, v] of searchParams.entries()){ + values[k] = decodeURIComponent(v) + } + return values + }, + } + })() + + function setFullWidth(sel, width){ + width = width || innerWidth - 20 + var pWidth = d3.select('p').node().offsetWidth + var marginLeft = -(width - pWidth)/2 + sel.st({width, marginLeft}) + } + + function addAxisLabel(c, xText, yText, xOffset=30, yOffset=-30){ + c.svg.select('.x').append('g') + .translate([c.width/2, xOffset]) + .append('text.axis-label') + .text(xText) + .at({textAnchor: 'middle'}) + + c.svg.select('.y') + .append('g') + .translate([yOffset, c.height/2]) + .append('text.axis-label') + .text(yText) + .at({textAnchor: 'middle', transform: 'rotate(-90)'}) + } + + function ggPlot(c, isBlack=true){ + c.svg.append('rect.bg-rect') + .st({height: c.height, width: c.width, fill: '#0F0F22'}).lower() + + c.svg.selectAll('.tick').selectAll('line').remove() + c.svg.selectAll('.y .tick') + .append('path').at({d: 'M 0 0 H ' + c.width, stroke: '#fff', strokeWidth: .15}) + c.svg.selectAll('.y text').at({x: -3}) + c.svg.selectAll('.x .tick') + .append('path').at({d: 'M 0 0 V -' + c.height, stroke: '#fff', strokeWidth: .15}) + c.svg.selectAll('.x text').at({y: 3}) + + } + + var shortMonths = ["Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."] + var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + + return {params, addAxisLabel, ggPlot, setFullWidth, shortMonths, months} +})() + +window.init?.() \ No newline at end of file diff --git a/source/box-office-hits/TODO b/source/box-office-hits/TODO new file mode 100644 index 00000000..d994ca5f --- /dev/null +++ b/source/box-office-hits/TODO @@ -0,0 +1,5 @@ +x conclusion +x links +- color legend +- search box +x oscar filter \ No newline at end of file diff --git a/source/box-office-hits/bo_mojo_inflation.csv b/source/box-office-hits/bo_mojo_inflation.csv new file mode 100644 index 00000000..01248f44 --- /dev/null +++ b/source/box-office-hits/bo_mojo_inflation.csv @@ -0,0 +1,83 @@ +year,price_per_ticket +2022,9.37 +2021,9.37 +2020,9.37 +2019,9.01 +2018,9.11 +2017,8.97 +2016,8.65 +2015,8.43 +2014,8.17 +2013,8.13 +2012,7.96 +2011,7.93 +2010,7.89 +2009,7.50 +2008,7.18 +2007,6.88 +2006,6.55 +2005,6.41 +2004,6.21 +2003,6.03 +2002,5.81 +2001,5.66 +2000,5.39 +1999,5.08 +1998,4.69 +1997,4.59 +1996,4.42 +1995,4.35 +1994,4.18 +1993,4.14 +1992,4.15 +1991,4.21 +1990,4.23 +1989,3.97 +1988,4.11 +1987,3.91 +1986,3.71 +1985,3.55 +1984,3.36 +1983,3.15 +1982,2.94 +1981,2.78 +1980,2.69 +1979,2.51 +1978,2.34 +1977,2.23 +1976,2.13 +1975,2.05 +1974,1.87 +1973,1.77 +1972,1.70 +1971,1.65 +1970,1.55 +1969,1.42 +1968,1.31 +1967,1.20 +1966,1.09 +1965,1.01 +1964,0.93 +1963,0.85 +1962,0.70 +1961,0.69 +1959,0.51 +1956,0.50 +1954,0.45 +1953,0.60 +1951,0.53 +1949,0.46 +1948,0.40 +1945,0.35 +1944,0.32 +1943,0.29 +1942,0.27 +1941,0.25 +1940,0.24 +1939,0.23 +1936,0.25 +1935,0.24 +1934,0.23 +1929,0.35 +1924,0.25 +1910,0.07 \ No newline at end of file diff --git a/source/box-office-hits/draw-best-week-scatter.js b/source/box-office-hits/draw-best-week-scatter.js new file mode 100644 index 00000000..0822a827 --- /dev/null +++ b/source/box-office-hits/draw-best-week-scatter.js @@ -0,0 +1,320 @@ +var oscarWinners = d3.csvParse(`year,name +2021,Nomadland +2020,Parasite +2019,Green Book +2018,The Shape of Water +2017,Moonlight +2016,Spotlight +2015,Birdman or (The Unexpected Virtue of Ignorance) +2014,12 Years a Slave +2013,Argo +2012,The Artist +2011,The King's Speech +2010,The Hurt Locker +2009,Slumdog Millionaire +2008,No Country for Old Men +2007,The Departed +2006,Crash +2005,Million Dollar Baby +2004,The Lord of the Rings: The Return of the King +2003,Chicago +2002,A Beautiful Mind +2001,Gladiator +2000,American Beauty +1999,Shakespeare in Love +1998,Titanic +1997,The English Patient +1996,Braveheart +1995,Forrest Gump +1994,Schindler's List +1993,Unforgiven +1992,The Silence of the Lambs +1991,Dances with Wolves +1990,Driving Miss Daisy +1989,Rain Man +1988,The Last Emperor +1987,Platoon +1986,Out of Africa +1985,Amadeus +1984,Terms of Endearment +1983,Gandhi +1982,Chariots of Fire +1981,Ordinary People +1980,Kramer vs. Kramer +1979,The Deer Hunter +1978,Annie Hall +1977,Rocky +1976,One Flew over the Cuckoo's Nest +1975,The Godfather Part II +1974,The Sting +1973,The Godfather +1972,The French Connection +1971,Patton`) + + +var bestWeeekAnnotations = [ + {key: 'rl995132929', align: '', isBot: 1, str: 'E.T.'}, + {key: 'rl3949954561', align: 'r', isBot: 1, str: `Schindler's List`, mHide: 1}, + // {key: 'rl342132225', align: '', isBot: 1, str: 'My Big Fat Greek Wedding'}, + {key: 'rl21399041', align: 'r', isBot: 0, str: 'A Beautiful Mind', y: 3, x: 2}, + {key: 'rl1816888833', align: 'r', isBot: 1, str: 'Chicago', y: -5, x: 2}, + // {key: 'rl2839774721', align: 'm', isBot: 1, str: 'American Beauty', y: 0, x: -20}, + {key: 'rl876971521', align: '', isBot: 1, str: 'Avatar'}, + {key: 'rl357926401', align: '', isBot: 1, str: 'Frozen'}, + {key: 'rl944539137', align: '', isBot: 1, str: 'Dances with Wolves', mHide: 1}, + // {key: 'rl2053801473', align: 'r', isBot: 1, str: 'Pulp Fiction'}, + {key: 'rl3698624001', align: '', isBot: 1, str: 'Titanic',mHide: 1}, + + + // {key: 'rl755467777', align: '', isBot: 1, str: 'Jumanji: The Next Level'}, + + {key: 'rl2993915393', align: 'r', str: 'Star Trek II'}, + {key: 'rl3629483521', align: 'l', isBot: 1, str: 'Ghostbusters II', y: -3, x: 0, mHide: 1}, + {key: 'rl1380943361', align: 'l', str: 'Die Hard 2', y: 2, x: 2}, + {key: 'rl3544548865', align: '', str: 'Batman Returns', mHide: 1}, + {key: 'rl3561326081', align: 'l', str: 'Batman & Robin'}, + {key: 'rl2488829441', align: 'l', str: 'Hulk'}, + {key: 'rl3292956161', align: '', str: 'The Twilight Saga', x: -2, y: -2}, + {key: 'rl3561326081', align: 'l', str: 'Batman & Robin'}, + {key: 'rl2238875137', align: 'r', str: 'Batman v Superman', mHide: 1}, + +] + +window.drawBestWeekScatter = function({byMovie}){ + var oscarColor = '#f0f' + + var isOscar = {} + oscarWinners.forEach(d => isOscar[d.name.toLowerCase()] = d.year) + var key2annotation = {} + bestWeeekAnnotations.forEach(d => key2annotation[d.key] = d) + + byMovie.forEach(d => { + d.isOscar = isOscar[d.name.toLowerCase()] && Math.abs(isOscar[d.name.toLowerCase()] - d.year) < 4 + d.annotation = key2annotation[d.key] + }) + + window.byMovie = byMovie + + var topMovies = byMovie + .filter(d => d.gross > 10000000) + // .filter(d => d.gross > 200000000) + .filter(d => 1981 < d.year && d.year < 2022) + .filter(d => d.highestGrossPercent < .9) + + var sel = d3.select('.best-week-scatter').html('') + + var c = d3.conventions({ + sel: sel.append('div'), + height: 500, + margin: {left: 25, bottom: 40, top: 10}, + layers: 'sd', + }) + + c.x.domain([1982, 2022]) + c.y.domain([0, .8]) + + c.xAxis.tickFormat(d => d) + c.yAxis.tickFormat(d3.format('.0%')) + d3.drawAxis(c) + util.ggPlot(c) + + c.svg.select('.y .tick:last-child') + .append('text') + .text(`of movie’s total domestic gross earned in its best week`) + .at({textAnchor: 'start', dy: '.33em'}) + .parent() + .select('path').at({d: `M ${c.x(1990)} 0 H ${c.width}`}) + + var rScale = d3.scaleSqrt().domain([0, 1e9]).range([0, 10]) + var circleSel = c.svg.appendMany('circle', topMovies) + .translate(d => [c.x(d.year + d[0].week/52), c.y(d.highestGrossPercent)]) + .at({ + r: d => rScale(d.gross), + fill: 'rgba(0,0,0,.1)', + stroke: '#000', + }) + .call(d3.attachTooltip) + .on('mouseover', d => { + window.ttSel.html(` +
+ ${d.name} + +
+ Total Gross: $${d3.format(',')(Math.round(d.gross))} — + + ${d3.format('.0%')(d.highestGrossPercent)} during its best week. + `) + + var c = d3.conventions({ + sel: window.ttSel.append('div'), + height: 100, + width: 26*8, + margin: {left: 40, top: 10, right: 0, bottom: 15}, + }) + + var movie = d + c.x.domain([0, movie.length]) + c.x.domain([0, 26]) + c.y.domain([0, d3.max(movie, d => d.gross)]) + + c.xAxis.ticks(0) + c.yAxis.ticks(3).tickFormat(d => '$' + d/1000000 + 'M') + d3.drawAxis(c) + + c.svg.select('.x').append('text') + .text('Weeks since release →') + .at({x: c.x(0), y: 12, textAnchor: 'start'}) + + c.svg.selectAll('.y .tick line').at({x1: -3}) + + c.svg.appendMany('rect', movie) + .at({ + x: (d, i) => c.x(i), + y: d => Math.min(c.height - 1, c.y(d.gross)) + 1, + height: d => Math.max(1, c.height - c.y(d.gross)), + width: c.x(1) - c.x(0) - 1, + }) + + }) + + var annoSel = c.svg.appendMany('text.annotation', topMovies.filter(d => d.annotation)) + .translate(d => [c.x(d.year + d[0].week/52), c.y(d.highestGrossPercent)]) + .text(d => d.annotation.str) + .at({ + textAnchor: d => ({l: 'end', r: 'start'}[d.annotation.align] || 'middle'), + dy: d => d.annotation.isBot ? 11 + rScale(d.gross) : -3 - rScale(d.gross), + dx: d => d.annotation.align == 'r' ? 0 + rScale(d.gross) : d.annotation.align == 'l' ? -0 -rScale(d.gross) : 0, + x: d => d.annotation.x || 0, + y: d => d.annotation.y || 0, + fontWeight: d => d.annotation.weight || '', + }) + .st({ + pointerEvents: 'none', + }) + .classed('m-hide', d => d.annotation.mHide) + + var labels = [ + {pos: [c.x(2015.4), c.y(.192)], html: `Since 2005, Lincoln and Frozen are the only movies to gross more than $200M while selling less than 20% of their tickets opening week — something best picture Oscar winners used to consistently do.`} + ] + + var labelSel = c.layers[1] + .st({pointerEvents: 'none'}) + .appendMany('div.annotation.m-hide', labels) + .translate(d => d.pos) + .st({width: 164, lineHeight: 10}) + .html(d => d.html) + + d3.selectAll('x').st({color: oscarColor}) + + var allAnnoSel = c.sel.selectAll('.annotation') + + var state = {minGross: 200000000, isOscar: 1} + + function renderCircles(){ + circleSel + .st({display: d => d.gross <= state.minGross ? 'none' : ''}) + + if (state.isOscar){ + circleSel + .filter(d => d.isOscar) + .at({stroke: oscarColor, fill: oscarColor, fillOpacity: .2}) + // .raise() + + allAnnoSel + .filter(d => d.isOscar) + .st({fill: oscarColor}) + + } else{ + circleSel.order((a, b) => a.gross - b.gross) + + allAnnoSel + .st({fill: ''}) + } + + allAnnoSel.classed('hidden', state.minGross != 200000000) + } + + + function addSliders(){ + var width = 140 + var height = 30 + var color = '#000' + + var sliders = [ + {key: 'minGross', label: 'Minimum Gross', r: [10000000, 200000000]}, + ] + sliders.forEach(d => { + d.value = state[d.key] + d.xScale = d3.scaleLinear().range([0, width]).domain(d.r).clamp(1) + }) + + var svgSel = sel + // .st({marginTop: 5, marginBottom: 5}) + .appendMany('div.slider-container', sliders) + .st({width: 140, userSelect: 'none'}) + .translate((c.width - 140)/2, 0) + .append('svg').at({width, height}) + .append('g').translate([10, 25]) + + var sliderSel = svgSel + .on('click', function(d){ + d.value = d.xScale.invert(d3.mouse(this)[0]) + renderSliders(d) + }) + .classed('slider', true) + .st({cursor: 'pointer'}) + + var textSel = sliderSel.append('text') + .at({y: -15, fontWeight: 300, textAnchor: 'middle', x: 140/2}) + .st({fontSize: 12, fontFamily: 'sans-serif'}) + + sliderSel.append('rect') + .at({width, height, y: -height/2, fill: 'rgba(0,0,0,0)'}) + + sliderSel.append('path').at({ + d: `M 0 -.5 H ${width}`, + stroke: color, + strokeWidth: 1 + }) + + var leftPathSel = sliderSel.append('path').at({ + d: `M 0 -.5 H ${width}`, + stroke: color, + strokeWidth: 3 + }) + + var drag = d3.drag() + .on('drag', function(d){ + var x = d3.mouse(this)[0] + d.value = d.xScale.invert(x) + + renderSliders(d) + }) + + var circleSel = sliderSel.append('circle').call(drag) + .at({r: rScale(200000000) + 2, stroke: 'rgba(0,0,0,0)', strokeWidth: 30}) + + var innerCircleSel = sliderSel.append('circle').call(drag) + .at({fill: '#ccc'}) + + function renderSliders(d){ + if (d) state[d.key] = d.value + + circleSel + .at({cx: d => d.xScale(d.value)}) + innerCircleSel + .at({cx: d => d.xScale(d.value), r: rScale(state.minGross)}) + leftPathSel.at({d: d => `M 0 -.5 H ${d.xScale(d.value)}`}) + textSel + // .at({x: d => d.xScale(d.value)}) + .text(d => 'Minimum Gross: ' + d3.format('$,')(Math.round(d.value))) + renderCircles() + } + renderSliders() + } + addSliders() + +} + + +if (window.init) window.init() \ No newline at end of file diff --git a/source/box-office-hits/draw-weekly-top-percent.js b/source/box-office-hits/draw-weekly-top-percent.js new file mode 100644 index 00000000..61c69ea4 --- /dev/null +++ b/source/box-office-hits/draw-weekly-top-percent.js @@ -0,0 +1,112 @@ +var annotations = [ + {key: '1982 20', align: 'r', str: 'Conan', mHide: 1}, + {key: '1984 21', align: '', str: 'Temple of Doom'}, + {key: '1987 21', align: 'l', str: 'Beverly Hills Cop II', mHide: 1}, + {key: '1992 20', align: 'l', str: 'Lethal Weapon 3'}, + {key: '1984 21', align: '', str: 'Temple of Doom'}, + {key: '1994 51', align: '', str: 'Street Fighter'}, + {key: '1996 19', align: '', str: 'Twister'}, + {key: '2002 18', align: '', str: 'Spider-Man'}, + {key: '2007 18', align: 'l', str: 'Spider-Man 3'}, + {key: '2010 19', align: '', str: 'Iron Man 2'}, + {key: '2012 18', align: 'l', str: 'The Avengers', mHide: 1}, + {key: '2015 18', align: 'l', str: 'Age of Ultron'}, + {key: '2018 17', align: '', str: 'Infinity War', y: -1, mHide: 1}, + {key: '2019 17', align: 'l', str: 'Avengers: Endgame', y: 8, x: -5}, + {key: '2021 51', align: 'l', weight: 700, str: 'Spider-Man: No Way Home'}, + {key: '1992 25', align: 'l', str: 'Batman Returns', mHide: 1}, + {key: '1996 19', align: '', str: 'Twister'}, + {key: '1993 24', align: 'l', str: 'Jurassic Park'}, +] +var key2annotation = {} +annotations.forEach(d => key2annotation[d.key] = d) + +window.drawWeeklyTopPercent = function({byWeek, byMovie}){ + var sel = d3.select('.weekly-top-percent').html('') + .st({marginBottom: -10}) + + // sel.append('h3') + // .text(`“No Way Home” Collected 92% of the Domestic Box Office Opening Weekend`) + // .text(`“No Way Home” Was the First Movie To Collect More Than 90% Of A Weekend Box Office`) + // .st({width: 800}) + + var c = d3.conventions({ + sel: sel.append('div'), + // width: 800, + height: 500, + margin: {left: 30, bottom: 30, top: 10} + }) + + c.sel.append('h3') + + + c.x.domain([1982, 2022]) + + c.xAxis.tickFormat(d => d) + c.yAxis.tickFormat(d3.format('.0%')) + d3.drawAxis(c) + util.ggPlot(c) + + c.svg.select('.y .tick:last-child') + .append('text') + .text(`of Weekend Box Office Taken by The Top Grossing Movie`) + .text(`of weekend's domestic box office taken by the top grossing movie`) + .at({textAnchor: 'start', dy: '.33em'}) + .parent() + .select('path').at({d: `M ${c.x(1994)} 0 H ${c.width}`}) + + byWeek.forEach(d => d.annotation = key2annotation[d.year + ' ' + d.week]) + + var circleSel = c.svg.appendMany('circle', byWeek) + .translate(d => [c.x(d.year + d.week/52), c.y(d[0].percent)]) + .at({r: 2, fill: 'rgba(0,0,0,0)', stroke: '#000'}) + .st({display: d => d.isHidden ? 'none' : ''}) + .call(d3.attachTooltip) + .st({opacity: d => d.gross > 10000000 ? 1 : .1}) + .on('mouseover', week => { + var movie = byMovie.idLookup[week[0].id].filter(d => d.isTop) + + hoverCircleSel + .at({opacity: 0}) + .filter(i => movie[i]) + .at({opacity: 1}) + .translate(i => [ + c.x(movie[i].year + movie[i].week/52), + c.y(movie[i].percent) + ]) + + var dateStr = d3.timeFormat('%Y-%m-%d')(d3.timeParse('%Y %U')(week.year + ' ' + week.week)) + var dateStr = d3.timeFormat('%B %d, %Y')(d3.timeParse('%Y %U')(week.year + ' ' + week.week)) + var dateStr = d3.timeFormat('%b %d')(d3.timeParse('%Y %U')(week.year + ' ' + week.week)) + + window.ttSel.html(` +
+ ${week.top} + +
+ $${d3.format(',')(Math.round(week[0].gross))} + gross on the weekend of ${dateStr} —
+ ${d3.format('.0%')(week[0].percent)} of the domestic box office. + `) + }) + .at({strokeWidth: d => d.top.includes('No Way Home') ? 2 : ''}) + + var hoverCircleSel = c.svg.appendMany('circle', d3.range(60)) + .at({fill: '#f0f', stroke: '#f0f', fillOpacity: .4, opacity: 0, r: 4, pointerEvents: 'none'}) + + + c.svg.appendMany('text.annotation', byWeek.filter(d => d.annotation)) + .translate(d => [c.x(d.year + d.week/52), c.y(d[0].percent)]) + .text(d => d.annotation.str) + .at({ + textAnchor: d => ({l: 'end', r: 'start'}[d.annotation.align] || 'middle'), + dy: -5, + x: d => d.annotation.x || 0, + y: d => d.annotation.y || 0, + fontWeight: d => d.annotation.weight || '', + }) + .classed('m-hide', d => d.annotation.mHide) + +} + +if (window.init) window.init() diff --git a/source/box-office-hits/draw-year-distribution.js b/source/box-office-hits/draw-year-distribution.js new file mode 100644 index 00000000..2f5c6c97 --- /dev/null +++ b/source/box-office-hits/draw-year-distribution.js @@ -0,0 +1,159 @@ + +window.drawYearDistribution = function({byMovie}){ + var byReleaseYear = d3.nestBy(_.sortBy(_.sortBy(byMovie, d => -d.gross), d => d.year), d => d.year) + .filter(d => 1981 < d.key && d.key < 2022) + + byReleaseYear.forEach(year => { + year.gross = d3.sum(year, d => d.gross) + + var prev = 0 + year.forEach(d => { + d.prev = prev + d.yearPercent = d.gross/year.gross + prev += d.yearPercent + }) + }) + + var sel = d3.select('.year-distribution').html('') + + var c = d3.conventions({ + sel: sel.append('div'), + height: 500, + margin: {left: 25, bottom: 50, top: 10} + }) + + c.x.domain([1982, 2022]) + + c.xAxis.tickFormat(d => d)//.ticks(5) + c.yAxis.tickFormat(d3.format('.0%')).tickValues([.25, .5, .75]) + d3.drawAxis(c) + util.ggPlot(c, false) + + c.svg.selectAll('.axis path').at({strokeWidth: 1, stroke: '#ddd', strokeDasharray: '1 2'}) + c.svg.selectAll('.x.axis path').remove() + c.svg.selectAll('.x text').at({x: (c.x(1) - c.x(0))/2}) + + + var yearSel = c.svg + .append('g').lower() + .appendMany('g', byReleaseYear) + .translate(d => c.x(d.key), 0) + + var ramp = d3.interpolateMagma + var ramp = d3.interpolateViridis + + var domain = [1, 5, 15, 25, 50, 100] + var color = d3.scaleThreshold() + .domain(domain) + .range(d3.range(domain.length + 1).map(i => ramp(i/domain.length))) + .range(d3.range(domain.length + 1).reverse().map(i => ramp(i/domain.length))) + + yearSel.appendMany('rect', d => d.filter(d => d.yearPercent > .0001)) + .at({ + x: .5, + width: c.x(1) - c.x(0) - .2, + height: (d, i) => i > 100 ? .9 : Math.max(.1, c.height - c.y(d.yearPercent) - .1), + y: d => Math.round(c.y(d.prev + d.yearPercent)*10)/10, + fill: (d, i) => color(i), + // opacity: d => d.year == 2020 ? .3 : 1, + }) + .call(d3.attachTooltip) + .on('mouseover', d => { + window.ttSel.html(` +
+ ${d.name} + +
+ Total Gross: $${d3.format(',')(Math.round(d.gross))} — + + ${d3.format('.2%')(d.yearPercent)} of the total earned by movies released in ${d.year}. + `) + }) + + + + yearSel.filter(d => d.key == 2020) + .append('rect') + .at({ + width: c.x(1) - c.x(0), + height: c.height, + fill: '#fff', + fillOpacity: .6, + }) + .st({pointerEvents: 'none'}) + + + c.svg.select('.bg-rect').lower() + .at({fill: '#000', x: .3, width: c.width, height: c.height - .1}) + // .at({fill: color(1000)}) + + c.svg.append('text.annotation.m-hide') + .text('E.T. →') + .translate([c.x(1982) - 5, c.y(.07)]) + .at({textAnchor: 'end'}) + + c.svg.append('text.annotation.m-hide') + .text('← No Way Home') + .translate([c.x(2022) + 5, c.y(.07)]) + .at({textAnchor: 'start'}) + + + var ticks = [ + {c: 0, s: '#1 Grossing Movie'}, + {c: 2, s: '#2 - #5'}, + {c: 6, s: '#6 - #15'}, + {c: 16, s: '#16 - #25'}, + {c: 16, s: '#26 - #50'}, + {c: 16, s: '#51 - #100'}, + {c: 16, s: '100'}, + ] + // c.sel.append('div') + // .st({textAlign: 'center'}) + // .appendMany('span', ticks) + // .text(d => d.s) + // .st({ + // display: 'inline-block', + // width: 30 + // }) + + + var ticks = [ + {c: 0, s: '#1'}, + {c: 2, s: '#5'}, + {c: 6, s: '#15'}, + {c: 16, s: '#25'}, + {c: 26, s: '#50'}, + {c: 51, s: '#100'}, + {c: 101, s: ''}, + ] + + // legend + !(function(){ + var h = 10 + var w = 40 + + var svg = d3.select('.year-distribution-legend').html('') + .st({textAlign: 'center', fontFamily: 'sans-serif', fontSize: 12}) + .append('svg') + .at({ + height: 20, + width: w*ticks.length, + }) + + svg.append('text').text('Box Office Rank').at({fontWeight: 600, y: -3, textAnchor: 'middle', x: w*ticks.length/2}) + + var tickSel = svg.appendMany('g', ticks) + .translate((d, i) => i*w, 0) + + tickSel.append('rect') + .at({width: w - 1, height: h, fill: d => color(d.c)}) + + tickSel.append('text') + .text(d => d.s) + .at({y: h, x: w, dy: '1em', textAnchor: 'middle'}) + + })() + +} + +if (window.init) window.init() diff --git a/source/box-office-hits/init.js b/source/box-office-hits/init.js new file mode 100644 index 00000000..e49d8f72 --- /dev/null +++ b/source/box-office-hits/init.js @@ -0,0 +1,40 @@ +console.clear() +window.ttSel = d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + +// throw 'up' + + +window.init = async function(){ + var {weekendData, weeklyData} = util.parseData() + + drawWeeklyTopPercent(weekendData) + sleep(20) + + drawYearDistribution(weeklyData) + drawBestWeekScatter(weeklyData) + + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) + } +} + + +if (window.__datacache){ + window.init() +} else{ + d3.loadData( + 'https://roadtolarissa.com/data/box-office-mojo-weekend.csv', + 'https://roadtolarissa.com/data/box-office-mojo-weekly.csv', + 'bo_mojo_inflation.csv', + (err, res) => { + window.window.__datacache = res + window.init() + }) +} + + +// d3.selectAll('h3') +// .classed('full-width', 1) +// .st({minHeight: 0, width: '100%'}) + + diff --git a/source/box-office-hits/share.png b/source/box-office-hits/share.png new file mode 100644 index 00000000..d7cb9095 Binary files /dev/null and b/source/box-office-hits/share.png differ diff --git a/source/box-office-hits/style.css b/source/box-office-hits/style.css new file mode 100644 index 00000000..23e4a810 --- /dev/null +++ b/source/box-office-hits/style.css @@ -0,0 +1,142 @@ +body{ +} +html{ + background: #eee; + +} + +.tooltip { + top: -1000px; + position: fixed; + padding: 7px; + background: rgba(255, 255, 255, .94); + border: 1px solid lightgray; + pointer-events: none; + width: 300px; + font-size: 12px; + font-family: sans-serif; + line-height: 1.3em; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +.tooltip text{ + text-shadow: none; + fill: #000 !important; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.domain{ + display: none; +} + +text{ + /*pointer-events: none;*/ + text-shadow: 0 1px 0 #eee, 1px 0 0 #eee, 0 -1px 0 #eee, -1px 0 0 #eee; +} + + +.year{ + display: inline-block; +} + +.axis text{ + fill: #777; + font-size: 10px; +} +.x.axis text{ + font-size: 9px; +} + +.year-sm{ + width: 950px; +} + +.year-scatter .axis text{ + font-size: 10px; + fill: #555; +} + +.annotation{ + font-family: sans-serif; + font-size: 10px; + transition: opacity .3s; + pointer-events: none; +} +.annotation.hidden{ + opacity: 0; +} + +h3{ + max-width: 800px; +} + + + +.sliders{ + position: relative; + top: 10px; + padding-top: 5px; +} + +.slider-container{ + height: 35px; +} + + +blockquote{ + border-left: 1px solid #bbb; + margin: 1.5em 10px; + padding: 0em 1em; + margin: 0px; +} + +blockquote p{ + margin-bottom: 0px; +} + + +.full-width{ + width: min(980px, 100vw); + position: relative; + /*margin-left: min(-490px, -50vw);*/ + /*margin-right: min(-490px, -50vw);*/ + /*overflow: hidden;*/ + + left: 50%; + transform: translateX(-50%); + min-height: 550px; +} +.no-min-height{ + min-height: 0px; +} + + +#notes{ + font-family: sans-serif; + font-size: 12px; + opacity: .7; +} + +@media (max-width: 800px){ + .annotation.m-hide{ + display: none; + } +} \ No newline at end of file diff --git a/source/box-office-hits/util.js b/source/box-office-hits/util.js new file mode 100644 index 00000000..53a8b788 --- /dev/null +++ b/source/box-office-hits/util.js @@ -0,0 +1,121 @@ + +window.util = (function(){ + function parseData(){ + var cache = window.__datacache + + var year2inflation = {} + cache[2].forEach(d => year2inflation[+d.year] = +d.price_per_ticket) + + var weekendData = cache.weekendData = cache.weekendData || parsePeriod(window.__datacache[0]) + var weeklyData = cache.weeklyData = cache.weeklyData || parsePeriod(window.__datacache[1]) + + return {weekendData, weeklyData} + + function parsePeriod(tidy){ + tidy.forEach(d => { + d.yearweek = d.year + d.week + + d.rawGross = +d.gross + d.year = +d.year + d.week = +d.week + + d.gross = d.rawGross*year2inflation[2020]/year2inflation[d.year] + }) + + var yearweek2index = {} + d3.nestBy(tidy, d => d.yearweek).forEach((d, i) => yearweek2index[d.key] = i) + + tidy.forEach(d => { + d.weekIndex = yearweek2index[d.yearweek] + }) + + var byYear = d3.nestBy(tidy, d => d.year) + .filter(d => d.key > 1981) + + var byWeek = [] + byYear.forEach(year => { + year.byWeek = d3.nestBy(year, d => d.week) + .filter(d => d.length > 1) + + year.byWeek.forEach(week => { + week.gross = d3.sum(week, d => d.gross) + week.forEach(d => { + d.percent = d.gross/week.gross + }) + week.top = week[0].name + week[0].isTop = true + + byWeek.push(week) + }) + }) + + byWeek.forEach(d => { + d.week = d[0].week + d.year = d[0].year + // Wonky christmas weekend data + d.isHidden = (d.week == 51 || d.week == 52) && (d.year == 1988 || d.year ==1989) + }) + + var byMovie = d3.nestBy(tidy, d => d.id) + byMovie.idLookup = {} + byMovie.forEach(movie => { + movie.name = movie[0].name + movie.maxPercent = d3.max(movie, d => d.percent) + byMovie.idLookup[movie.key] = movie + + movie.weekIndex0 = movie[0].weekIndex + movie.year = movie[0].year + movie.gross = d3.sum(movie, d => d.gross) + movie.highestWeeklyGross = d3.sum(_.sortBy(movie, d => -d.gross).slice(0, 1), d => d.gross) + movie.highestGrossPercent = movie.highestWeeklyGross/movie.gross + + movie.forEach(d => { + d.weeksSinceRelease = d.weekIndex - movie.weekIndex0 + }) + + }) + + return {tidy, byWeek, byYear, byMovie} + } + } + + function addAxisLabel(c, xText, yText, xOffset=40, yOffset=-40){ + c.svg.select('.x').append('g') + .translate([c.width/2, xOffset]) + .append('text.axis-label') + .text(xText) + .at({textAnchor: 'middle'}) + .st({fill: '#000', fontSize: 14}) + + c.svg.select('.y') + .append('g') + .translate([yOffset, c.height/2]) + .append('text.axis-label') + .text(yText) + .at({textAnchor: 'middle', transform: 'rotate(-90)'}) + .st({fill: '#000', fontSize: 14}) + } + + function ggPlot(c, isBlack=true){ + // if (isBlack){ + c.svg.append('rect.bg-rect') + .at({width: c.width, height: c.height, fill: '#eee'}) + .lower() + // } + + c.svg.selectAll('.tick').selectAll('line').remove() + c.svg.selectAll('.y .tick') + .append('path').at({d: 'M 0 0 H ' + c.width, stroke: '#fff', strokeWidth: 1}) + c.svg.selectAll('.y text').at({x: -3}) + c.svg.selectAll('.x .tick') + .append('path').at({d: 'M 0 0 V -' + c.height, stroke: '#fff', strokeWidth: 1}) + + // c.svg.selectAll('.y .tick').filter(d => d == 0).remove() + } + + + return {addAxisLabel, ggPlot, parseData} +})() + + +if (window.init) window.init() \ No newline at end of file diff --git a/source/central-park-rain/init.js b/source/central-park-rain/init.js new file mode 100644 index 00000000..5bbc173f --- /dev/null +++ b/source/central-park-rain/init.js @@ -0,0 +1,236 @@ +window.visState = window.visState || { + streak: .01, + rolling: 1, +} + +window.init = function(){ + console.clear() + + daily.forEach(d => { + var [year, month, day] = d.DATE.replaceAll('/', '-').split('-') + d.year = year + d.month = month + d.day = day + d.PRCP = +d.PRCP + }) + + initDailyRain('streak') + initDailyRain('rolling') + + initByMonth() +} + +function initDailyRain(type){ + var isStreak = type == 'streak' + var renderFns = [val => visState[type] = val] + function render(val){ renderFns.forEach(fn => fn(val)) } + + function drawSlider(){ + var sel = d3.select('.' + type + '-slider').html('') + var textSel = sel.append('div.streak-val') + + var scale = d3.scalePow().range(isStreak ? [.01, 50] : [1, 3652]).exponent(2.5) + if (!isStreak) scale.interpolate(d3.interpolateRound) + var sliderSel = sel.append('input') + .at({type: 'range', min: 0, max: 1, value: 0, step: .001}) + .on('input', function(){ render(scale(this.value)) }) + + renderFns.push(val => textSel.text(isStreak ? d3.format('.2f')(val) + '″' : val + ' day' + (val > 1 ? 's' : ''))) + } + drawSlider() + + var streakDaily = daily.filter(d => d.year > '1879') + var byYear = d3.nestBy(streakDaily, d => d.year) + byYear.forEach((year, yearIndex) => { + year.yearIndex = yearIndex + year.forEach((d, dayIndex) =>{ + d.yearIndex = yearIndex + d.dayIndex = dayIndex + }) + }) + + var dw = 3 + var yh = 4 + var c = d3.conventions({ + sel: d3.select('.' + type).html(''), + width: (366 + 1)*dw, + height: (d3.max(daily, d => d.yearIndex) + 1)*yh, + layers: 'cs', + margin: {top: 0}, + }) + util.setFullWidth(c.sel, c.totalWidth) + + c.svg.append('g.axis').appendMany('text', util.shortMonths) + .text(d => d) + .translate((d, i) => [dw*(i*30 + 15), c.height + 12]) + .at({textAnchor: 'middle'}) + + c.svg.append('g.axis').appendMany('g', byYear.filter(d => d.key[3] == '0')) + .translate(d => [-14, yh*(d.yearIndex)]) + .append('text') + .text(d => d.key) + .at({textAnchor: 'middle', dy: '.33em'}) + + c.svg.append('rect').at({width: c.width, height: c.height, opacity: 0}) + .call(d3.attachTooltip) + .on('mousemove mouseover', function(){ + var ttSel = d3.select('.tooltip')//.html('') + + var [mx, my] = d3.mouse(this) + + var year = byYear[Math.floor(Math.max(0, my)/yh)] + var d = year && year[Math.floor(mx/dw)] + if (!d) return ttSel.classed('.tooltip-hidden', 1) + + ttSel.html(` +
${d.DATE}
+ +
${d3.format('.2f')(d.PRCP) + '″'} of precipitation
+ +
${d.streak} prior days ${visState.streak > .01 ? + `to get to ${d3.format('.2f')(visState.streak) + '″'} of precipitation` : + `of no precipitation` + }
+ +
${visState.rolling > 1 ? + `Over the last ${visState.rolling} days, ${d3.format('.2f')(d.rolling) + '″'} of precipitation` : + `` + }
+ `) + + + + }) + + renderFns.push(val => { + if (isStreak){ + var sum = 0 + var start = 0 + for (var i = 0; i < daily.length; i++) { + sum += daily[i].PRCP + daily[i][type] = i - start + 1 + + while (sum >= val && start <= i) { + sum -= daily[start].PRCP + start++ + daily[i][type] = i - start + 1 + } + } + } else { + var sum = 0 + for (var i = 0; i < daily.length; i++) { + sum += daily[i].PRCP + if (i >= val) sum -= daily[i - val].PRCP + daily[i][type] = sum < .001 ? 0 : sum + } + } + + var [minVal, maxVal] = d3.extent(streakDaily, d => d[type]) + if (isStreak){ + var color = d3.scaleSequential(d3.interpolateTurbo).domain([0, maxVal]) + } else { + // var byVal = streakDaily.map(d => +d[type]).sort((a, b) => a - b) + // var maxVal = d3.quantile(byVal, 0.999) + + var colorRaw = d3.scaleSequential(d3.interpolateCool).domain([minVal, maxVal]) + + // var colorRaw = d3.scaleQuantile() + // .domain(streakDaily.map(d => d[type])) + // .range(d3.schemePuOr[11].slice().reverse()) + // var color = d3.scaleSequential(d3.interpolateCool).domain([maxVal, minVal]) + // var color = d3.scaleSequential(d3.interpolateTurbo).domain([maxVal, minVal]) + // var color = d3.scaleSequential(d3.interpolateOranges).domain([maxVal, 0]) + + var color = d => d < .005 ? '#000' : colorRaw(d) + + } + + var ctx = c.layers[0] + ctx.clearRect(0, 0, c.width, c.height) + streakDaily.forEach(d =>{ + ctx.beginPath() + ctx.rect(dw*d.dayIndex, yh*d.yearIndex, dw, yh) + ctx.fillStyle = color(d[type]) + ctx.fill() + }) + }) + + render(isStreak ? .01 : 1) +} + + +function initByMonth(){ + var byMonth = d3.nestBy(daily, d => d.month) + byMonth.forEach(month => { + month.byYear = d3.nestBy(month, d => d.year) + month.byYear.forEach(arr => arr.total_prcp = d3.sum(arr, d => +d.PRCP)) + }) + + var byMonthSel = d3.select('.by-month').html('') + util.setFullWidth(byMonthSel, 1200) + + byMonthSel + .appendMany('div.month', byMonth) + .each(drawMonth) + .st({display: 'inline-block'}) + + var monthYearSel = byMonthSel.selectAll('.month-year') + .on('mouseover', d => setActiveYear(d[0].year)) + + function setActiveYear(year){ + monthYearSel.classed('active', d => d[0].year == year) + } + setActiveYear(2024) + + function drawMonth(month){ + var c = d3.conventions({ + sel: d3.select(this), + height: 100, + margin: {left: 35, right: 35, top: 10} + }) + + var max = 10 + var nBuckets = 30 + var bucketSize = max/nBuckets + + c.x.domain([0, max]).clamp(1).interpolate(d3.interpolateRound) + c.y.domain([0, 14]) + c.xAxis.tickFormat(d => d + '″' + (d == max ? '+' : '')) + d3.drawAxis(c) + c.svg.select('.y').remove() + c.x.interpolate(d3.interpolateNumber) + + c.svg.append('g.axis').append('text').text(util.months[month.key - 1] + ' Total Precipitation') + .at({textAnchor: 'middle', x: c.width/2, y: c.height + 33}) + .st({fontSize: 12}) + + // filter out 1924 data and incomplete 2024 data + var byYear = month.byYear.filter(d => d.key > '1924' && (d.key < '2024' || d[0].month < '11')) + byYear.forEach(d => d.xPos = c.x(Math.round(d.total_prcp/bucketSize)*bucketSize)) + d3.nestBy(byYear, d => d.xPos).forEach(bucket => { + bucket.forEach((byYear, bucketIndex) => byYear.bucketIndex = bucketIndex) + }) + // console.log(d3.max(byYear, d => d.bucketIndex)) + + c.svg.appendMany('text.month-year', byYear) + .text(d => d.key.slice(2)) + .translate(d => [d.xPos, c.y(d.bucketIndex)]) + .at({dy: -1, textAnchor: 'middle'}) + .call(d3.attachTooltip) + .on('mousemove', d => { + d3.select('.tooltip') + .html(`${d3.round(d.total_prcp, 2)}″ of precipitation in ${util.months[d[0].month - 1]} ${d[0].year}`) + }) + } +} + + +if (!window.daily){ + d3.loadData('https://roadtolarissa.com/data/central-park-daily-weather.csv', (err, res) => { + window.daily = res[0] + init() + }) +} else{ + init() +} + diff --git a/source/central-park-rain/style.css b/source/central-park-rain/style.css new file mode 100644 index 00000000..b79420d9 --- /dev/null +++ b/source/central-park-rain/style.css @@ -0,0 +1,102 @@ +body{ + height: 3000px; +} +.tooltip { + top: -1000px; + position: fixed; + padding: 7px; + background: rgba(255, 255, 255, .8); + border: 1px solid lightgray; + pointer-events: none; +/* width: 300px;*/ + font-size: 12px; + font-family: sans-serif; + line-height: 1.3em; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +.tooltip text{ + text-shadow: none; + fill: #000 !important; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.axis{ + stroke-width: .5; +} +.domain{ + stroke: #777; +/* display: none;*/ +} + +text{ + /*pointer-events: none;*/ +/* text-shadow: 0 1px 0 #eee, 1px 0 0 #eee, 0 -1px 0 #eee, -1px 0 0 #eee;*/ +} + +.axis text{ + fill: #777; + font: 10px sans-serif; +} + +#notes{ + font-family: sans-serif; + font-size: 12px; + opacity: .7; +} + + +.by-month{ + .month{ + width: calc(33%); + } +} + +text.month-year{ + font-family: monospace; + font-size: 6px; + font-weight: 200; + cursor: default; +} + +text.month-year.active{ + font-weight: 900; + fill: #f0f; +} + + + +.streak-slider, .rolling-slider{ + margin: 0px auto; + width: 150px; + margin-top: 20px; + margin-bottom: 20px; +} +input[type='range']{ + accent-color: #000; + width: 150px; +} +.streak-val, .rolling-val{ + font-variant-numeric: tabular-nums; + text-align: center; + font-family: sans-serif; +} + diff --git a/source/central-park-rain/util.js b/source/central-park-rain/util.js new file mode 100644 index 00000000..f29f75e4 --- /dev/null +++ b/source/central-park-rain/util.js @@ -0,0 +1,42 @@ +window.util = (function(){ + function setFullWidth(sel, width){ + width = width || innerWidth - 20 + var pWidth = d3.select('p').node().offsetWidth + var marginLeft = -(width - pWidth)/2 + sel.st({width, marginLeft}) + } + + function addAxisLabel(c, xText, yText, xOffset=30, yOffset=-30){ + c.svg.select('.x').append('g') + .translate([c.width/2, xOffset]) + .append('text.axis-label') + .text(xText) + .at({textAnchor: 'middle'}) + + c.svg.select('.y') + .append('g') + .translate([yOffset, c.height/2]) + .append('text.axis-label') + .text(yText) + .at({textAnchor: 'middle', transform: 'rotate(-90)'}) + } + + function ggPlot(c, isBlack=true){ + c.svg.append('rect.bg-rect') + // .st({height: c.height, width: c.width, fill: '#eee'}).lower() + + c.svg.selectAll('.tick').selectAll('line').remove() + c.svg.selectAll('.y .tick') + .append('path').at({d: 'M 0 0 H ' + c.width, stroke: '#999', strokeWidth: .5}) + c.svg.selectAll('.y text').at({x: -3}) + c.svg.selectAll('.x .tick') + .append('path').at({d: 'M 0 0 V -' + c.height, stroke: '#999', strokeWidth: .5}) + } + + var shortMonths = ["Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."] + var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + + return {addAxisLabel, ggPlot, setFullWidth, shortMonths, months} +})() + +window.init?.() \ No newline at end of file diff --git a/source/chart-diary/script.js b/source/chart-diary/script.js new file mode 100644 index 00000000..eb52d896 --- /dev/null +++ b/source/chart-diary/script.js @@ -0,0 +1,80 @@ +var zIndex = 10 +var imgs = [] + +d3.selectAll('a').each(function(){ + var sel = d3.select(this) + + var [src, href] = sel.attr('href').split('---') + if (href && href.includes('imgur')) [href, src] = sel.attr('href').split('---') + if (!src.includes('imgur')) return + + sel.attr('href', href || '') + if (href) sel.classed('actual-link', true) + + if (!src.includes('.png') && !src.includes('.gif')) src = src + '.png' + + imgs.push(src) + sel + .classed('mouse-over-img', true) + .on('mouseover click touchstart', () => { + sel.st({zIndex: zIndex++}) + + var bb = imgSel.node().getBoundingClientRect() + console.log(bb, bb.left) + if (innerWidth < 800){ + var width = Math.min(350, innerWidth - 10) + + console.log(width, bb.width + 2) + if (width == bb.width - 2){ + imgSel.st({width: '', left: '', maxHeight: ''}) + d3.selectAll('p').classed('active', false) + } else{ + imgSel.st({ + maxWidth: width + 'px', + left: 5 - bb.left, + maxHeight: '400px', + }) + sel.parent().classed('active', true) + } + } + + if (href) return + + d3.event.preventDefault() + d3.event.stopPropagation() + }) + .on('mouseout', () => { + // imgSel.st({width: '', left: '', maxHeight: ''}) + }) + .append('div').append('div').append('img').at({src}) + + var imgSel = sel.select('img') + +}) + +d3.select('body').on('touchstart', () => { + d3.select('body').selectAll('img').st({maxWidth: '', left: '', maxHeight: ''}) + d3.selectAll('p').classed('active', false) +}) + // .st({maxWidth: Math.min(innerWidth - 10, 750) + 'px'}) + +setTimeout(function(){ + d3.selectAll('.mouse-over-img img').each(function(){ + var bb = this.getBoundingClientRect() + + var classStr = bb.left < 120 ? 'left' : innerWidth - bb.left < 120 ? 'right' : '' + d3.select(this).attr('class', classStr) + }) +}, 0) + + +// d3.select('body').html('') +// d3.select('html') +// .append('div').st({zIndex: 100}) +// .appendMany('img', _.shuffle(imgs)) +// .at({src: d => d, width: 200}) +// .st({ +// position: 'absolute', +// left: (d, i) => (i % 9)*200, +// top: (d, i) => Math.floor(i/9)*100 +// }) diff --git a/source/chart-diary/style.css b/source/chart-diary/style.css new file mode 100644 index 00000000..a4aa54cc --- /dev/null +++ b/source/chart-diary/style.css @@ -0,0 +1,103 @@ +html{ + overflow-x: hidden; +} + +.mouse-over-img{ + position: relative; + transition: all .5s; + background: #000; + color: #f5f5f5; + padding: 1px; + padding-left: 2px; + text-decoration: none; + z-index: 0; + cursor: default; +} + +.mouse-over-img.actual-link{ + text-decoration: underline; + cursor: pointer; +} + + + +.mouse-over-img div{ + display: inline-block; + line-height: 25px; + position: relative; + overflow: visible; + width: 25px; + /* border: 3px solid black; */ +} + +.mouse-over-img div div{ + position: absolute; + width: auto; + height: auto; +} + +.mouse-over-img img{ + max-width: 19px; + max-height: 14px; + + position: absolute; + bottom: -1px; + left: 4px; + + transition: all .25s; + border: 1px solid black; +} + +.mouse-over-img:hover{ +} +p:hover{ + position: relative; + z-index: 10; +} +p{ + position: relative; + z-index: 1; +} +p.active{ + z-index: 12; +} + +.mouse-over-img div div{ + pointer-events: none; +} +.mouse-over-img img{ + pointer-events: all; +} + +.mouse-over-img:hover img{ + max-width: 350px; + max-height: 400px; + opacity: 1; +} + +.mouse-over-img:hover img{ + /*transform: translate(-150px, -80px);*/ + left: -150px; + bottom: -80px; +} + +.mouse-over-img:hover img.left{ + /*transform: translate(0px, -80px);*/ + left: 0px; +} +.mouse-over-img:hover img.right{ + left: -300px; + /*transform: translate(-300px, -80px);*/ +} + +/*@media only screen and (max-width: 768px){ + .mouse-over-img div div{ + position: inline-block; + } + + .mouse-over-img img{ + position: inline; + width: 200px; + height: 200px; + } +}*/ diff --git a/source/dvs-privacy/_script.js b/source/dvs-privacy/_script.js new file mode 100644 index 00000000..81baca5e --- /dev/null +++ b/source/dvs-privacy/_script.js @@ -0,0 +1,634 @@ +console.clear() +var ttSel = d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + + +var colors = [ + '#DDB32B', + '#2DB2A5', + '#A05E9C' +] + +var slides = [ +`

Over 3,000 people have joined the Data Visualization Society. The glyphs here represent their self reported skill levels at different aspects of charting. + +

The data released by DVS lists city and submission time, but doesn't include the raw responses to the nine skill questions on the survey. To protect respondents' privacy, the questions were combined into three categories—data, visualization and society—and averaged together. `, + +`

Even with that dimensionality reduction, it's still hard to pick out patterns in the data. + +

We can simplify more by reducing the granularity of the data. Instead of trying to show every skill at its exact level, we can bucket them as low, medium or high. + +

We can see more already! Medium in all three categories is the modal choice. +`, + +` +

Grouping glyphs with the same bucketed skills removes noise from the chart, making it easier to find little insights. + +

It looks like high society skills are less common than data or visualization skills, for example. +`, + +`

This method of grouping is flexible. I've added back some granularity, using six buckets for each skill to match the original 0-5 scale. All 0s are way more common than straight 5s. + +

In the biggest groups all the skills are within a point of each other. More variance isn't as common; I think there's a high data / visualization and low society group with just me in it! + +

In total, there are 28 groups here with just one person in them. +`, + +`

Adding back even more granularity—remember each skill slice represents the average of three questions, so fractional values are possible—there are about 600 people who have a unique combination of averaged skills. +`, + +`

In addition to the redacted survey data, DVS also created PNG and SVG badges visualizing every member's response to the nine questions. Here's a bit of one: + +

<path d="M49,0L93,74L7,76Z"/> +<path d="M50,36L62,56L38,57Z"/> +<path d="M55,18L78,67L98,75Z"/> + + +

By extracting the nine raw responses from the SVGs, I was able to link them to 600 people’s submission times and locations by calculating the skill category averages. + +

This linked data was intentionally not released. + +`, + +`

Significantly more detailed information about the visualization community has been published before, so the impact of this inadvertent leakage of survey data is quite low. But it does point to the difficulties of releasing data on the internet. + +

I'm conflicted about this. Two of my favorite NYT pieces required detailed, administrative data about sensitive topics. How can we use data to understand the world if the most important and interesting data is impossible to share? + +

One common solution, k-anonymity, selectively reduces granularity to guarantee that there'll always be several people in any given grouping. This gets tricky with higher dimensional data. + +

The state-of-the-art differential privacy uses random noise and cryptographic math to construct summary statistics that don’t reveal any single individual's response. I'm not aware of an easy way to use it though. + +

In this instance, the much maligned "security through obscurity" would have been sufficient. If the badges were only released as PNGs I definitely wouldn't have taken the time to parse them. + +

chart code

+` + +] + + +var slideSel = d3.select('#slides').html('') + .appendMany('div.slide', slides) + .append('div').html(d => d) + +gs = d3.graphScroll() + .eventId('lol-scroll') + .container(d3.select('#container')) + .sections(slideSel) + .offset(innerHeight - 100) + + +var c = d3.conventions({ + sel: d3.select('html').selectAppend('div#graph').html(''), + layers: 'ds', + margin: {left: innerWidth < 500 ? 10 : 20, top: 10, right: 10, bottom: 10} +}) + + +if (window.data){ + init() +} else { + // d3.loadData('https://roadtolarissa.com/data/dvs/membership.json', (err, res) => { + // data = res[0] + // init() + // }) + d3.loadData('membership.json', (err, res) => { + data = res[0] + init() + }) +} + +function init(){ + data = data//.slice(0, 100) + + data.forEach((d, i) => { + d.groups = [ + {d, type: 'data', val: d.data/5}, + {d, type: 'visualization', val: d.visualization/5}, + {d, type: 'society', val: d.society/5}, + ] + + d.groups.forEach(d => { + d.val2 = Math.round(d.val*2)/2 + d.val6 = Math.round(d.val*5)/5 + d.val9 = d.val + }) + + d.val2Key = d.groups.map(d => d.val2).join(' ') + d.val6Key = d.groups.map(d => d.val6).join(' ') + d.val9Key = d.groups.map(d => d.val9).join(' ') + }) + + data = _.sortBy(data, d => d3.sum(d.groups, d => d.val2)) + data = _.sortBy(data, d => d.val2Key) + byVal2 = d3.nestBy(data, d => d.val2Key) + byVal6 = d3.nestBy(data, d => d.val6Key) + byVal9 = d3.nestBy(data, d => d.val9Key) + .filter(d => d.length == 1 && d[0].match) + .map(d => d[0]) + + var s = Math.sqrt(c.width*c.height/data.length) + var nCols = Math.ceil(c.width/s) + var r = s/2 - 1*0 + data.forEach((d, i) => { + d.flatPos = [s*(i % nCols), s*(Math.floor(i/nCols))] + d.i = i + }) + + var s9 = Math.sqrt(c.width*c.height/byVal9.length) + var nCols = Math.floor(c.width/s9) + var r9 = s9/2 - 1*0 + byVal9.forEach((d, i) => { + d.pos9 = [s9*(i % nCols + .3), s9*(.5 + Math.floor(i/nCols))] + d.i9 = i + }) + _.shuffle(byVal9).forEach((d, i) => { + d.ir = i + }) + + + var rScale = d3.scaleSqrt().range([1, r]) + var rScale = d3.scaleLinear().range([0, r]) + var rScale9 = d3.scaleLinear().range([0, r9]) + + var arc = d3.arc() + .outerRadius(d => rScale(d.data.val)) + .innerRadius(0) + + var pie = d3.pie() + .sort(null) + .value(d => 1) + + layer0Sel = c.svg.append('g.layer0') + layer2Sel = c.svg.append('g.layer2') + layer6Sel = c.svg.append('g.layer6') + layer9Sel = c.svg.append('g.layer9') + + + // first two slides + glphySel0 = layer0Sel.appendMany('g', data) + .translate(d => d.flatPos) + .call(d3.attachTooltip) + + glphySel0.append('circle') + .at({ + r, + stroke: '#ddd', + fill: 'none', + strokeWidth: .5, + }) + + var sliceSel0 = glphySel0.appendMany('path', d => pie(d.groups)) + .at({ + d: arc, + fill: (d, i) => colors[i] + }) + + + + + function runSim(array, key, numTicks){ + array.forEach(combined => { + combined.forEach(d => { + d[key] = combined + }) + + combined.vals = combined.key.split(' ').map(d => d) + combined.groups = combined.vals.map(d => d*combined.length) + combined.r = rScale1(combined.length) + + combined.mean = d3.mean(combined.vals) + + combined.count = combined.length + }) + + array = _.sortBy(array, d => d.length).reverse() + + var total = 0 + array.slice().reverse().forEach((d, i) => { + total += d.length + d.total = total + }) + + c.y.domain([1, 0]) + c.x.domain(d3.extent(array, d => d.length)) + c.x.domain([0, data.length]) + + // var sim1 = d3.forceSimulation(byVal2) + // .force('x', d3.forceX(c.width / 2).strength(.1)) + // .force('x', d3.forceX(d => c.x(d.variance)).strength(.1)) + // .force('x', d3.forceX(d => c.x(d.length)).strength(.1)) + // .force('y', d3.forceY(c.height / 2).strength(1)) + // .force('collide', d3.forceCollide(d => d.r + 2).strength(1.5)) + // .stop() + + if (key == 'combined9'){ + array.forEach(d => { + if (d.length > 1) return + + d.x = Math.random()*c.width + d.y = Math.random()*c.height + }) + array = array.filter(d => d.length > 1) + } + + var xForce = key == 'combined2' ? + d3.forceX(c.width / 2).strength(.05) : + d3.forceX(d => c.x(d.total)).strength(.3) + + var yForce = d3.forceY(d => c.y(d.mean)).strength(.5) + + if (key == '2to6-transition'){ + xForce = d3.forceX(d => d[0].combined2.x).strength(1) + yForce = d3.forceY(d => d[0].combined2.y).strength(1) + } + + var sim = d3.forceSimulation(array) + .force('x', xForce) + .force('y', yForce) + .force('collide', d3.forceCollide(d => d.r + 2).strength(1.5)) + .force('container', forceContainer([[0, 0],[c.width, c.height]]).strength(1.5)) + .stop() + + for (var i = 0; i < numTicks; ++i){ + sim.tick() + array.forEach(d => { + if (i % 10 != 9) return + d.x = d3.clamp(d.r*.5, d.x, c.width - d.r*.5) + d.y = d3.clamp(d.r*.5, d.y, c.height - d.r*.5) + }) + } + + } + + + + + rScale1 = d3.scaleSqrt() + .domain([0, 100]) + .range([1, r*8]) + + function randCirclePos(){ + var r0 = 1 + var r1 = 1 + + while (r0*r0 + r1*r1 > 1){ + r0 = Math.random()*2 - 1 + r1 = Math.random()*2 - 1 + } + + return [r0, r1] + } + + function randCircleBorder(){ + var θ = Math.random()*Math.PI*2 + + return [Math.cos(θ), Math.sin(θ)] + } + + function phyllotaxis(i) { + var radius = r*Math.sqrt(i)*.6 + var θ = Math.PI*(3 - Math.sqrt(5))*i + return [radius*Math.cos(θ), radius*Math.sin(θ)] + } + + // third slide + runSim(byVal2, 'combined2', 200) + + // calc 0 -> 2 transition + // data.forEach(d => { + // var c = d.combined2 + + // var [r0, r1] = randCirclePos() + // d.pos2 = [r0*(c.r - r) + c.x, r1*(c.r - r) + c.y] + // }) + + byVal2.forEach(c => { + c.forEach((d, i) => { + var [x, y] = phyllotaxis(i) + d.pos2 = [x + c.x, y + c.y] + }) + }) + + _.sortBy(byVal2, d => d[0].i).forEach((d, i) => { + d.animationIndex = i + }) + + + + + glphySel2 = layer2Sel.appendMany('g', byVal2) + .translate(d => [d.x, d.y]) + .call(d3.attachTooltip) + + glphySel2.append('circle.combined') + .at({r: d => d.r}) + // .st({stroke: d => d.count == 1 && d[0].match ? 'red' : ''}) + + glphySel2.appendMany('path', d => pie(d.groups)) + .at({ + d: arc.outerRadius(d => rScale1(d.data)), + fill: (d, i) => colors[i] + }) + + + // fourth slide + runSim(byVal6, 'combined6', 300) + + byVal6.forEach(d => d.pos = [d.x, d.y]) + _.sortBy(byVal6, d => d.x).forEach((d, i) => d.animationIndex = i) + + // calc 2 -> 6 transition + d3.nestBy(byVal6, d => d[0].combined2).forEach(c2 =>{ + var sorted = _.sortBy(c2, d => -d.length) + sorted[0].is2to6Max = true + + var c = c2[0][0].combined2 + sorted.forEach((d, i) => { + var θ = Math.PI*2*i/(c2.length - 1) + + var [r0, r1] = [Math.cos(θ), Math.sin(θ)] + d.pos2 = [r0*.8*(c.r - d.r) + c.x, r1*.8*(c.r - d.r) + c.y] + + if (d.is2to6Max) d.pos2 = [c.x, c.y] + }) + }) + + + glphySel6 = layer6Sel.appendMany('g', byVal6) + .translate(d => d.pos2) + .call(d3.attachTooltip) + + glphySel6.append('circle.combined') + .at({r: d => d.r}) + .st({stroke: d => d.count == 1 && d[0].match ? 'red' : ''}) + .filter(d => d.count == 1) + .st({stroke: '#000', strokeWidth: 1.5}) + + glphySel6.appendMany('path', d => pie(d.groups)) + .at({ + d: arc.outerRadius(d => rScale1(d.data)), + fill: (d, i) => colors[i] + }) + + + + // calc 6 -> 9 transition + d3.nestBy(byVal9, d => d.val6Key).forEach(c6 =>{ + var sorted = _.sortBy(c6, d => -d.length) + sorted[0].is6to9Max = true + + var c = c6[0].combined6 + sorted.forEach((d, i) => { + var θ = Math.PI*2*i/(c6.length - 1) + + var [r0, r1] = [Math.cos(θ), Math.sin(θ)] + d.pos6to9 = [r0*.8*(c.r - r) + c.x, r1*.8*(c.r - r) + c.y] + // throw 'up' + if (d.is6to9Max) d.pos6to9 = [c.x, c.y] + }) + }) + + + // last two slides + glphySel9 = layer9Sel.appendMany('g', byVal9) + .translate(d => d.pos6to9) + .call(d3.attachTooltip) + + var circleSel9 = glphySel9.append('circle.combined') + .at({r}) + .st({strokeWidth: .5}) + + byVal9.forEach(d => { + d.groups9 = [] + + d.match.triangles.forEach(tri => { + var vals = tri.vals.map(d => d/5) + var mean = d3.mean(vals) + + vals.forEach(val => { + d.groups9.push({d, val, mean}) + }) + }) + }) + + var sliceSel9 = glphySel9.appendMany('path', d => pie(d.groups9)) + .at({ + d: arc.outerRadius(d => rScale(d.data.mean)), + fill: (d, i) => colors[Math.floor(i/3)], + stroke: '#fff', + strokeWidth: .1 + }) + + var locTimeSel = layer9Sel.append('g') + + + + + + + + + + var prevI = 4 + onScroll(0) + // onScroll(5) + + function onScroll(i){ + var isNormal = i - prevI == 1 + + console.log({i, prevI, isNormal}) + + if (i == 0){ + + sliceSel0 + .transition().duration(0) + .at({d: arc.outerRadius(d => rScale(d.data.val))}) + } + if (i == 1){ + var l = byVal2.length + sliceSel0 + .transition() + .duration(5000/l) + .delay(d => d.data.d.combined2.animationIndex/l*5000) + .at({d: arc.outerRadius(d => rScale(d.data.val2))}) + + // sliceSel0 + // .at({d: arc.outerRadius(d => rScale(d.data.val2))}) + } + + if (isNormal){ + if (i == 2){ + // var l = data.length + // glphySel0 + // .transition().duration(0).delay((d, i) => i/l*5000) + // .translate(d => d.pos2) + var l = byVal2.length + glphySel0 + .transition() + .duration(5000/l) + .delay(d => d.combined2.animationIndex/l*5000) + .translate(d => d.pos2) + + layer0Sel.transition().duration(1000).delay(5000) + .st({opacity: 0}) + + layer2Sel.transition().duration(1000).delay(5000) + .st({opacity: 1}) + } + + if (i == 3){ + layer2Sel.transition().duration(500) + .st({opacity: 0}) + + layer6Sel.transition().duration(500) + .st({opacity: 1}) + + var l = byVal2.length + glphySel6 + .transition() + .duration(500) + .delay(d => d.animationIndex/l*200 + 1000) + .translate(d => d.pos) + } + + if (i == 4){ + layer6Sel.transition().duration(500) + .st({opacity: 0}) + + layer9Sel.transition().duration(500) + .st({opacity: 1}) + + var l = byVal6.length + glphySel9 + .transition() + .duration(500) + .delay((d, i) => i/l*500 + 1000) + .translate(d => d.pos9) + } + if (i == 5){ + sliceSel9.transition().duration(150).delay(d => d.data.d.ir*300) + .at({d: arc.outerRadius(d => rScale9(d.data.mean))}) + .transition().delay(200).duration(700) + .at({d: arc.outerRadius(d => rScale9(d.data.val))}) + + circleSel9.transition().duration(150).delay(d => d.ir*300) + .at({r: r9}) + .on('start', d => { + var isShowing = d.ir % 3 == 0 || true + var sel = locTimeSel.append('g.loctime') + .translate([ + d.pos9[0] - r9, + d.pos9[1] + (isShowing ? r9 + 8 : -r9 - 14) + ]) + .st({fontSize: 10, fontFamily: 'monospace'}) + + sel.transition().duration(isShowing ? 3000 : 0) + .st({opacity: 0}) + .remove() + + + sel.append('text').at({y: 12}) + // .at({textAnchor: 'middle'}) + // .text([d.lat, d.long].map(Math.round).map(d3.format('02')).join(', ')) + .text([d.lat, d.long].map(d3.format('04.1f'))) + // .text([d.lat, d.long].map(d => d3.format('05.1f')(d))) + // .join(', ')) + + sel.append('text') + // .at({textAnchor: 'middle'}) + .text(d.date_with_hour.replace('/2019', '') + (+d.hour < 12 ? 'AM' : 'PM')) + .text(d.date_with_hour.replace('/2019', '') + ':00') + + }) + + } + + } else { + glphySel0.transition().duration(0) + .translate(d => d.flatPos) + glphySel6.transition().duration(0) + .translate(d => i < 3 ? d.pos2 : d.pos) + glphySel9.transition().duration(0) + .translate(d => i < 4 ? d.pos6to9 : d.pos9) + + layer0Sel.transition().duration(0) + .st({opacity: i < 2 ? 1 : 0}) + layer2Sel.transition().duration(0) + .st({opacity: i == 2 ? 1 : 0}) + layer6Sel.transition().duration(0) + .st({opacity: i == 3 ? 1 : 0}) + layer9Sel.transition().duration(0) + .st({opacity: i > 3 ? 1 : 0}) + + if (i < 5){ + sliceSel9.transition().duration(0) + .at({d: arc.outerRadius(d => rScale(d.data.mean))}) + + circleSel9.transition().duration(0) + .at({r}) + } + } + + layer0Sel.st({pointerEvents: i < 2 ? 'all' : 'none'}) + layer2Sel.st({pointerEvents: i == 2 ? 'all' : 'none'}) + layer6Sel.st({pointerEvents: i == 3 ? 'all' : 'none'}) + layer9Sel.st({pointerEvents: i == 4 ? 'all' : 'none'}) + + prevI = i + } + + gs.on('active', onScroll) + +} + + + + + + + + + +function forceContainer (bbox){ + var nodes, strength = 1;; + + if (!bbox || bbox.length < 2) bbox = [[0, 0], [100, 100]] + + + function force(alpha) { + var i, + n = nodes.length, + node, + x = 0, + y = 0; + + for (i = 0; i < n; ++i) { + node = nodes[i], x = node.x, y = node.y, r = node.r; + + if (x - r < bbox[0][0]) node.vx += (bbox[0][0] - x + r)*alpha*strength + if (y - r < bbox[0][1]) node.vy += (bbox[0][1] - y + r)*alpha*strength + if (x + r > bbox[1][0]) node.vx += (bbox[1][0] - x - r)*alpha*strength + if (y + r > bbox[1][1]) node.vy += (bbox[1][1] - y - r)*alpha*strength + } + } + + force.initialize = function(_){ + nodes = _; + }; + + force.bbox = function(_){ + return arguments.length ? (bbox = +_, force) : bbox; + }; + force.strength = function(_){ + return arguments.length ? (strength = +_, force) : strength; + } + + return force; +} + + + + + + diff --git a/source/dvs-privacy/style.css b/source/dvs-privacy/style.css new file mode 100644 index 00000000..3294fb37 --- /dev/null +++ b/source/dvs-privacy/style.css @@ -0,0 +1,131 @@ +html{ +} + +body{ + margin: 0px auto; + /*pointer-events: none;*/ +} + +#container{ + margin: 0px auto; + margin-top: 30px; + position: relative; + min-height: 4000px; + /*pointer-events: none;*/ +} + +.slide{ + margin-bottom: 75vh; + background: rgba(255,255,255, .95); + padding: 15px; + padding-top: 1px; + padding-bottom: 1px; + max-width: 700px; + /*opacity: 0;*/ +} + +.slide:last-child{ + margin-bottom: 30vh; +} + +.text-d { + background: #DDB32B; +} +.text-v { + background: #2DB2A5; +} +.text-s { + background: #A05E9C; +} +.text-d, .text-v, .text-s{ + padding-left: 2px; + padding-right: 2px; + color: #fff; + border-radius: 2px; + font-weight: 500; +} + + + +#graph{ + height: 100vh; + width: 100vw; + position: fixed !important; + z-index: -10; + top: 0px; + left: 0px; +} + + +circle.combined{ + stroke: #aaa; + fill: #f5f5f5; + stroke-width: .5px; +} + + +.header > span{ + /*display: none;*/ + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; + +} + +h1{ + font-size: 45px; + margin-top: 5px; + /*-webkit-text-fill-color: white; */ + -webkit-text-stroke-width: 1px; + -webkit-text-stroke-color: white; + text-shadow: 0 2px 0 #fff, 2px 0 0 #fff, 0 -2px 0 #fff, -2px 0 0 #fff; +} + + +svg{ + overflow: visible; +} + + + +text{ + pointer-events: none; + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; +} + + + +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; + display: none; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + + + +#circle-icon{ + width: 12px; + height: 12px; + border: 1.5px solid #000; + display: inline-block; + border-radius: 12px; +} + diff --git a/source/dvs-privacy/test.html b/source/dvs-privacy/test.html new file mode 100644 index 00000000..b5c6d0cc --- /dev/null +++ b/source/dvs-privacy/test.html @@ -0,0 +1,15 @@ + + + + + + +
+
+
+
+
+ + + + diff --git a/source/dvs-privacy/todo b/source/dvs-privacy/todo new file mode 100644 index 00000000..e69de29b diff --git a/source/flushing-cbtc-finished/cbtc-queens.png b/source/flushing-cbtc-finished/cbtc-queens.png new file mode 100644 index 00000000..e587cbe6 Binary files /dev/null and b/source/flushing-cbtc-finished/cbtc-queens.png differ diff --git a/source/flushing-cbtc-finished/script.js b/source/flushing-cbtc-finished/script.js new file mode 100644 index 00000000..331cc09e --- /dev/null +++ b/source/flushing-cbtc-finished/script.js @@ -0,0 +1,331 @@ +console.clear() +d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + +var data = `date,link,cost,finish,endDate +2011-07-28,http://web.mta.info/mta/news/books/archive/110627_1345_CPOC.pdf,550,November 2016,2016-11-30 +2012-07-23,http://web.mta.info/mta/news/books/archive/120723_1400_CPOC.pdf,550,November 2016,2016-11-30 +2013-04-22,http://web.mta.info/mta/news/books/archive/130422_1300_Cpoc.pdf,550,November 2016,2016-11-30 +2013-11-12,http://web.mta.info/mta/news/books/archive/131112_1345_CPOC.pdf,550,4th Quarter 2016 to 2nd Quarter 2017,2017-03-31 +2015-04-27,http://web.mta.info/mta/news/books/archive/150427_1315_CPOC.pdf,550,2nd Quarter 2017,2017-06-30 +2015-10-26,http://web.mta.info/mta/news/books/archive/151026_1345_CPOC.pdf,550 + 20M, 3rd Quarter 2017 (Delayed from 2nd Quarter 2017),2017-09-30 +2016-07-25,http://web.mta.info/mta/news/books/archive/160725_1345_CPOC.pdf, ,4th Qtr. 2017 - delayed by 1 Qtr.,2017-12-31 +2017-07-24,http://web.mta.info/mta/news/books/pdf/170724_1345_CPOC.pdf,595M,4th Qtr. 2017,2017-12-31 +2017-12-11,http://web.mta.info/mta/news/books/pdf/171211_1330_CPOC.pdf,588M,In-Service projected for 2nd Quarter 2018; previously 4th Quarter 2017,2018-06-30 +2018-01-22,http://web.mta.info/mta/news/books/pdf/180122_1400_CPOC.pdf,563.6M,2nd Qtr 2018,2018-06-30 +2018-04-23,http://web.mta.info/mta/news/books/pdf/180423_1330_CPOC.pdf,$588,2nd - 4th Quarter 2018 ,2018-12-31 +2018-07-23,http://web.mta.info/mta/news/books/pdf/180723_1400_CPOC.pdf,563.6M,Oct- 18,2018-10-31 +2018-10-22,http://web.mta.info/mta/news/books/pdf/181022_1330_CPOC.pdf,563.6M,Nov-17,2018-11-30 +2018-11-13,http://web.mta.info/mta/news/books/pdf/181113_1400_CPOC.pdf,588,11/26/2018,2018-11-26 +2018-11-26,http://web.mta.info/mta/news/books/pdf/181113_1400_CPOC.pdf,588,11/26/2018,2018-11-26` + +data = d3.csvParse(data) +var timeFmt = d3.timeParse('%Y-%m-%d') + +data.forEach((d, i) => { + d.i = i + + var prev = i ? data[i - 1] : d + d.prev = prev + + d.rawDate = d.date + d.rawEndDate = d.endDate + + // TK don't be lazyyy + d.date = timeFmt(d.date) + d.endDate = timeFmt(d.endDate) + + d.remain0 = (+d.prev.endDate - d.date)/24/60/60/1000 + d.remain1 = (+d.endDate - d.date)/24/60/60/1000 +}) + +var isMobile = innerWidth < 800 +var sel = d3.select('#two-years').html('').append('div') +var c = d3.conventions({ + sel, + margin: isMobile ? {left: 30, right: 2, top: 8} : {left: 70, right: 70}, + height: isMobile ? innerWidth/2 : 400, + width: isMobile ? 0 : 700 +}) + +var {width, height} = c + +c.svg.append('mask#draw-area').append('rect').at({width, height, fill: '#fff'}) + + +var allDates = data.map(d => d.date).concat(data.map(d => d.endDate)) + +c.x = d3.scaleTime() + .domain(d3.extent(allDates)) + .domain(['2015-01-01', '2018-11-26'].map(timeFmt)) + .range(c.x.range()) + +c.y + .domain([0, 365*3]) + .domain([0, 830]) + +c.xAxis = d3.axisBottom(c.x).ticks(5) + +c.yAxis.ticks(isMobile ? 5 : 10) +d3.drawAxis(c) + +c.svg.selectAll('.y text').at({x: -3}) +c.svg.select('.y text').at({x: -6}).remove() + +if (!isMobile){ + c.svg.selectAll('.y .tick').filter(d => d == 800) + .append('text').text('days to') + .at({fill: '#000', y: 0, dy: '.32em', x: -3}) + .parent() + .append('text').text('completion') + .at({fill: '#000', y: 12, dy: '.32em', x: -3}) + .parent().st({fontWeight: 700}) + .select('text') + .at({y: -12}) +} + +var topPath = 'M' + data.map(d => { + return [c.x(d.date), c.y(d.remain0)] + ' L ' + [c.x(d.date), c.y(d.remain1)] +}).join(' L ') + +var baseSel = c.svg.append('g').at({mask: 'url(#draw-area)'}) + +baseSel.append('path') + .at({ + d: topPath + ` V ${c.height} H 0 Z`, + fill: '#BE00C1', + opacity: .1, + }) + +baseSel.appendMany('line', isMobile ? [200, 400, 600, 800] : c.y.ticks()) + .at({x1: width, stroke: '#f5f5f5', strokeWidth: 1.5}) + .translate(c.y, 1) + +baseSel.append('path') + .at({ + d: topPath, + stroke: '#BE00C1', + fill: 'none', + strokeWidth: 2, + }) + + + + + +// baseSel.appendMany('circle', data) +// .at({r: 3, fill: '#fff', stroke: '#000'}) +// .translate(d => [c.x(d.date), c.y(d.remain0)]) +// .call(d3.attachTooltip) + +// baseSel.appendMany('circle', data) +// .at({r: 3, fill: '#fff', stroke: '#f0f'}) +// .translate(d => [c.x(d.date), c.y(d.remain1)]) +// .call(d3.attachTooltip) + + +var initDays = -(c.x.domain()[0] - data[0].endDate)/24/60/60/1000 +baseSel.append('path#original') + .at({ + d: `M ${[0, c.y(initDays)]} L ${[c.x(data[0].endDate), c.height]}`, + stroke: '#000', + strokeDasharray: '5 5' + }) + +baseSel.append('text') + .at({dy: -5, x: -5}) + .append('textPath') + .text('Original Completion Date →') + .at({href: '#original', textAnchor: 'end'}) + .attr('startOffset', '100%') + + + // 2015-10-26 + // additional time required for testin and commissioning + // Shadow mode operations start was delayed several months + // CBTC car equipment installations has not progressed as planned. + // http://web.mta.info/mta/news/books/archive/151026_1345_CPOC.pdf + + + // 2016-07-25 + // http://web.mta.info/mta/news/books/archive/160725_1345_CPOC.pdf + // Stability of the system software is taking longer than expected + + +// 2017-12-11 +// http://web.mta.info/mta/news/books/pdf/171211_1330_CPOC.pdf +// Reliable and stable system software is needed +// Additional hardware and software issues have been identified. +// implemented an interim fix to address hardware failures +// The issues impacting project progress are complex and require extensive effort and time to resolve +// Additional software and hardware modifications could be +// identified as passenger service expands to operate with full +// CBTC functionalities during rush hours service. +// - Availability of General Orders to complete commissioning +// activities. +// - Need for an updated System Performance Analysis that +// reflects current operational data. + + +// 2018-04-23 +// http://web.mta.info/mta/news/books/pdf/180423_1330_CPOC.pdf +// Progress was made in expanding CBTC revenue service +// north of 74th Street. However, system performance issues +// and lack of system stability are impacting the project +// team’s ability to extend CBTC operation to the remaining +// section of the Line. + +// Schedule: NYCT is currently evaluating a revised schedule +// for remaining commissioning activities that was recently +// submitted by Thales. +// - The IEC is of the opinion that completion of in-service +// activities in the 2nd Quarter (as was reported to the Board in +// December) is no longer achievable. +// - System performance within the next few weeks will +// determine when CBTC operation can expand to the entire +// line. +// - Budget: The project remains within the approved budget. +// The project has $5M in reserve. However, additional +// funds may be required in the event of further delays to inservice +// activities. +// IEC Observations: +// - Progress was made in expanding CBTC passenger service +// operation between Main Street & 74th Street to AM, PM and +// limited rush hours. +// - The contractor has implemented a number of software +// modifications to address identified CBTC system issues. +// - System performance issues continue to delay CBTC +// migration south of 74th Street +// - The project team implemented an interim fix for damages to +// speed sensor hardware that caused speed measurement +// failures +// - A permanent fix based on a redundant architecture is +// planned for after completion of in-service activities + +// IEC Concerns: +// - Communication issues continue to impact system +// performance. The root cause of communication failures has +// not been determined +// - Additional design/software modifications are required +// - Software updates are needed to address Automatic Train +// Supervision (ATS) failures +// - System issues are causing system interruptions +// - Rescheduling of General Orders to support commissioning +// activities south of 74th Street could be challenging +// Recommendation: +// The IEC recommends a third party comprehensive, in-depth +// review of the Data Communication System (DCS) design + +// Project Risks +// - Remaining system issues continue to delay critical project +// milestones (achieving full CBTC service North of 74th Street & +// achieving CBTC system stability), which continue to impact +// full revenue service & Substantial Completion +// - Migrating CBTC operation to south of 74th Street requires a +// high level of system stability +// - Limited existing wayside signals to support passenger +// service in the event of CBTC failure + +var annos = +[ + { + "x": "2015-10-26", + "y": 650, + "path": "M -5,-68 A 31.223 31.223 0 0 0 -8,-12", + "text": "More testing time to validate and verify safety", + "textOffset": [ + 3, + -81 + ] + }, + { + "x": "2016-07-25", + "y": 480, + "path": "M 3,-60 A 29.471 29.471 0 0 0 -11,-10", + "text": "Various anomalies discovered during CBTC monitoring", + "textOffset": [ + 14, + -67 + ] + }, + { + "x": "2017-12-11", + "y": 120, + "path": "M -53,-58 A 42.66 42.66 0 0 0 -10.99999713897705,-9.999998092651367", + "text": "Reliable and stable system software is needed", + "textOffset": [ + -100, + -95 + ] + }, + { + "x": "2018-04-23", + "y": 180, + "path": "M 3,-60 A 29.471 29.471 0 0 0 -11,-10", + "text": "Software updates are needed to address ATS failures", + "textOffset": [ + 14, + -90 + ] + } +] + +var swoopy = d3.swoopyDrag() + .x(d => c.x(timeFmt(d.x))) + .y(d => c.y(d.y)) + .draggable(0) + .annotations(annos) + +var swoopySel = c.svg.append('g.swoopy').call(swoopy).st({opacity: isMobile ? 0 : 1}) + +swoopySel.selectAll('path') + .attr('marker-end', 'url(#arrow)') + .at({fill: 'none', stroke: '#000', strokeWidth: .6}) + +swoopySel.selectAll('text') + .each(function(d){ + d3.select(this) + .text('') //clear existing text + .tspans(d3.wordwrap(d.text, 20)) //wrap after 20 char + }) + + +c.svg.append('marker') + .attr('id', 'arrow') + .attr('viewBox', '-10 -10 20 20') + .attr('markerWidth', 17) + .attr('markerHeight', 17) + .attr('orient', 'auto') + .append('path') + .attr('d', "M8,0 L0,-4 L 0,4Z") + + + + + +sel.append('div.source') + .st({marginLeft: c.margin.left, bottom: -25, position: 'absolute'}) + .html(` + Capital Program Oversight Committee +`) + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/flushing-cbtc-finished/slope.js b/source/flushing-cbtc-finished/slope.js new file mode 100644 index 00000000..a473192b --- /dev/null +++ b/source/flushing-cbtc-finished/slope.js @@ -0,0 +1,274 @@ +var parentSel = d3.select('#slope').html('') + +var bgcolor = '#f5f5f5' +var color = {'L': '#878787', '7': '#BE00C1'} + + +var sel = parentSel.append('div') +var c = d3.conventions({ + sel, + margin: {left: 50, right: 70, bottom: 50}, + height: 400, +}) + + +c.y.domain([0, 810]) +c.x.domain([0, 1]) + + +var data = [ + [217, 328], + [632, 810] +] + +data.forEach((d, i) => d.line = !i ? 'L' : '7') + + +c.svg.appendMany('pattern', ['L', '7']) + .at({x: 0, y: 0, width: 2, height: 2, id: d => 'line-pattern-' + d}) + .attr('patternUnits', 'userSpaceOnUse') + .append('circle') + .at({r: 5, fill: bgcolor}) + .parent() + .append('line') + .at({x1: 10, stroke: d => color[d], strokeWidth: 1.5}) + +var line = c.svg.appendMany('g', data) + .at({class: d => 'line-' + d.line}) + + + +line.append('path') + .at({ + stroke: '#000', + strokeWidth: 1, + d: d => 'M' + [c.x(0), c.y(d[0])] + 'L' + [c.x(1), c.y(d[1])] + }) + + +line.appendMany('circle', d => d) + .at({ + r: 6, + cx: (d, i) => c.x(i), + cy: c.y, + }) + .st({ + fill: (d, i) => i ? '' : `url(#line-pattern-${d == 217 ? 'L' : 7})`, + strokeWidth: 0, + }) + +line.appendMany('text', d => d) + .at({ + x: (d, i) => c.x(i), + y: c.y, + dy: '.33em', + textAnchor: (d, i) => i ? 'start' : 'end', + dx: (d, i) => i ? 7 : -9, + }) + .text(d => '$' + d + 'M') + + +var baseSel = c.svg.append('g.axis') + .translate(c.height, 1) + +baseSel.append('line') + .at({x2: c.width, stroke: '#000'}) +baseSel.append('line') + .at({y2: 5, stroke: '#000'}) +baseSel.append('line') + .translate(c.width, 0) + .at({y2: 5, stroke: '#000'}) + +baseSel.append('text') + .tspans('Estimated Cost'.split(' ')) + .at({ + textAnchor: 'middle', + dy: 17 + }) + +baseSel.append('text') + .tspans('Actual Cost'.split(' ')) + .at({ + textAnchor: 'middle', + x: c.width, + dy: 17 + }) + + + + + + + + + + + + + +// TODO +// Mask pattern fill circle +// Cost and Timeline label + + +// timeline + + +var sel = parentSel.append('div') +var c = d3.conventions({ + sel, + margin: {left: 50, right: 70, bottom: 50}, + height: 400, +}) + + +c.x.domain([0, 1]) +c.y.domain([2019, 1999].map(d3.timeParse('%Y')).reverse()) + + +c.svg.appendMany('g.axis', d3.range(2000, 2020, 2)) + .translate(d => [c.width/2, c.y(d3.timeParse('%Y')(d))]) + .append('text') + .text(d => d) + .at({textAnchor: 'middle', dy: '.33em'}) + + + +var timeline = [ + {line: 'L', type: 0, t0: 'Oct. 1999', t1: 'April 2004'}, + {line: 'L', type: 1, t0: 'Dec. 1999', t1: 'March 2009'}, + + {line: '7', type: 0, t0: 'June 2007', t1: 'Dec. 2013'}, + {line: '7', type: 1, t0: 'June 2010', t1: 'Nov. 2018'}, +] + +timeline.forEach(d => { + d.d0 = d3.timeParse('%b %Y')(d.t0) + d.d1 = d3.timeParse('%b %Y')(d.t1) +}) + + +var line = c.svg.appendMany('g', timeline) + .translate(d => c.x(d.type), 0) + +line.append('path') + .at({ + stroke: '#000', + strokeWidth: 5, + d: d => 'M' + [0, c.y(d.d0)] + 'V' + c.y(d.d1) + }) + .st({ + // stroke: 'url(line-pattern-7)' + stroke: d => color[d.line], + strokeDasharray: d => d.type ? '' : '1 2' + }) + + +// line.append('circle') +// .at({ +// r: 4, +// strokeWidth: 2, +// cy: d => c.y(d.d0), +// }) +// .st({ +// stroke: d => color[d.line], +// fill: bgcolor +// }) + +// line.append('circle') +// .at({ +// r: 4, +// strokeWidth: 2, +// cy: d => c.y(d.d1), +// }) +// .st({ +// stroke: d => color[d.line], +// fill: d => color[d.line] +// }) + + + +var baseSel = c.svg.append('g.axis') + .translate(c.height, 1) + +baseSel.append('line') + .at({x2: c.width, stroke: '#000'}) +baseSel.append('line') + .at({y2: 5, stroke: '#000'}) +baseSel.append('line') + .translate(c.width, 0) + .at({y2: 5, stroke: '#000'}) + +baseSel.append('text') + .tspans('Estimated Timeline'.split(' ')) + .at({ + textAnchor: 'middle', + dy: 17 + }) + +baseSel.append('text') + .tspans('Actual Timeline'.split(' ')) + .at({ + textAnchor: 'middle', + x: c.width, + dy: 17 + }) + + + +// baseSel.append('text') +// .text('Timeline') +// .at({ +// textAnchor: 'middle', +// x: c.width/2, +// dy: 17 +// }) + + + + +window.tlanno = +[ + { + "path": "M -63,-25 A 35.86 35.86 0 0 0 -12,21", + "text": "The MTA planned on starting in 2007 and working for 6.5 years", + "textOffset": [ + -108, + -59 + ] + } +] + + +var swoopy = d3.swoopyDrag() + .x(d => c.x(0)) + .y(d => c.y(d3.timeParse('%Y')(2013))) + .draggable(0) + .annotations(tlanno) + +var swoopySel = c.svg.append('g.swoopy').call(swoopy) + .st({opacity: innerWidth< 700 ? 0 : 1}) + +swoopySel.selectAll('path') + .attr('marker-end', 'url(#arrow)') + .at({fill: 'none', stroke: '#000', strokeWidth: .4}) + +swoopySel.selectAll('text') + .each(function(d){ + d3.select(this) + .text('') //clear existing text + .tspans(d3.wordwrap(d.text, 20)) //wrap after 20 char + }) + + + +parentSel.append('div.source') + .html(` + L timeline and costs don't include Phase I prototyping. 7 estimated start date from the 2005 capital plan, estimated duration from the 2010 contract award. +
+ Reinvent Albany + Citizens Budget Commission + IEEE +`) + + diff --git a/source/flushing-cbtc-finished/style.css b/source/flushing-cbtc-finished/style.css new file mode 100644 index 00000000..ac451c06 --- /dev/null +++ b/source/flushing-cbtc-finished/style.css @@ -0,0 +1,211 @@ +body{ + max-width: 600px; +} + +h1{ + max-width: 530px; +} + +/*h1{ + max-width: 500px; + margin: 0px auto; + margin-top: 20px; + margin-bottom: 20px; + +} + +p{ + max-width: 500px; + margin: 0px auto; + margin-top: 20px; + margin-bottom: 20px; + font-weight: 200; +} + +h3{ + max-width: 500px; + margin: 0px auto; +} +*/ +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.domain{ + display: none; +} + +text{ + font-size: 14px; + /*pointer-events: none;*/ + /*text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;*/ +} + +.img-cont{ + max-width: 805px; + margin: 0px auto; + margin-top: 20px; + margin-bottom: 20px; +} + +.img-cont img{ + max-width: 100%; +} + +#two-years > div{ + max-width: 805px; + margin: 0px auto; + margin-bottom: 45px; +} + +.full-width { + width: 100vw; + position: relative; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; +} + +#two-years .trip{ + display: inline-block; +} + +.axis{ + opacity: .5; +} +.y line{ + opacity: 0; +} + + + + +/*slope*/ + +.line-L circle{ + fill: #878787; + stroke: #878787; +} + +.line-L path{ + stroke: #878787; +} + +b.line-L{ + color: #fff; + background: #878787; + border-radius: 50%; + padding: 3px 7.5px; + font-size: 18px; + position: relative; + top: -1px; + width: 15.5px; + text-align: center; + display: inline-block; +} +b.line-7{ + color: #fff; + background: #BE00C1; + border-radius: 50%; + padding: 3px 7.5px; + font-size: 18px; + position: relative; + top: -1px; + width: 15.5px; + text-align: center; + display: inline-block; +} + +.line-7 circle{ + fill: #BE00C1; + stroke: #BE00C1; +} + +.line-7 path{ + stroke: #BE00C1; +} + +#slope > div { + display: inline-block; + width: 290px; +} + +#slope > .source{ + width: 98%; +} + +text{ + font-family: monospace; + font-size: 12px; +} + + + + + + + + + + + +.cbtc-queens-key{ + background: #F83313; + color: #fff; + padding: 3px; + border-radius: 3px; +} + + + + + + + +.source{ + font-weight: 400; + font-size: 12px; + line-height: 13px; + opacity: .5; + font-family: monospace; +} + +@media (max-width: 800px){ + .img-source{ + margin-left: 5px !important; + } +} + + + + + + + + diff --git a/source/flushing-cbtc-finished/todo b/source/flushing-cbtc-finished/todo new file mode 100644 index 00000000..106dc7ad --- /dev/null +++ b/source/flushing-cbtc-finished/todo @@ -0,0 +1,12 @@ +x add anno back +x lower labels +x pattern fill +x sources +x 800 days +| label on est? +x mobile +x we finished est compl +x computer annos +- z read? + +twitter diff --git a/source/forecast-correlation/README.md b/source/forecast-correlation/README.md new file mode 100644 index 00000000..996c0f7b --- /dev/null +++ b/source/forecast-correlation/README.md @@ -0,0 +1,4 @@ +Code for [Forecast Correlation Comparisons](https://roadtolarissa.com/forecast-correlation/) + +- [Frontend](https://github.com/1wheel/roadtolarissa/blob/master/source/forecast-correlation/script.js) +- [Data munging](https://github.com/1wheel/archive-roadtolarissa/blob/master/data/forecast-correlation/parse.js) \ No newline at end of file diff --git a/source/forecast-correlation/hcluster.js b/source/forecast-correlation/hcluster.js new file mode 100644 index 00000000..5d4c1f75 --- /dev/null +++ b/source/forecast-correlation/hcluster.js @@ -0,0 +1,413 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.hcluster = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o data.length) throw new Error('n must be less than the size of the dataset'); + return clustersGivenK[data.length - n] + .map(function(indexes) { + return indexes.map(function(ndx) { return data[ndx]; }); + }); + }; + + // + // math, matrix utility fn's + + // return unique pairs of indexes on n x n matrix above the diagonal + clust._squareMatrixPairs = function(n) { + var pairs = []; + for(var row = 0; row < n; row++) { + for(var col = row + 1; col < n; col++) { + pairs.push([row, col]); + } + } + return pairs; + }; + + // average distance between set of cluster indexes + clust._avgDistance = function(setA, setB) { + var distance = 0; + for(var ndxA = 0; ndxA < setA.length; ndxA++) { + for(var ndxB = 0; ndxB < setB.length; ndxB++) { + distance += data[setA[ndxA]]._distances[setB[ndxB]]; + } + } + return distance / setA.length / setB.length; + }; + + // min distance between set of cluster indexes + clust._minDistance = function(setA, setB) { + var distances = []; + for(var ndxA = 0; ndxA < setA.length; ndxA++) { + for(var ndxB = 0; ndxB < setB.length; ndxB++) { + distances.push(data[setA[ndxA]]._distances[setB[ndxB]]); + } + } + return distances.sort()[0]; + }; + + // max distance between set of cluster indexes + clust._maxDistance = function(setA, setB) { + var distances = []; + for(var ndxA = 0; ndxA < setA.length; ndxA++) { + for(var ndxB = 0; ndxB < setB.length; ndxB++) { + distances.push(data[setA[ndxA]]._distances[setB[ndxB]]); + } + } + return distances.sort()[distances.length-1]; + }; + + // + // tree construction + + // + clust._buildTree = function() { + if(!data || !data.length) throw new Error('Need `data` to build tree'); + + // + var node, clusterPairs, nearestPair, newCluster; + clusters = []; + clustersGivenK = []; + tree = {}; + + // calculate distances and build single datum clusters + data.forEach(function(d, ndx) { + d._distances = data.map(function(compareTo) { + return distanceFn(d[posKey], compareTo[posKey]); + }); + clusters.push(extend(d, { + height: 0, + indexes: [ndx] + })); + }); + + // for tree of n leafs, n-1 linkages + for(var iter = 0; iter < data.length - 1; iter++) { + verbose && console.log(iter + ': ' + + clusters.map(function(c) { return c.indexes; }).join('|')); + + // find closest pair of clusters, pair[2] is distance + clusterPairs = clust._squareMatrixPairs(clusters.length); + clusterPairs.forEach(function(pair) { + pair[2] = clust['_'+linkage+'Distance']( + clusters[pair[0]].indexes, + clusters[pair[1]].indexes ); }); + nearestPair = clusterPairs + .reduce(function(pairA, pairB) { return pairA[2] <= pairB[2] ? pairA : pairB; }, + [0, 0, Infinity]); + newCluster = { + name: 'Node ' + iter, + height: nearestPair[2], + indexes: clusters[nearestPair[0]].indexes.concat(clusters[nearestPair[1]].indexes), + children: [ clusters[nearestPair[0]], clusters[nearestPair[1]] ], + }; + verbose && console.log(newCluster); + clustersGivenK.push(clusters.map(function(c) { return c.indexes; })); + + // remove merged nodes and push new node + clusters.splice(Math.max(nearestPair[0], nearestPair[1]),1); + clusters.splice(Math.min(nearestPair[0], nearestPair[1]),1); + clusters.push(newCluster); + } + + treeRoot = clusters[0]; + // clust._rebalanceTree(treeRoot); + }; + + // TODO: better rebalancing algo? ... this is just for presentation + // rebalance after tree is built (b/c it is top down operation) + // clust._rebalanceTree = function(node) { + // if(node.parent && node.parent.children && node.parent.children.length && + // node.children && node.children.length) { + // var rightDistance = clust['_'+linkage+'Distance']( + // node.parent.children[1].indexes, + // node.children[0].indexes); + // var leftDistance = clust['_'+linkage+'Distance']( + // node.parent.children[1].indexes, + // node.children[1].indexes); + + // // switch order of node.children + // if(leftDistance > rightDistance) { + // node.children = [ node.children[1], node.children[0] ]; + // node.indexes = node.children[0].indexes.concat(node.children[1].indexes); + // } + // } + // if(node.children) { + // clust._rebalanceTree(node.children[0]); + // clust._rebalanceTree(node.children[1]); + // } + // }; + + return clust; +}; + +module.exports = hcluster; + +},{"distancejs":1,"extend":8}]},{},[9])(9) +}); \ No newline at end of file diff --git a/source/forecast-correlation/script.js b/source/forecast-correlation/script.js new file mode 100644 index 00000000..25f82ecd --- /dev/null +++ b/source/forecast-correlation/script.js @@ -0,0 +1,630 @@ +console.clear() +var ttSel = d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + + +var states = ["AK","AL","AR","AZ","CA","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","LA","MA","MD","ME","MI","MN","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","NY","OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VA","VT","WA","WI","WV","WY"].map((str, stateIndex) => { + return {str, stateIndex} +}) +var nStates = states.length + +var corScale = d3.scaleLinear().domain([-.75, .75]) +var corColor = d => d3.interpolatePiYG(corScale(d)) + +var state = { + pairStr: '' +} + + +window.globalSetPair = function(pair){ + matrix538.setPair(pair) + matrixEco.setPair(pair) + + if (!window.scatterEco) return + + var extent = d3.extent(scatter538.calcExtent(pair).concat(scatterEco.calcExtent(pair))) + extent = d3.scaleLinear().domain(extent).nice(5).domain() + scatter538.drawPair(pair, extent) + scatterEco.drawPair(pair, extent) + + corScatter.setPair(pair) +} + +window.globalSetScenario = function(x, y){ + // map538.drawScenario(scenarioIndex) + // mapEco.drawScenario(scenarioIndex) + + scatter538.drawScenario(x, y) + scatterEco.drawScenario(x, y) +} + +d3.loadData( + 'https://roadtolarissa.com/data/forecast-correlation/pairs-538.json', + 'https://roadtolarissa.com/data/forecast-correlation/pairs-eco.json', + 'https://roadtolarissa.com/data/forecast-correlation/states-10m.json', + 'states.csv', + async (err, res) => { + var totalWidth = 1200 + var colMarginLeft = -(totalWidth - 750)/2 + 40 + d3.select('.graph').html(`
`) + + d3.selectAll('.graph,.state-sm') + .st({width: totalWidth, marginLeft: colMarginLeft}) + + var [model538, modelEco] = ['538', 'eco'].map((str, i) => { + var sel = d3.select('.col-' + str) + .html(`
`) + var pairs = res[i] + var strLong = ['538', 'Economist'][i] + var strShort = ['538', 'Econ'][i] + + pairs.forEach((d, i) => { + d.canonicalStr = [d.strA, d.strB].sort().join(' ') + d.pairIndex = i + }) + + var stateData = [] + var activeScenarioIndex = -1 + + return {str, strLong, strShort, i, sel, pairs, stateData, activeScenarioIndex} + }) + window.model538 = model538 + window.modelEco = modelEco + + var index2cluster = calcIndex2Cluster(model538, modelEco) + window.matrix538 = initMatrix(model538, index2cluster) + window.matrixEco = initMatrix(modelEco, index2cluster) + + window.corScatter = initCorScatter() + + + + + if (!window.mapsEco){ + var url = 'https://roadtolarissa.com/data/forecast-correlation/maps-538.buf' + window.maps538 = model538.maps = new Int16Array(await(await fetch(url)).arrayBuffer()) + + var url = 'https://roadtolarissa.com/data/forecast-correlation/maps-eco.buf' + window.mapsEco = modelEco.maps = new Int16Array(await(await fetch(url)).arrayBuffer()) + } else { + model538.maps = window.maps538 + modelEco.maps = window.mapsEco + } + + window.scatter538 = initScatter(model538) + window.scatterEco = initScatter(modelEco) + + var [us, stateVotes] = res.slice(-2) + stateVotes.forEach((d, i) => { + d.i = i + }) + + window.map538 = initMap(model538, us, stateVotes) + window.mapEco = initMap(modelEco, us, stateVotes) + + window.globalSetPair(model538.pairs[324]) + window.globalSetScenario(5000, 5000) + + initStateSm() +}) + +function initMatrix(model, index2cluster){ + var isLock = false + var sel = model.sel.select('.matrix').on('mouseleave', d => isLock = false) + + var bs = 10 + var width = bs*nStates + + var c = d3.conventions({ + sel: sel.append('div'), + width: width, + height: width, + margin: {top: 70}, + }) + + var rectSel = c.svg.appendMany('rect', model.pairs) + .translate(d => [bs*index2cluster[d.indexA], bs*index2cluster[d.indexB]]) + .at({ + width: bs - 0, + height: bs - 0, + // fill: d => d.indexA == d.indexB ? '#fff' : d3.interpolatePiYG(corScale(d.cor)) + fill: d => corColor(d.cor), + cursor: 'pointer', + }) + .on('mouseover', d => { + if (isLock) return + globalSetPair(d) + }) + .on('click', d => { + globalSetPair(d) + isLock = !isLock + }) + + var aTextSel = c.svg.appendMany('g', states) + .translate(d => [index2cluster[d.stateIndex]*bs, -3]) + .append('text.abv') + .text(d => d.str) + .at({textAnchor: 'start', y: bs/2, dy: '.33em', transform: 'rotate(-90)'}) + + var bTextSel = c.svg.appendMany('text.abv', states) + .text(d => d.str) + .translate(d => [-3, index2cluster[d.stateIndex]*bs]) + .at({textAnchor: 'end', y: bs/2, dy: '.33em'}) + + var abvSel = d3.selectAll('.abv') + + function setPair(pair){ + rectSel + .classed('active', 0) + .filter(d => d.canonicalStr == pair.canonicalStr) + .classed('active', 1) + .raise() + + aTextSel.classed('active', d => d.str == pair.strA) + bTextSel.classed('active', d => d.str == pair.strB) + } + + + c.svg.append('g.x.axis').translate(-70, 1) + addAxisLabel(c, model.strLong + ' Correlations Between States') + + return {abvSel, rectSel, setPair} +} + +function initScatter(model){ + var isLock = false + var sel = model.sel.select('.scatter').on('mouseleave', d => isLock = false) + + var titleSel = sel.append('div.small-title') + + var c = d3.conventions({ + sel, + layers: 'scs', + width: 200, + height: 200, + margin: {bottom: 50, right: 20} + }) + + c.svg.append('rect') + .at({width: c.width, height: c.height, fill: '#eee'}) + + c.x.interpolate(d3.interpolateRound) + c.y.interpolate(d3.interpolateRound) + c.xAxis.tickFormat(d3.format('.0%')).ticks(5).tickSize(c.height) + c.yAxis.tickFormat(d3.format('.0%')).ticks(5).tickSize(c.height) + + d3.drawAxis(c) + var xAxisSel = c.svg.select('.x').translate(0, 0) + var yAxisSel = c.svg.select('.y').translate(c.width, 0) + + addAxisLabel(c, ' ', ' ') + xAxisSel.select('.label').translate(c.height, 1) + yAxisSel.select('.label').at({y: -c.width + 10}) + + c.svg.append('clipPath#line-clip') + .append('rect').at({width: c.width, height: c.height}) + + var lineSel = c.svg.append('path') + .at({stroke: '#555', strokeWidth: 2, clipPath: 'url(#line-clip)', strokeDasharray: '2 2'}) + + var ctx = c.layers[1] + + var svg2 = c.layers[2] + svg2.append('rect') + .at({width: c.width, height: c.height, fillOpacity: 0}) + .parent() + .on('mousemove', function(){ + if (isLock) return + var [x, y] = d3.mouse(this) + globalSetScenario(c.x.invert(x), c.y.invert(y)) + }) + .on('click', function(){ + var [x, y] = d3.mouse(this) + globalSetScenario(c.x.invert(x), c.y.invert(y)) + isLock = !isLock + }) + .st({cursor: 'pointer'}) + + var circleSel = svg2.append('circle') + .at({stroke: 'orange', r: 3, fill: 'none', strokeWidth: 2 }) + + var hPathSel = svg2.append('path').at({stroke: 'orange', strokeWidth: .5}) + var vPathSel = svg2.append('path').at({stroke: 'orange', strokeWidth: .5}) + + var xData = d3.range(40000) + var yData = d3.range(40000) + + function calcExtent(pair){ + xData = model.stateData[pair.indexA] + if (!xData){ + xData = model.stateData[pair.indexA] = d3.range(40000).map(i => model.maps[i*states.length + pair.indexA]/10000) + } + + yData = model.stateData[pair.indexB] + if (!yData){ + yData = model.stateData[pair.indexB] = d3.range(40000).map(i => model.maps[i*states.length + pair.indexB]/10000) + } + + return d3.extent(xData.concat(yData)) + } + + function drawPair({indexA, indexB}, extent){ + var pair = _.find(model.pairs, {indexA, indexB}) + + titleSel.html(` + ${model.strShort} ${pair.strA}-${pair.strB} correlation: + ${d3.format('+.2f')(pair.cor)} `) + + c.x.domain(extent) + c.y.domain(extent) + + c.svg.select('.x').call(c.xAxis).selectAll('.tick').classed('bold', d => d == .5) + c.svg.select('.y').call(c.yAxis).selectAll('.tick').classed('bold', d => d == .5) + ctx.clearRect(-c.margin.left, -c.margin.right, c.totalWidth, c.totalWidth) + + xAxisSel.select('.label').text('Trump Vote Share in ' + pair.strA) + yAxisSel.select('.label').text('Trump Vote Share in ' + pair.strB) + + ctx.fillStyle = 'rgba(0,0,0,.2)' + xData.slice(0, 5000).forEach((_, i) => { + ctx.beginPath() + ctx.rect(c.x(xData[i]), c.y(yData[i]), 1, 1) + ctx.fill() + }) + + var l = ss.linearRegressionLine(pair) + var [x0, x1] = extent.map(d => d*10000) + + lineSel.at({d: `M ${c.x(x0/10000)} ${c.y(l(x0)/10000)} L ${c.x(x1/10000)} ${c.y(l(x1)/10000)}`}) + + if (model.activeScenarioIndex > -1){ + drawScenario(null, null, model.activeScenarioIndex) + } + } + + function drawScenario(x, y, minI=-1){ + var skipMap = true + if (minI > -1){ + x = xData[minI] + y = yData[minI] + } else { + skipMap = false + var minDist = Infinity + xData.forEach((xVal, i) => { + var dx = x - xVal + var dy = y - yData[i] + + var dist = dx*dx + dy*dy + + if (dist < minDist){ + minDist = dist + minI = i + } + }) + } + + + + circleSel.translate([c.x(xData[minI]), c.y(yData[minI])]) + hPathSel.at({d: 'M 0 ' + c.y(y) + ' H ' + c.x(x)}) + vPathSel.at({d: 'M ' + c.x(x) + ' ' + c.height + ' V ' + c.y(y)}) + + model.activeScenarioIndex = minI + + if (!skipMap) model.map.drawScenario(minI) + } + + return {drawPair, calcExtent, drawScenario} +} + +function initMap(model, us, stateVotes){ + var width = 275 + + var sel = model.sel.select('.map') + var titleSel = sel.append('div.small-title') + .text('') + var c = d3.conventions({ + sel, + layers: 's', + width, + height: width, + margin: {bottom: 0, left: 0, right: 0} + }) + + us.land = us.land || topojson.feature(us, us.objects.nation) + var path = d3.geoPath().projection(d3.geoAlbersUsa().fitSize([width, width], us.land)) + + var stateSel = c.svg.appendMany('path.states', topojson.feature(us, us.objects.states).features) + .at({d: path}) + .each(d => { + d.state = _.find(stateVotes, {name: d.properties.name}) + }) + .filter(d => d.state) + + us.stateMesh = us.stateMesh || path(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b })) + c.svg.append('path.state-borders') + .at({stroke: '#fff', strokeWidth: .4, fill: 'none', d: us.stateMesh}) + + var dSel = c.svg.append('g.small-title').append('text').st({fill: '#648DCF'}).translate([60, 36]) + var rSel = c.svg.append('g.small-title').append('text').st({fill: '#BC5454'}).translate([170, 36]) + + function drawScenario(scenarioIndex){ + var rVotes = 0 + stateSel.at({fill: d => { + var isR = model.maps[scenarioIndex*states.length + d.state.i] > 5000 + if (isR) rVotes += +d.state.votes + return isR ? '#BC5454' : '#648DCF' + }}) + + dSel.text('Biden ' + (538 - rVotes)) + rSel.text('Trump ' + rVotes) + } + + var rv = {drawScenario} + model.map = rv + return rv +} + + +function initCorScatter(){ + var sel = d3.select('.cor-scatter').html('').st({margin: '0px auto'}) + + var c = d3.conventions({ + sel: sel, + width: 500, + height: 500, + margin: {bottom: 50, right: 20, top: 50} + }) + + c.svg.append('g.axis').append('text.small-title') + .text('Pairwise Correlations Between States') + .at({textAnchor: 'middle', x: c.width/2, y: -20}) + .st({fontSize: 14}) + + + c.x.domain([-.5, 1]) + c.y.domain([-.5, 1]) + + c.svg.append('rect') + .at({width: c.width, height: c.height, fill: '#eee'}) + + c.x.interpolate(d3.interpolateRound) + c.y.interpolate(d3.interpolateRound) + c.xAxis.tickFormat(d3.format('.2f')).ticks(10).tickSize(c.height) + c.yAxis.tickFormat(d3.format('.2f')).ticks(10).tickSize(c.height) + + d3.drawAxis(c) + c.svg.selectAll('.tick').classed('bold', d => d == 0) + + + var xAxisSel = c.svg.select('.x').translate(0, 0) + var yAxisSel = c.svg.select('.y').translate(c.width, 0) + + addAxisLabel(c, 'Economist Correlation', '538 Correlation') + xAxisSel.select('.label').translate(c.height, 1) + yAxisSel.select('.label').at({y: -c.width + 10}) + + modelEco.pairs.forEach((d, i) => { + d.cor538 = model538.pairs[i].cor + }) + + var circleSel = c.svg.appendMany('circle', modelEco.pairs.filter(d => d.indexA < d.indexB)) + .translate((d, i) => [c.x(d.cor), c.y(d.cor538)]) + .at({r: 1.5, fillOpacity: 0, stroke: '#444', opacity: d => d.cor < .99 ? 1 : 0}) + .call(d3.attachTooltip) + .on('mouseover', pair => { + var i = pair.pairIndex + + var corEco = modelEco.pairs[i].cor + var cor538 = model538.pairs[i].cor + + ttSel.html(` + ${pair.strA}-${pair.strB} +
+ ${d3.format('+.2f')(corEco)} + Economist correlation +
+ + ${d3.format('+.2f')(cor538)} + 538 correlation + `) + + globalSetPair(pair) + }) + + var activeCircle = c.svg.append('circle') + .at({r: 0, stroke: '#000', r: 5, strokeWidth: 2, pointerEvents: 'none', fill: 'none'}) + + function setPair(pair){ + var i = pair.pairIndex + activeCircle + .translate([c.x(modelEco.pairs[i].cor), c.y(model538.pairs[i].cor)]) + .at({r: 0}) + + circleSel + .at({ + r: d => d.canonicalStr == pair.canonicalStr ? 3 : 1.5, + }) + .classed('active', d => d.canonicalStr == pair.canonicalStr) + + } + + return {setPair} +} + +function initStateSm(){ + var sel = d3.select('.state-sm').html('') + + // copy(_.sortBy(modelEco.stateData.map((d, i) => ({i, v: d3.mean(d)})), d => d.v).map(d => d.i)) + var meanOrder = [7, 11, 46, 4, 19, 20, 34, 47, 39, 6, 8, 31, 37, 14, 32, 21, 5, 45, 23, 30, 22, 48, 33, 38, 9, 27, 3, 10, 12, 35, 43, 0, 40, 24, 26, 25, 16, 18, 15, 42, 29, 41, 1, 2, 44, 17, 28, 13, 36, 49, 50] + + var stateSel = d3.select('.state-sm').html('') + .appendMany('div.state', meanOrder.map(i => states[i])) + .st({width: 230}) + .st({margin: 5, marginTop: 10, marginBottom: 10, fontSize: 14, height: 66}) + + + var drawQueue = [] + stateSel.each(addToDrawQueue) + function addToDrawQueue(symptom, i){ + drawQueue.push(() => drawState(symptom, i, d3.select(this))) + } + + function drawNext(){ + d3.range(1).forEach(() => { + var fn = drawQueue.shift() + if (fn) fn() + }) + + if (drawQueue.length) window.drawNextTimeout = d3.timeout(drawNext, 50) + } + if (window.drawNextTimeout) window.drawNextTimeout.stop() + d3.timeout(drawNext, 100) + + + function drawState(state, i, sel){ + var c = d3.conventions({ + sel: sel.append('div'), + height: 50, + width: 200, + layers: 'sc', + margin: {top: 0, left: 10, right: 0, bottom: 10} + }) + + var stateIndex = state.stateIndex + if (!model538.stateData[stateIndex]){ + model538.stateData[stateIndex] = d3.range(40000).map(i => model538.maps[i*states.length + stateIndex]/10000) + } + if (!modelEco.stateData[stateIndex]){ + modelEco.stateData[stateIndex] = d3.range(40000).map(i => modelEco.maps[i*states.length + stateIndex]/10000) + } + + var nBuckets = 200 + + var h538 = d3.range(nBuckets + 1).map(i => ({v: 0, i})) + model538.stateData[state.stateIndex].forEach(d => { + h538[Math.round(d*nBuckets)].v++ + }) + + var hEco = d3.range(nBuckets + 1).map(i => ({v: 0, i})) + modelEco.stateData[state.stateIndex].forEach(d => { + hEco[Math.round(d*nBuckets)].v++ + }) + + + c.x.domain([0, nBuckets]) + c.y.domain([0, 4000]) + + c.svg.append('rect') + .at({width: c.width, height: c.height, fill: '#eee'}) + + c.x.interpolate(d3.interpolateRound) + c.y.interpolate(d3.interpolateRound) + c.xAxis + .tickFormat(d => { + return d3.format('.0%')(d/nBuckets) + (d == 100 ? ' R' : '') + return d3.format('.0%')(d/nBuckets) + ' R' + + d3.format('.0%')((.5 - d/nBuckets)*2) + (d/nBuckets < .5 ? ' D' : ' R') + + }) + .ticks(5) + .tickSize(c.height) + + c.yAxis.tickFormat(d => (d/1000 + 'k').replace('0k', '')).ticks(2).tickSize(c.width) + + d3.drawAxis(c) + c.svg.selectAll('.tick').classed('bold', d => d == 100) + c.svg.selectAll('.tick text').at({fontWeight: 400}) + + // c.svg.select('.x text').at({textAnchor: 'start', x: -10}) + + c.svg.append('text.small-title').text(state.str) + .translate([c.width - 20, 15]) + .st({fontWeight: 600}) + + var xAxisSel = c.svg.select('.x').translate(0, 0) + var yAxisSel = c.svg.select('.y').translate(c.width, 0) + + var line = d3.line() + .x((d, i) => c.x(i)) + .y(d => c.y(d.v)) + .defined(d => d.v > 0) + + c.svg.append('path') + .at({d: line(hEco), stroke: '#C2190F', fill: 'none'}) + + c.svg.append('path') + .at({d: line(h538), stroke: '#000', fill: 'none'}) + + + var ctx = c.layers[1] + + ctx.fillStyle = '#000' + ctx.beginPath() + h538.forEach(d => { + if (d.v == 0) return + if (d.v > 10) return + ctx.fillRect(c.x(d.i), c.height, 1, 1) + }) + ctx.fill() + + ctx.fillStyle = '#C2190F' + ctx.beginPath() + hEco.forEach(d => { + if (d.v == 0) return + if (d.v > 10) return + ctx.fillRect(c.x(d.i), c.height, 1, 1) + }) + ctx.fill() + } + +} + + +function calcIndex2Cluster(model538, modelEco){ + states.forEach(state => { + state.position538 = [] + state.positionEco = [] + }) + + model538.pairs.forEach(d => states[d.indexA].position538[d.indexB] = d.cor) + modelEco.pairs.forEach(d => states[d.indexA].positionEco[d.indexB] = d.cor) + + states.forEach(d => d.position = d.position538.concat(d.positionEco)) + states.forEach(d => d.position = d.positionEco) + states.forEach(d => d.position = d.position538) + + var index2cluster = [] + var clusteredStates = cluster = hcluster() + .distance('angular') + .linkage('avg') + .data(states) + .orderedNodes() + .forEach((d, i) => { + index2cluster[d.stateIndex] = i + }) + + return index2cluster +} + + +function addAxisLabel(c, xText, yText){ + if (xText){ + c.svg.select('.x').append('g') + .translate([c.width/2, 35]) + .append('text.label') + .text(xText) + .at({textAnchor: 'middle'}) + .st({fill: '#000'}) + } + + if (yText){ + c.svg.select('.y') + .append('g') + .translate([-50, c.height/2]) + .append('text.label') + .text(yText) + .at({textAnchor: 'middle', transform: 'rotate(-90)'}) + .st({fill: '#000'}) + } +} diff --git a/source/forecast-correlation/states.csv b/source/forecast-correlation/states.csv new file mode 100644 index 00000000..265926bc --- /dev/null +++ b/source/forecast-correlation/states.csv @@ -0,0 +1,52 @@ +abv,name,votes +AK,Alaska,3 +AL,Alabama,9 +AR,Arkansas,6 +AZ,Arizona,11 +CA,California,55 +CO,Colorado,9 +CT,Connecticut,7 +DC,District of Columbia,3 +DE,Delaware,3 +FL,Florida,29 +GA,Georgia,16 +HI,Hawaii,4 +IA,Iowa,6 +ID,Idaho,4 +IL,Illinois,20 +IN,Indiana,11 +KS,Kansas,6 +KY,Kentucky,8 +LA,Louisiana,8 +MA,Massachusetts,11 +MD,Maryland,10 +ME,Maine,4 +MI,Michigan,16 +MN,Minnesota,10 +MO,Missouri,10 +MS,Mississippi,6 +MT,Montana,3 +NC,North Carolina,15 +ND,North Dakota,3 +NE,Nebraska,5 +NH,New Hampshire,4 +NJ,New Jersey,14 +NM,New Mexico,5 +NV,Nevada,6 +NY,New York,29 +OH,Ohio,18 +OK,Oklahoma,7 +OR,Oregon,7 +PA,Pennsylvania,20 +RI,Rhode Island,4 +SC,South Carolina,9 +SD,South Dakota,3 +TN,Tennessee,11 +TX,Texas,38 +UT,Utah,6 +VA,Virginia,13 +VT,Vermont,3 +WA,Washington,12 +WI,Wisconsin,10 +WV,West Virginia,5 +WY,Wyoming,3 \ No newline at end of file diff --git a/source/forecast-correlation/style.css b/source/forecast-correlation/style.css new file mode 100644 index 00000000..cb1e01a0 --- /dev/null +++ b/source/forecast-correlation/style.css @@ -0,0 +1,174 @@ +html{ + min-width: 1200px; + +} + + +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; + font-family: monospace; + font-size: 12px; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.domain{ + display: none; +} + +text{ + pointer-events: none; + /*text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;*/ +} + + +.col{ + display: inline-block; + width: 550px; +} + +.abv{ + font-family: monospace; + font-size: 10px; + fill: #555; +} + +.abv.active{ + fill: #000; + font-weight: 600; + font-size: 12px; +} + + + +rect.active{ + stroke: #000; + stroke-width: 2; +} + + +.axis text, .state-sm .axis text{ + font-weight: 400; +} +.axis text, .small-title{ + font-family: monospace; + font-size: 10px; + fill: #555; +} + +.axis line{ + opacity: .1; +} +.axis .tick.bold line{ + opacity: .3; +} +.axis .tick.bold text{ + fill: #000; + font-weight: 600; +} + +.matrix .axis text{ + font-size: 14px !important; + font-weight: 600; +} + +.small-title, .axis text.small-title{ + font-size: 12px; + margin-left: 23px; + margin-bottom: -20px; + fill: #000; + font-weight: 600; +} + +.label{ +} + + +#notes{ + font-family: sans-serif; + font-size: 12px; + opacity: .7; + margin-top: 60px; +} + +.graph{ + min-height: 800px +} + + +.map, .scatter{ + display: inline-block; + vertical-align: top; + position: relative; + left: 30px; +} +.map{ + left: 10px; + top: -20px; + z-index: -20; +} + + +.cor-scatter circle.active{ + /*stroke-width: 8px;*/ + /*opacity: 1;*/ + /*stroke-width: 3;*/ + fill: #ddd; + fill-opacity: 1; + stroke-width: 4px; + stroke: #000; + +} + +.u-538{ + border-bottom: 2px solid #000; +} +.u-eco{ + border-bottom: 2px solid #C2190F; +} + +.neg-cor{ + background: #DFB0D2; + padding-left: 2px; + padding-right: 2px; +} + +.state-sm{ + display: flex; + flex-flow: column wrap; + height: 867px; + /*overflow-x: hidden;*/ + position: relative; + left: -30px; + overflow-y: visible; + width: 1200px; +} + +.state-sm > div:last-child { + position: absolute; + bottom: -81px; + right: 0px; +} diff --git a/source/forecast-correlation/todo b/source/forecast-correlation/todo new file mode 100644 index 00000000..effeb839 --- /dev/null +++ b/source/forecast-correlation/todo @@ -0,0 +1,29 @@ +- correlation legend +- text +- words + + +# matrix +x add lock + + +# scatter +x title +x regression line +x link hover + + +# parms [meh] +- default +- set + + +# map +x link hover +x load states + +# cor scatter + + + +# state sm diff --git a/source/fox-forecast/script.js b/source/fox-forecast/script.js new file mode 100644 index 00000000..6633ccbb --- /dev/null +++ b/source/fox-forecast/script.js @@ -0,0 +1,85 @@ +d3.loadData('states.csv', 'https://roadtolarissa.com/data/fox-2020-wp.csv', (err, res) => { + window.states = res[0] + window.tidy = res[1] + + tidy.forEach(d => { + d.date = new Date(d.last_update) + d.rProb = +d.rProb + }) + + window.timeScale = d3.scaleTime() + .domain(d3.extent(tidy, d => d.date)) + .interpolate(d3.interpolateRound) + + var byState = _.sortBy(d3.nestBy(tidy, d => d.state), d => d.key) + byState = _.sortBy(byState, d => { + var score = d[60].rProb < .02 || d[60].rProb > .98 ? 1 : 0 + if (d.key == 'UT') score = 0 + if (d.key == 'KS') score = 0 + if (d.key == 'ID') score = 0 + if (d.key == 'IN') score = 0 + if (d.key == 'HI') score = 1 + + return score + }) + + var stateSel = d3.select('.state-sm').html('') + .appendMany('div.state', byState.concat([0,0,0,0,0,0])) + .each(drawState) +}) + + +function drawState(state){ + if (!state) return; + var sel = d3.select(this) + // sel.append('div.name').text(state.key) + + var c = d3.conventions({ + sel: sel.append('div'), + height: 100, + margin: {bottom: 30, right: 20} + }) + + var m = _.find(states, {abv: state.key}) + c.svg.append('g.axis').append('text').text(m.name) + .at({x: c.width/2, textAnchor: 'middle', y: -5}) + .st({fill: '#000'}) + + timeScale.range(c.x.range()) + + c.x = timeScale + c.y.interpolate(d3.interpolateRound) + c.xAxis.scale(c.x).ticks(4) + c.yAxis.ticks(4).tickFormat(d3.format('.0%')).tickSize(c.width) + + d3.drawAxis(c) + var yAxisSel = c.svg.select('.y').translate(c.width, 0) + + c.svg.selectAll('.x text').text(function(){ + var sel = d3.select(this) + var text = sel.text() + if (text[0] == 0) text = text.slice(1, 20) + text = text.replace('Wed 04', 'Wed') + return text + }) + + var line = d3.line() + .x(d => c.x(d.date)) + .y(d => c.y(d.rProb)) + .curve(d3.curveStepAfter) + + + c.svg.append('path.wp-line') + .at({ + d: line(state), + stroke: '#B4152A', + }) + + c.svg.append('path.wp-line') + .at({ + d: line.y(d => c.y(1 - d.rProb))(state), + stroke: '#1F3664', + }) + + +} \ No newline at end of file diff --git a/source/fox-forecast/states.csv b/source/fox-forecast/states.csv new file mode 100644 index 00000000..5eee118b --- /dev/null +++ b/source/fox-forecast/states.csv @@ -0,0 +1,52 @@ +abv,name,votes +AK,Alaska,3 +AL,Alabama,9 +AR,Arkansas,6 +AZ,Arizona,11 +CA,California,55 +CO,Colorado,9 +CT,Connecticut,7 +DC,District of Columbia,3 +DE,Delaware,3 +FL,Florida,29 +GA,Georgia,16 +HI,Hawaii,4 +IA,Iowa,6 +ID,Idaho,4 +IL,Illinois,20 +IN,Indiana,11 +KS,Kansas,6 +KY,Kentucky,8 +LA,Louisiana,8 +MA,Massachusetts,11 +MD,Maryland,10 +ME,Maine,4 +MI,Michigan,16 +MN,Minnesota,10 +MO,Missouri,10 +MS,Mississippi,6 +MT,Montana,3 +NC,North Carolina,15 +ND,North Dakota,3 +NE,Nebraska,5 +NH,New Hampshire,4 +NJ,New Jersey,14 +NM,New Mexico,5 +NV,Nevada,6 +NY,New York,29 +OH,Ohio,18 +OK,Oklahoma,7 +OR,Oregon,7 +PA,Pennsylvania,20 +RI,Rhode Island,4 +SC,South Carolina,9 +SD,South Dakota,3 +TN,Tennessee,11 +TX,Texas,38 +UT,Utah,6 +VA,Virginia,13 +VT,Vermont,3 +WA,Washington,12 +WI,Wisconsin,10 +WV,West Virginia,5 +WY,Wyoming,3 \ No newline at end of file diff --git a/source/fox-forecast/style.css b/source/fox-forecast/style.css new file mode 100644 index 00000000..71e4ac90 --- /dev/null +++ b/source/fox-forecast/style.css @@ -0,0 +1,104 @@ + + + +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; + font-family: monospace; + font-size: 12px; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.domain{ + display: none; +} + +text{ + pointer-events: none; + /*text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;*/ +} + +.axis text, .title{ + font-family: monospace; + font-size: 11px; + fill: #666; +} + +.axis line{ + opacity: .1; +} + + + + +.state-sm{ + display: flex; + flex-flow: row wrap; + /*overflow-x: hidden;*/ + position: relative; + overflow-y: visible; + width: 100%; + justify-content: space-between; +} + +.state{ + /*flex: 1 1 /0px;*/ + /*flex-basis: 100%;*/ + flex: 1; + min-width: 270px; +} + +/*.state-sm > div:last-child { + position: absolute; + bottom: -81px; + right: 0px; +} +*/ + + +#notes{ + font-family: sans-serif; + font-size: 12px; + opacity: .7; + margin-top: 60px; +} + + +.wp-line{ + stroke-width: 2; + fill: none; +} + +@media (min-width: 1150px){ + .state-sm{ + margin-left: -175px; + width: 1100px; + } + + .state{ + min-width: 270px; + } +} \ No newline at end of file diff --git a/source/homepage/index-div-grid.html b/source/homepage/index-div-grid.html new file mode 100644 index 00000000..ab94ab0a --- /dev/null +++ b/source/homepage/index-div-grid.html @@ -0,0 +1,704 @@ + + + + + + + + roadtolarissa + + + + + + + +
+ +
+ Adam Pearce + + + + + + + + + + + + +
+
+ + + +

libraries

+ + +

talks

+ + +

posts

+
+ Literate Blogging + 4096 Paths Into MSI + Buy High and Sell Low + Hackable Hot Reloading + Top Three Movies + Same-Sex Marriage Bans + Aaronson Oracle + 2017 Chart Diary + D3 to MP4 + 2017 Worlds Group + Hurricane How-To + The 64 Ways into MSI + Kindle Tracker + The Ways Out of World Group Stage + Projecting Land + NBA Win/Loss Records + Point Differentials of 2015 NBA Games + Golden State's 2015 Point Differentials + Lyric Typing + Stacked Bump Charts + Data Exploration With D3 + SVG Path Strings + Coloring Maps + Convex Hulls + Dragon Curve + Golf Paths + Drawdown + 215 Teeth / 1008 Beats + Even Fewer Lambdas + Population Division + Making Music With D3 + Twisters + NBA Draft + Meteor Map + Film Strips + Whale Words + Unemployment Rates + Zoomable Sierpinski Triangle + White House Petition Signatures + Next Project + Redditgraphs Retrospective + Reddit Comment Visualizer + Yglesias on Amazon's P/E Ratio + Speed Issues Goko Dominion + Connect 4 AI: How It Works + Maximin Connect 4 Completed + First Post + + + + + + + + + + + + + + + + + + + + diff --git a/source/homepage/index-svg.html b/source/homepage/index-svg.html new file mode 100644 index 00000000..f6464a2f --- /dev/null +++ b/source/homepage/index-svg.html @@ -0,0 +1,387 @@ + + + + + + + + roadtolarissa + + + + + + + +
+ +
+ Adam Pearce + + + + + + + + + + + + +
+
+ + + +

libraries

+ + +

talks

+ + +

posts

+
+ Literate Blogging + 4096 Paths Into MSI + Buy High and Sell Low + Hackable Hot Reloading + Top Three Movies + Same-Sex Marriage Bans + Aaronson Oracle + 2017 Chart Diary + D3 to MP4 + 2017 Worlds Group + Hurricane How-To + The 64 Ways into MSI + Kindle Tracker + The Ways Out of World Group Stage + Projecting Land + NBA Win/Loss Records + Point Differentials of 2015 NBA Games + Golden State's 2015 Point Differentials + Lyric Typing + Stacked Bump Charts + Data Exploration With D3 + SVG Path Strings + Coloring Maps + Convex Hulls + Dragon Curve + Golf Paths + Drawdown + 215 Teeth / 1008 Beats + Even Fewer Lambdas + Population Division + Making Music With D3 + Twisters + NBA Draft + Meteor Map + Film Strips + Whale Words + Unemployment Rates + Zoomable Sierpinski Triangle + White House Petition Signatures + Next Project + Redditgraphs Retrospective + Reddit Comment Visualizer + Yglesias on Amazon's P/E Ratio + Speed Issues Goko Dominion + Connect 4 AI: How It Works + Maximin Connect 4 Completed + First Post + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/homepage/list-bin.js b/source/homepage/list-bin.js new file mode 100644 index 00000000..1e26d575 --- /dev/null +++ b/source/homepage/list-bin.js @@ -0,0 +1,123 @@ +var {jp, d3, _, request, fs} = require('scrape-stl') + + +var topProjects = ` +subway-crisis +spoofing +vegas-guns +who-marries-whom +race-class-white-and-black +houston-cries-for-help +what-is-code +some-trains-dont-run-at-all +2017-chart-diary +tax-calculator +twisters +you-draw-obama-legacy +new-geography-of-prisons +nba-win-loss +hot-reload +dumb-lawyer +uk-splatter +nba-minutes +nine-percent-of-america +europe-right-wing +2018-chart-diary +hurricane-how-to +uncertainty-over-space +symptoms-search-trends +fill-in-the-blank +private-and-fair +grokking +scaling-monosemanticity +circuit-tracing +`.split('\n') + + + +var templateHTML = ` + + + + + + + roadtolarissa + + + + + + + + + + +
+
+ +
+ Adam Pearce + + github + + + twitter + + + email + + + rss + +
+
+ + + +
+` + + + +// d3.csv('https://docs.google.com/spreadsheets/d/e/2PACX-1vTGqTVxJ_yfhMaRRRQ1BjvmbCEFrw57kAC5d6iK9gdEiaL_MKEAi1r6eMQ_9QRN6xpDdO-MAbbFKqqQ/pub?output=csv', (err, res) => { +d3.csv('https://www.googleapis.com/drive/v3/files/1xUvK5PGo8XPqARJIvXRnn71lJSL8CwjvjUWB9Jcy0Ho/export?mimeType=text/csv&key=AIzaSyAT-ALGW_bcmcvNs1dPgcV7fF6tR1vKY44', (err, res) => { + var projects = res + + projects.forEach(d => { + d.year = d.date.split('-')[0] + d.isTop = topProjects.includes(d.slug) + d.img = `https://roadtolarissa.com/homepage-list/thumb-img/${d.slug}.jpg` + }) + + updateHTML(projects) +}) + + +function updateHTML(projects){ + + projects.sort((a, b) => a.date > b.date ? -1 : 1) + + var boringHTML = jp.nestBy(projects, d => d.year).map(year => { + return ` +
+
${year.key}
+ ${year.map(d => ` + +
+ ${d.slug.toLowerCase().replace(/-/g, ' ').trim()} +
+ `).join('')} +
+ ` + }).join('') + + fs.writeFileSync(__dirname + '/../index.html', templateHTML.replace('boringHTML', boringHTML)) + +} + + + + diff --git a/source/homepage/list.css b/source/homepage/list.css new file mode 100644 index 00000000..daf5d987 --- /dev/null +++ b/source/homepage/list.css @@ -0,0 +1,133 @@ +html{ +} + + + +#boring{ + margin-top: 30px; + font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif; + line-height: 1.9em; +} + +.slug{ + margin-left: 20px; + text-decoration: none; + pointer-events: none; + position: relative; + top: -20px; +} + +.row{ + cursor: pointer; + display: block; + text-decoration: none; + white-space: nowrap; + font-family: monospace; + /*font-family: Consolas, 'Lucida Console', monospace; */ + display: inline-block; + width: 370px; + font-size: 14px; + + user-select: none; + -webkit-tap-highlight-color: transparent; + +} +.row:hover .slug{ + text-decoration: underline; +} +.row:hover .month, .row:hover .year{ + opacity: 1 !important; +} + +.row:hover .thumbnail{ + border-bottom-width: 2px; + margin-bottom: -1px; +} + +.row .slug{ + color: #333; +} +.row.is-top .slug{ + color: #f0f; + font-weight: 600; +} + +.row.is-top .thumbnail{ + /*border: 1px solid purple;*/ +} + + +.thumbnail{ + background-size: cover; + background-position: center center; + width: 100px; + height: 50px; + display: inline-block; + border: 1px solid #333; + position: relative; + /*top: 18px;*/ + /*margin-left: 20px;*/ +} + +.month, .year{ + color: #999; + font-size: 12px; +} + +.header{ + font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif; + font-family: monospace; +} + +.header a, .header span{ +/* opacity: 1; + font-weight: 300; +*/} + +.header-right{ + margin-right: 32px; +} + + + +html{ + background: #f1f1f1;; +} + +.year-label{ + position: relative; + color: #333; + font-family: monospace; + left: -80px; + margin-bottom: 20px; + height: 0px; + top: 30px; + font-size: 12px; +} + +.year{ + margin-bottom: 40px; +} + +@media (max-width: 930px){ + + .row{ + width: 100%; + } + + .year-label{ + top: -20px; + left: 0px; + font-size: 14px; + } + + .year:first-child .year-label{ + display: none; + } + + .header-right{ + margin-right: 0px; + } + +} + diff --git a/source/homepage/list.js b/source/homepage/list.js new file mode 100644 index 00000000..fff01f05 --- /dev/null +++ b/source/homepage/list.js @@ -0,0 +1,69 @@ +var topProjects = ` +subway-crisis +spoofing +vegas-guns +who-marries-whom +race-class-white-and-black +houston-cries-for-help +what-is-code +some-trains-dont-run-at-all +2017-chart-diary +tax-calculator +twisters +you-draw-obama-legacy +new-geography-of-prisons +nba-win-loss +hot-reload +dumb-lawyer +uk-splatter +nba-minutes +nine-percent-of-america +europe-right-wing +2018-chart-diary +hurricane-how-to +uncertainty-over-space +fill-in-the-blank +`.split('\n') + + +var projects = nytProjects.concat(otherProjects) +projects.forEach((d, i) => { + if (!d.img) d.img = '/images/thumbnails/' + d.slug + '.png' + + d.isTop = topProjects.includes(d.slug) +}) + + +// boring +!(function(){ + projects.forEach(d => { + d.year = d.date.split('-')[0] + }) + + projects.sort((a, b) => a.date > b.date ? -1 : 1) + var byYear = d3.nestBy(projects, d => d.year) + + var sel = d3.select('#boring').html('') + + var yearSel = sel.appendMany('div.year', byYear) + + yearSel.append('div.year-label').text(d => d.key) + + var linkSel = yearSel.appendMany('a.row', d => d) + .attr('href', d => d.url.replace('https://roadtolarissa.com', '')) + .attr('target', '_blank') + .classed('is-top', d => d.isTop) + + linkSel.append('div.thumbnail') + .st({ + backgroundImage: d => `url(${d.img})`, + }) + + linkSel.append('a.slug') + .text(d => d.slug.toLowerCase().replace(/-/g, ' ').trim()) + .at({href: d => d.url}) +})() + + + + diff --git a/source/homepage/projects.js b/source/homepage/projects.js new file mode 100644 index 00000000..533d548b --- /dev/null +++ b/source/homepage/projects.js @@ -0,0 +1,538 @@ +var nytProjects = [ +{ + "slug": "republicans-state-politics", + "date": "2018-11", + "url": "https://www.nytimes.com/interactive/2018/11/10/upshot/republicans-dominate-state-politics-but-democrats-made-a-dent.html", + "img": "https://static01.nyt.com/images/2018/11/09/us/up-state-houses-promo-1541803667826/up-state-houses-promo-1541803667826-master1050-v2.jpg" +}, +{ + "slug": "possible-midterm-recounts", + "date": "2018-11", + "url": "https://www.nytimes.com/interactive/2018/11/10/us/elections/2018-possible-midterm-recounts-georgia-florida-arizona.html", + "img": "https://static01.nyt.com/images/2018/11/09/us/2018-possible-midterm-recounts-georgia-florida-arizona-promo-1541820719668/2018-possible-midterm-recounts-georgia-florida-arizona-promo-1541820719668-master1050-v2.png" +}, +{ + "slug": "tracking-hurricane-michael", + "date": "2018-10", + "url": "https://www.nytimes.com/interactive/2018/10/08/us/tracking-hurricane-michael-path.html", + "img": "https://static01.nyt.com/images/2018/10/08/us/tracking-hurricane-michael-path-promo-1539020902270/tracking-hurricane-michael-path-promo-1539020902270-master1050-v98.png" +}, +{ + "slug": "hurricane-florence-flooding", + "date": "2018-09", + "url": "https://www.nytimes.com/interactive/2018/09/18/us/hurricane-florence-flooding.html", + "img": "https://static01.nyt.com/images/2018/09/18/us/hurricane-florence-promo-1537304616488/hurricane-florence-promo-1537304616488-master1050.jpg" +}, +{ + "slug": "hurricane-florence-map", + "date": "2018-09", + "url": "https://www.nytimes.com/interactive/2018/09/13/us/hurricane-florence-impact-damage-map.html", + "img": "https://static01.nyt.com/images/2018/09/11/us/hurricane-florence-tracking-map-promo-1536673849664/hurricane-florence-tracking-map-promo-1536673849664-master1050-v7.png" +}, +{ + "slug": "live-polling", + "date": "2018-09", + "url": "https://www.nytimes.com/interactive/2018/upshot/elections-polls.html", + "img": "https://i.imgur.com/PVlcwbo.png" +}, +{ + "slug": "uncontested-primaries", + "date": "2018-06", + "url": "https://www.nytimes.com/interactive/2018/06/30/us/elections/representatives-running-unopposed-uncontested-primaries.html", + "img": "https://static01.nyt.com/images/2018/09/04/us/representatives-running-unopposed-uncontested-primaries-promo-1536116329367/representatives-running-unopposed-uncontested-primaries-promo-1536116329367-master1050.jpg" +}, +{ + "slug": "primaries-turnout", + "date": "2018-06", + "url": "https://www.nytimes.com/interactive/2018/06/25/us/politics/midterm-primaries-voter-turnout.html", + "img": "https://static01.nyt.com/images/2018/06/25/us/midterm-primaries-voter-turnout-promo-1529957148807/midterm-primaries-voter-turnout-promo-1529957148807-master1050.png" +}, +{ + "slug": "lebron-nba-finals", + "date": "2018-06", + "url": "https://www.nytimes.com/interactive/2018/06/08/sports/basketball/lebron-nba-finals.html", + "img": "https://static01.nyt.com/images/2018/06/08/sports/lebron-promo/lebron-promo-master1050.jpg" +}, + + { + "slug": "subway-crisis", + "date": "2018-05", + "url": "https://www.nytimes.com/interactive/2018/05/09/nyregion/subway-crisis-mta-decisions-signals-rules.html", + "img": "/images/thumbnails/train-heat.png" + }, + { + "slug": "mobility-animation", + "date": "2018-03", + "url": "https://www.nytimes.com/interactive/2018/03/27/upshot/make-your-own-mobility-animation.html", + "img": "https://static01.nyt.com/images/2018/03/26/upshot/make-your-own-mobility-animation-1521838318116/make-your-own-mobility-animation-1521838318116-master1050-v6.png" + }, + { + "slug": "arctic-ice-maximum", + "date": "2018-03", + "url": "https://www.nytimes.com/interactive/2018/03/23/climate/arctic-ice-maximum.html", + "img": "https://static01.nyt.com/images/2018/03/23/climate/arctic-ice-maximum-1521835992567/arctic-ice-maximum-1521835992567-master1050.jpg" + }, + { + "slug": "race-class-white-and-black", + "date": "2018-03", + "url": "https://www.nytimes.com/interactive/2018/03/19/upshot/race-class-white-and-black-men.html", + "img": "https://static01.nyt.com/images/2018/03/20/us/race-class-white-and-black-men-promo2/race-class-white-and-black-men-promo-master1050.png" + }, + { + "slug": "tax-calculator", + "date": "2017-12", + "url": "https://www.nytimes.com/interactive/2017/12/17/upshot/tax-calculator.html", + "img": "https://static01.nyt.com/images/2017/12/15/upshot/will-your-taxes-go-up-or-down-calculator-for-the-new-tax-plan-republican-1513403957560/will-your-taxes-go-up-or-down-calculator-for-the-new-tax-plan-republican-1513403957560-master1050.png" + }, + { + "slug": "every-tax-cut", + "date": "2017-11", + "url": "https://www.nytimes.com/interactive/2017/11/15/us/politics/every-tax-cut-in-the-house-tax-bill.html", + "img": "https://static01.nyt.com/images/2017/11/15/us/politics/every-tax-cut-in-the-house-tax-bill-1510688697984/every-tax-cut-in-the-house-tax-bill-1510688697984-master1050.png" + }, + { + "slug": "bump-stock-vegas", + "date": "2017-10", + "url": "https://www.nytimes.com/interactive/2017/10/04/us/bump-stock-las-vegas-gun.html", + "img": "https://static01.nyt.com/images/2017/10/04/us/bump-stock-las-vegas-gun-1507131222062/bump-stock-las-vegas-gun-1507131222062-master1050-v2.png" + }, + { + "slug": "vegas-guns", + "date": "2017-10", + "url": "https://www.nytimes.com/interactive/2017/10/02/us/vegas-guns.html", + "img": "https://static01.nyt.com/images/2017/10/02/us/vegas-sounds/vegas-sounds-master1050-v3.png" + }, + { + "slug": "arctic-sea-ice", + "date": "2017-09", + "url": "https://www.nytimes.com/interactive/2017/09/22/climate/arctic-sea-ice-shrinking-trend-watch.html", + "img": "https://static01.nyt.com/images/2017/09/22/climate/arctic-sea-ice-shrinking-trend-watch-1506090301026/arctic-sea-ice-shrinking-trend-watch-1506090301026-master1050-v5.jpg" + }, + { + "slug": "hurricane-irma-records", + "date": "2017-09", + "url": "https://www.nytimes.com/interactive/2017/09/09/us/hurricane-irma-records.html", + "img": "https://static01.nyt.com/images/2017/09/09/us/record-hurricane/record-hurricane-master1050.png" + }, + { + "slug": "hurricane-irma-map", + "date": "2017-09", + "url": "https://www.nytimes.com/interactive/2017/09/05/us/hurricane-irma-map.html", + "img": "https://i.imgur.com/Itl0sAI.png" + }, + { + "slug": "houston-cries-for-help", + "date": "2017-08", + "url": "https://www.nytimes.com/interactive/2017/08/30/us/houston-flood-rescue-cries-for-help.html", + "img": "https://static01.nyt.com/images/2017/08/30/us/houston-flood-rescue-cries-for-help-1504145710714/houston-flood-rescue-cries-for-help-1504145710714-master1050.jpg" + }, + { + "slug": "hurricane-harvey-maps", + "date": "2017-08", + "url": "https://www.nytimes.com/interactive/2017/08/28/us/houston-maps-hurricane-harvey.html", + "img": "https://static01.nyt.com/images/2017/08/28/us/houston-maps-hurricane-harvey-1503942493144/houston-maps-hurricane-harvey-1503942493144-master1050-v4.jpg" + }, + { + "slug": "hurricane-harvey-rain", + "date": "2017-08", + "url": "https://www.nytimes.com/interactive/2017/08/24/us/hurricane-harvey-texas.html", + "img": "/images/thumbnails/harvey-rain.png" + }, + { + "slug": "affirmative-action", + "date": "2017-08", + "url": "https://www.nytimes.com/interactive/2017/08/24/us/affirmative-action.html", + "img": "https://static01.nyt.com/images/2017/08/03/us/affirmative-action-in-colleges-and-representation-of-blacks-and-hispanics-1503525136789/affirmative-action-in-colleges-and-representation-of-blacks-and-hispanics-1503525136789-master1050-v3.png" + }, + { + "slug": "trump-rebukes-charlottesville", + "date": "2017-08", + "url": "https://www.nytimes.com/interactive/2017/08/22/us/politics/trump-rebukes-charlottesville.html", + "img": "https://static01.nyt.com/images/2017/08/21/us/trump-charlottesville-1503370660230/trump-charlottesville-1503370660230-master1050.png" + }, + { + "slug": "game-of-thrones-chart", + "date": "2017-08", + "url": "https://www.nytimes.com/interactive/2017/08/09/upshot/game-of-thrones-chart.html", + "img": "https://static01.nyt.com/images/2017/08/16/insider/game-of-thrones-chart-1502212116886/game-of-thrones-chart-1502212116886-master1050-v2.png" + }, + { + "slug": "some-trains-dont-run-at-all", + "date": "2017-08", + "url": "https://www.nytimes.com/interactive/2017/08/07/nyregion/new-yorks-subways-are-not-just-delayed-some-trains-dont-run-at-all.html", + "img": "https://i.imgur.com/7EBCITA.png" + }, + { + "slug": "more-extreme-summer-heat", + "date": "2017-07", + "url": "https://www.nytimes.com/interactive/2017/07/28/climate/more-frequent-extreme-summer-heat.html", + "img": "https://i.imgur.com/dnMA19k.png" + }, + { + "slug": "BASEBALL-LAUNCH-ANGLE", + "date": "2017-07", + "url": "https://www.nytimes.com/interactive/2017/07/09/sports/baseball/BASEBALL-LAUNCH-ANGLE.html", + "img": "https://static01.nyt.com/images/2017/07/09/sports/baseball/BASEBALL-LAUNCH-ANGLE-1499537467366/BASEBALL-LAUNCH-ANGLE-1499537467366-blog533-v2.png" + }, + { + "slug": "gsw-post-season", + "date": "2017-06", + "url": "https://www.nytimes.com/interactive/2017/06/17/sports/basketball/golden-state-warriors-post-season.html", + "img": "https://i.imgur.com/WGjLIil.png" + }, + { + "slug": "british-general-election", + "date": "2017-06", + "url": "https://www.nytimes.com/interactive/2017/06/08/world/europe/british-general-election-results-analysis.html", + "img": "https://static01.nyt.com/images/2017/06/08/world/europe/british-general-election-results-analysis-1496951627600/british-general-election-results-analysis-1496951627600-master1050-v11.png" + }, + { + "slug": "trump-paris-agreement", + "date": "2017-05", + "url": "https://www.nytimes.com/interactive/2017/05/31/climate/trump-climate-paris-agreement.html", + "img": "https://static01.nyt.com/images/2017/04/27/climate/trump-climate-paris-agreement-1496177513835/trump-climate-paris-agreement-1496177513835-master1050.png" + }, + { + "slug": "lebron-points-record", + "date": "2017-05", + "url": "https://www.nytimes.com/interactive/2017/05/25/sports/basketball/lebron-career-playoff-points-record.html", + "img": "https://static01.nyt.com/images/2017/05/25/sports/basketball/lebron-career-playoff-points-record-1495739283266/lebron-career-playoff-points-record-1495739283266-master1050.png" + }, + { + "slug": "wannacry-ransomware-map", + "date": "2017-05", + "url": "https://www.nytimes.com/interactive/2017/05/12/world/europe/wannacry-ransomware-map.html", + "img": "https://i.imgur.com/n8BfIhl.png" + }, + { + "slug": "congress-statements-comey", + "date": "2017-05", + "url": "https://www.nytimes.com/interactive/2017/05/10/us/politics/congress-statements-comey.html", + "img": "https://static01.nyt.com/images/2017/05/10/us/politics/congress-statements-comey-1494438245355/congress-statements-comey-1494438245355-master1050-v3.png" + }, + // { + // "slug": "health-bill-votes", + // "date": "2017-05", + // "url": "https://www.nytimes.com/interactive/2017/05/04/us/how-republican-voted-on-health-bill.html", + // "img": "https://static01.nyt.com/images/2017/05/04/us/how-republican-voted-on-health-bill-1493925679815/how-republican-voted-on-health-bill-1493925679815-master1050.jpg" + // }, + { + "slug": "health-care-votes-v2", + "date": "2017-05", + "url": "https://www.nytimes.com/interactive/2017/05/04/us/politics/house-vote-republican-health-care-bill.html", + "img": "https://static01.nyt.com/images/2017/05/04/us/politics/house-vote-republican-health-care-bill-1493910974236/house-vote-republican-health-care-bill-1493910974236-master1050-v3.png" + }, + { + "slug": "trumps-tax-proposal-cost", + "date": "2017-04", + "url": "https://www.nytimes.com/interactive/2017/04/26/us/politics/what-trumps-tax-proposal-will-cost.html", + "img": "https://static01.nyt.com/images/2017/04/26/us/politics/what-trumps-tax-proposal-will-cost-1493256774601/what-trumps-tax-proposal-will-cost-1493256774601-master1050-v2.jpg" + }, + { + "slug": "trump-financial-disclosure", + "date": "2017-04", + "url": "https://www.nytimes.com/interactive/2017/04/01/us/politics/how-much-people-in-the-trump-administration-are-worth-financial-disclosure.html", + "img": "https://static01.nyt.com/images/2017/04/01/us/politics/how-much-people-in-the-trump-administration-are-worth-financial-disclosure-1491077638041/how-much-people-in-the-trump-administration-are-worth-financial-disclosure-1491077638041-blog533.png" + }, + { + "slug": "health-care-votes-v1", + "date": "2017-03", + "url": "https://www.nytimes.com/interactive/2017/03/24/us/politics/house-vote-republican-health-care-bill.html", + "img": "https://static01.nyt.com/images/2017/03/24/us/politics/house-vote-republican-health-care-bill-1490364092707/house-vote-republican-health-care-bill-1490364092707-master1050-v2.png" + }, + { + "slug": "health-care-whip-count", + "date": "2017-03", + "url": "https://www.nytimes.com/interactive/2017/03/20/us/politics/health-care-whip-count.html", + "img": "https://static01.nyt.com/images/2017/03/20/us/politics/health-care-whip-count-1490065817149/health-care-whip-count-1490065817149-master1050-v2.jpg" + }, + { + "slug": "where-refugees-come-from", + "date": "2017-03", + "url": "https://www.nytimes.com/interactive/2017/03/06/world/where-refugees-come-from.html", + "img": "https://static01.nyt.com/images/2017/03/06/world/where-refugees-come-from-1488845437533/where-refugees-come-from-1488845437533-master1050.png" + }, + { + "slug": "trump-refugees-muslim", + "date": "2017-02", + "url": "https://www.nytimes.com/interactive/2017/02/26/us/trump-refugees-muslim.html", + "img": "https://static01.nyt.com/images/2017/02/27/us/trump-refugees-muslim-1488050208661/trump-refugees-muslim-1488050208661-master1050.png" + }, + { + "slug": "trump-cabinet-opposition", + "date": "2017-02", + "url": "https://www.nytimes.com/interactive/2017/02/09/us/politics/trump-cabinet-opposition.html", + "img": "https://static01.nyt.com/images/2017/02/09/us/politics/trump-cabinet-opposition-1486603337427/trump-cabinet-opposition-1486603337427-master1050-v4.jpg" + }, + { + "slug": "trump-agenda-tracker", + "date": "2017-01", + "url": "https://www.nytimes.com/interactive/2017/us/politics/trump-agenda-tracker.html", + "img": "https://static01.nyt.com/images/2017/01/26/us/politics/trump-agenda-1485391095675/trump-agenda-1485391095675-master1050-v4.png" + }, + { + "slug": "you-draw-obama-legacy", + "date": "2017-01", + "url": "https://www.nytimes.com/interactive/2017/01/15/us/politics/you-draw-obama-legacy.html", + "img": "https://static01.nyt.com/images/2017/01/11/us/politics/you-draw-obama-legacy-1484349598705/you-draw-obama-legacy-1484349598705-master1050.png" + }, + { + "slug": "cabinet-removal", + "date": "2016-12", + "url": "https://www.nytimes.com/interactive/2016/12/22/us/politics/cabinet-removal.html", + "img": "https://static01.nyt.com/images/2016/12/22/us/politics/cabinet-removal-1482354548542/cabinet-removal-1482354548542-blog533.png" + }, + { + "slug": "trump-climate-change", + "date": "2016-12", + "url": "https://www.nytimes.com/interactive/2016/12/08/us/trump-climate-change.html", + "img": "https://static01.nyt.com/images/2016/12/08/us/trump-climate-change-1481174650062/trump-climate-change-1481174650062-master1050-v2.png" + }, + { + "slug": "pope-francis", + "date": "2016-11", + "url": "https://www.nytimes.com/interactive/2016/11/18/world/europe/pope-francis-cardinals-shape-church.html", + "img": "https://static01.nyt.com/images/2016/11/18/world/europe/pope-francis-cardinals-shape-church-1479418314614/pope-francis-cardinals-shape-church-1479418314614-master1050.png" + }, + { + "slug": "rural-urban-split", + "date": "2016-11", + "url": "https://www.nytimes.com/2016/11/12/upshot/this-election-highlighted-a-growing-rural-urban-split.html", + "img": "https://i.imgur.com/NVOLG7s.png" + }, + // { + // "slug": "red-blue-divide-grew-stronger-in-2016", + // "date": "2016-11", + // "url": "https://www.nytimes.com/interactive/2016/11/10/us/politics/red-blue-divide-grew-stronger-in-2016.html", + // "img": "https://static01.nyt.com/images/2016/11/10/us/politics/red-blue-divide-grew-stronger-in-2016-1478802735078/red-blue-divide-grew-stronger-in-2016-1478802735078-master1050.png" + // }, + { + "slug": "forecast-president", + "date": "2016-11", + "url": "https://www.nytimes.com/elections/forecast/president", + "img": "https://i.imgur.com/lyB54bH.png" + }, + { + "slug": "red-blue-divide", + "date": "2016-11", + "url": "https://www.nytimes.com/interactive/2016/11/04/us/politics/growing-divide-between-red-and-blue-america.html", + "img": "https://static01.nyt.com/images/2016/11/04/us/politics/growing-divide-between-red-and-blue-america-1478196181299/growing-divide-between-red-and-blue-america-1478196181299-master1050.png" + }, + { + "slug": "television-ads", + "date": "2016-10", + "url": "https://www.nytimes.com/interactive/2016/10/21/us/elections/television-ads.html", + "img": "https://static01.nyt.com/images/2016/10/21/us/elections/television-ads-1477000949848/television-ads-1477000949848-master1050-v2.png" + }, + { + "slug": "electoral-map", + "date": "2016-10", + "url": "https://www.nytimes.com/interactive/2016/09/20/upshot/electoral-map.html", + "img": "https://static01.nyt.com/images/2016/09/20/upshot/electoral-map-1474301260955/electoral-map-1474301260955-master1050-v2.png" + }, + { + "slug": "support-for-trump", + "date": "2016-10", + "url": "https://www.nytimes.com/interactive/2016/10/11/us/politics/house-and-senate-support-for-trump.html", + "img": "https://static01.nyt.com/images/2016/10/10/us/politics/house-and-senate-support-for-trump-1476162470109/house-and-senate-support-for-trump-1476162470109-master1050.png" + }, + { + "slug": "voters-and-nonvoters", + "date": "2016-09", + "url": "https://www.nytimes.com/interactive/2016/09/13/us/politics/what-separates-voters-and-nonvoters.html", + "img": "https://i.imgur.com/YJD4n73.png" + }, + { + "slug": "debate-moments", + "date": "2016-09", + "url": "https://www.nytimes.com/interactive/2016/09/29/us/elections/debate-moments.html", + "img": "https://i.imgur.com/i7DPEio.png" + }, + { + "slug": "new-geography-of-prisons", + "date": "2016-09", + "url": "https://www.nytimes.com/2016/09/02/upshot/new-geography-of-prisons.html", + "img": "https://static01.nyt.com/images/2016/09/02/us/prison-admission-share/prison-admission-share-master1050.png" + }, + { + "slug": "voter-registration", + "date": "2016-08", + "url": "https://www.nytimes.com/interactive/2016/08/25/us/elections/voter-registration.html", + "img": "https://static01.nyt.com/images/2016/08/24/us/elections/voter-registration-1472069269686/voter-registration-1472069269686-blog533.png" + }, + { + "slug": "2016-senate-seats-in-play", + "date": "2016-08", + "url": "https://www.nytimes.com/interactive/2016/08/11/us/elections/2016-senate-seats-in-play.html", + "img": "https://static01.nyt.com/images/2016/08/11/us/elections/2016-senate-seats-in-play-1470868508682/2016-senate-seats-in-play-1470868508682-master1050.png" + }, + { + "slug": "Clinton-Donors", + "date": "2016-08", + "url": "https://www.nytimes.com/interactive/2016/08/09/us/elections/Bush-Rubio-and-Kasich-Donors-give-to-Clinton.html", + "img": "https://static01.nyt.com/images/2016/08/08/us/elections/Bush-Rubio-and-Kasich-Donors-give-to-Clinton-1470678226803/Bush-Rubio-and-Kasich-Donors-give-to-Clinton-1470678226803-blog533.png" + }, + { + "slug": "nine-percent-of-america", + "date": "2016-08", + "url": "https://www.nytimes.com/interactive/2016/08/01/us/elections/nine-percent-of-america-selected-trump-and-clinton.html", + "img": "https://i.imgur.com/QTawBdH.png" + }, + { + "slug": "political-firsts", + "date": "2016-07", + "url": "https://www.nytimes.com/interactive/2016/07/25/us/politics/political-firsts.html", + "img": "https://static01.nyt.com/images/2016/06/09/us/politics/political-firsts-1465593145470/political-firsts-1465593145470-master1050.jpg" + }, + // { + // "slug": "munich-shopping-mall-shooting", + // "date": "2016-07", + // "url": "https://www.nytimes.com/interactive/2016/07/22/world/europe/munich-shopping-mall-shooting.html", + // "img": "https://static01.nyt.com/images/2016/07/22/world/europe/munich-shopping-mall-shooting-1469213226573/munich-shopping-mall-shooting-1469213226573-master1050-v5.jpg" + // }, + { + "slug": "airport-security", + "date": "2016-07", + "url": "https://www.nytimes.com/interactive/2016/07/01/world/airport-security-around-the-world.html", + "img": "https://static01.nyt.com/images/2016/07/01/world/airport-security-around-the-world-1467378493722/airport-security-around-the-world-1467378493722-master1050-v2.png" + }, + { + "slug": "-brexit-referendum", + "date": "2016-06", + "url": "https://www.nytimes.com/interactive/2016/06/24/world/europe/how-britain-voted-brexit-referendum.html", + "img": "https://static01.nyt.com/images/2016/06/24/world/europe/how-britain-voted-brexit-referendum-1466746689036/how-britain-voted-brexit-referendum-1466746689036-master1050-v2.png" + }, + { + "slug": "euro-2016", + "date": "2016-06", + "url": "https://www.nytimes.com/interactive/2016/06/16/upshot/euro-2016-how-teams-can-advance-to-the-next-round.html", + "img": "https://i.imgur.com/HLIOSJM.png" + }, + { + "slug": "trump-campaign-finance", + "date": "2016-06", + "url": "https://www.nytimes.com/interactive/2016/06/21/us/elections/trump-campaign-finance.html", + "img": "https://static01.nyt.com/images/2016/06/21/us/politics/trump-finances-1466536881410/trump-finances-1466536881410-master1050.png" + }, + { + "slug": "why-orlando-was-so-deadly", + "date": "2016-06", + "url": "https://www.nytimes.com/interactive/2016/06/12/us/why-the-orlando-shooting-was-so-deadly.html", + "img": "https://i.imgur.com/c1p1fGI.png" + }, + { + "slug": "lebron-finals-streak", + "date": "2016-06", + "url": "https://www.nytimes.com/interactive/2016/06/02/sports/basketball/lebron-james-nba-finals-streak.html", + "img": "https://i.imgur.com/3c13aWf.png" + }, + { + "slug": "europe-right-wing", + "date": "2016-05", + "url": "https://www.nytimes.com/interactive/2016/05/22/world/europe/europe-right-wing-austria-hungary.html", + "img": "https://static01.nyt.com/images/2016/05/22/world/europe/europe-right-wing-austria-hungary-1463897749837/europe-right-wing-austria-hungary-1463897749837-master1050-v3.png" + } +] +var otherProjects = [ + // {slug: 'nyc-neighborhoods', date: '2020-02', url: 'url', img: 'https://i.imgur.com/L8lnEEb.jpg'}, + + + {slug: 'measuring-fairness', date: '2020-05', url: 'https://pair.withgoogle.com/explorables/measuring-fairness/', img: 'https://pair.withgoogle.com/explorables/images/measuring-fairness.png'}, + {slug: 'hidden-bias', date: '2020-05', url: 'https://pair.withgoogle.com/explorables/hidden-bias/', img: 'https://pair.withgoogle.com/explorables/images/hidden-bias.png'}, + {slug: 'covid19-mobility', date: '2020-04', url: 'https://www.google.com/covid19/mobility/', img: 'https://i.imgur.com/jEQwOku.png'}, + {slug: 'australian-fires', date: '2020-02', url: 'https://blocks.roadtolarissa.com/1wheel/46874895034f5bded13c97097bf25a83', img: 'https://i.imgur.com/MkkigKh.png'}, + + {slug: 'nyc-neighborhoods', date: '2020-02', url: 'https://pair-code.github.io/interpretability/uncertainty-over-space/neighborhood/', img: 'https://i.imgur.com/L8lnEEb.jpg', }, + {slug: 'uncertainty-over-space', date: '2020-02', url: 'https://pair-code.github.io/interpretability/uncertainty-over-space/', img: 'https://i.imgur.com/PiCK42n.png', }, + {slug: 'pitchfork', date: '2019-12', url: 'https://roadtolarissa.com/pitchfork', img: 'https://i.imgur.com/1TRa5SY.png', }, + {slug: 'understanding-umap', date: '2019-10', url: 'https://pair-code.github.io/understanding-umap/', img: 'https://pair-code.github.io/understanding-umap/share.png' }, + {slug: 'scan-sorted', date: '2019-07', url: 'https://roadtolarissa.com/scan-sorted', img: 'https://i.imgur.com/fhkwZxY.png', }, + {slug: 'bert-tree', date: '2019-06', url: 'https://pair-code.github.io/interpretability/bert-tree/', img: 'https://i.imgur.com/PoiNmfF.png', }, + {slug: 'dvs-privacy', date: '2019-03', url: 'https://roadtolarissa.com/dvs-privacy', img: 'https://i.imgur.com/9XHjYgq.png', }, + {slug: 'flushing-cbtc-finished', date: '2018-11', url: 'http://roadtolarissa.com/flushing-cbtc-finished', img: 'https://i.imgur.com/i5njD1m.png', }, + {slug: '2018-chart-diary', date: '2018-12', url: 'http://roadtolarissa.com/2018-chart-diary', img: 'https://roadtolarissa.com/imgur-down/2018-chart-diary-promo.png', }, + {slug: 'worlds-group-2018', date: '2018-10', url: 'http://roadtolarissa.com/worlds-group-2018', img: 'https://roadtolarissa.com/images/posts/worlds-group-2018.png', }, + {slug: 'literate-blogging', date: '2018-05', url: 'http://roadtolarissa.com/literate-blogging', img: 'https://i.imgur.com/3KDlIFQ.png', }, + {slug: 'msi-4096', date: '2018-05', url: 'https://roadtolarissa.com/msi-4096/', img: 'https://i.imgur.com/vgXcGio.png', }, + {slug: 'hot-reload', date: '2018-04', url: 'https://roadtolarissa.com/hot-reload/', img: 'https://i.imgur.com/ZNkXwEx.png', }, + {slug: 'top-3-movies', date: '2018-04', url: 'https://roadtolarissa.com/top-3-movies/', img: 'https://i.imgur.com/0IADOwR.png', }, + {slug: 'sell-strat', date: '2018-04', url: 'https://roadtolarissa.com/sell-strat/', img: 'https://i.imgur.com/8wKXXIe.png', }, + // {slug: 'nyt', date: '05/2016', url: 'http://www.nytimes.com/by/adam-pearce'}, + {slug: 'same-sex-legal', date: '2018-02', url: 'https://roadtolarissa.com/same-sex-legal/', img: 'https://i.imgur.com/YeoOlAC.png', }, + {slug: 'oracle', date: '2018-01', url: 'https://roadtolarissa.com/oracle', img: 'https://i.imgur.com/rfwNaUx.png', }, + {slug: '2017-chart-diary', date: '2017-12', url: 'https://roadtolarissa.com/2017-chart-diary', img: 'https://roadtolarissa.com/images/posts/2017-chart-diary.png', }, + {slug: 'd3-mp4', date: '2017-11', url: 'http://roadtolarissa.com/d3-mp4', img: 'https://i.imgur.com/cDzQWHX.png' }, + {slug: 'worlds-group-2017', date: '2017-10', url: 'http://roadtolarissa.com/worlds-group-2017', img: 'https://roadtolarissa.com/images/posts/worlds-group-2017.png' }, + {slug: 'hurricane-how-to', date: '2017-09', url: 'http://roadtolarissa.com/hurricane', img: 'https://i.imgur.com/FX5qZ3x.png' }, + {slug: 'msi-group', date: '2017-05', url: 'http://roadtolarissa.com/msi-group', img: 'https://roadtolarissa.com/images/posts/msi-group.png' }, + {slug: 'kindle-tracker', date: '2017-03', url: 'https://roadtolarissa.com/kindle-tracker', img: 'https://roadtolarissa.com/images/posts/kindle-tracker.png', }, + {slug: 'worlds-group', date: '2016-10', url: 'https://roadtolarissa.com/worlds-group', img: 'https://roadtolarissa.com/images/posts/worlds-group.png', }, + {slug: 'projecting-land', date: '2016-08', url: 'https://roadtolarissa.com/projecting-land', img: 'https://roadtolarissa.com/images/posts/projecting-land.png', }, + {slug: 'd3-module-faces', date: '2016-12', url: 'https://blocks.roadtolarissa.com/1wheel/68073eeba4d19c454a8c25fcd6e9e68a', img: 'https://i.imgur.com/0aPWgaV.png'}, + {slug: 'swoopy-drag', date: '2016-03', url: 'http://1wheel.github.io/swoopy-drag/', img: 'https://i.imgur.com/x2M55Yl.png'}, + {slug: 'line-intersection', date: '2016-03', url: 'https://blocks.roadtolarissa.com/1wheel/464141fe9b940153e636', img: 'https://i.imgur.com/wm8XlpS.png'}, + {slug: 'voroni-spiral', date: '2016-03', url: 'https://blocks.roadtolarissa.com/1wheel/c7122eee6247b95dba09', img: 'https://i.imgur.com/TDEygIX.png'}, + {slug: 'who-marries-whom', date: '2016-02', url: 'http://www.bloomberg.com/graphics/2016-who-marries-whom/'}, + {slug: 'donor-network', date: '2016-02', url: 'http://www.bloomberg.com/politics/graphics/2016-fec-filings/febuary/bush-donors/'}, + {slug: 'nba-win-loss', date: '2016-01', url: 'http://roadtolarissa.com/nba-win-loss/'}, + {slug: 'crossover-sales', date: '2016-01', url: 'http://www.bloomberg.com/graphics/2016-crossover-sales/'}, + {slug: 'nba-minutes', date: '2016-01', url: 'http://roadtolarissa.com/nba-minutes/'}, + + {slug: 'trading-analysis', date: '2015-12', url: 'http://www.bloomberg.com/features/2015-stock-chart-trading-game/analysis/'}, + {slug: 'redistricting', date: '2015-12', url: 'http://www.bloomberg.com/politics/graphics/2015-redistricting/'}, + {slug: 'gsw-streak', date: '2015-11', url: 'http://roadtolarissa.com/gsw-streak/'}, + {slug: 'year-ahead', date: '2015-11', url: 'http://www.bloomberg.com/graphics/year-ahead-2016/'}, + {slug: 'individual-donor', date: '2015-10', url: 'http://www.bloomberg.com/politics/graphics/2015-october-fec-filings/charts/'}, + {slug: 'speaker-timeline', date: '2015-10', url: 'http://www.bloomberg.com/politics/graphics/2015-paul-ryan-speaker-of-the-house/'}, + {slug: 'money-map', date: '2015-09', url: 'http://www.bloomberg.com/politics/graphics/2015-presidential-money-map/'}, + {slug: 'spoofing', date: '2015-09', url: 'http://www.bloomberg.com/graphics/2015-spoofing/'}, + {slug: 'dumb-lawyer', date: '2015-09', url: 'https://www.bloomberg.com/news/features/2015-08-20/are-lawyers-getting-dumber-/', img: 'https://i.imgur.com/zzvrdr4.png'}, + + {slug: 'lyric-type', date: '2015-08', url: 'http://roadtolarissa.com/lyric-type/'}, + {slug: 'fifa-scandal', date: '2015-07', url: 'http://www.bloomberg.com/graphics/2015-fifa-scandal/'}, + {slug: 'stacked-bump', date: '2015-07', url: 'http://roadtolarissa.com/stacked-bump/'}, + // {slug: 'super-pac-table', date: '2015-08', url: 'http://www.bloomberg.com/politics/graphics/2015-october-fec-filings/table/'}, + {slug: 'what-is-code', date: '2015-06', url: 'http://www.bloomberg.com/whatiscode'}, + {slug: 'dangerous-jobs', date: '2015-05', url: 'http://www.bloomberg.com/graphics/2015-dangerous-jobs/'}, + {slug: 'uk-election', date: '2015-05', url: 'http://www.bloomberg.com/graphics/2015-uk-election/'}, + {slug: 'uk-splatter', date: '2015-05', url: 'http://www.bloomberg.com/graphics/2015-uk-election/messy.html'}, + {slug: 'weed-index', date: '2015-04', url: 'http://www.bloomberg.com/graphics/2015-weed-index/'}, + {slug: 'ncaa-gambling', date: '2015-04', url: 'http://www.bloomberg.com/graphics/2015-march-madness-gambling/'}, + {slug: 'basketball-fund', date: '2015-03', url: 'http://www.bloomberg.com/graphics/2015-march-madness-basketball-fund/'}, + {slug: 'measles-outbreaks', date: '2015-02', url: 'http://www.bloomberg.com/graphics/2015-measles-outbreaks/'}, + {slug: 'oscar-winners', date: '2015-02', url: 'http://www.bloomberg.com/graphics/2015-oscar-winners/'}, + {slug: 'svg-path', date: '2015-02', url: 'http://roadtolarissa.com/blog/2015/02/22/svg-path-strings/'}, + {slug: 'data-exploration', date: '2015-04', url: 'http://roadtolarissa.com/data-exploration/'}, + {slug: 'graph-scroll', date: '2015-03', url: 'http://1wheel.github.io/graph-scroll/'}, + {slug: 'superbowl-salary', date: '2015-01', url: 'http://www.bloomberg.com/graphics/2015-nfl-super-bowl-salary/'}, + {slug: 'auto-sales', date: '2015-01', url: 'http://www.bloomberg.com/graphics/2015-auto-sales/'}, + {slug: 'space-race', date: '2015-01', url: 'http://www.bloomberg.com/news/features/2015-01-22/the-new-space-race-one-man-s-mission-to-build-a-galactic-internet-i58i2dp6'}, + {slug: 'coloring-maps', date: '2015-01', url: 'http://roadtolarissa.com/blog/2015/01/04/coloring-maps-with-d3/'}, + + {slug: 'convex-hulls', date: '2014-12', url: 'http://roadtolarissa.com/convex-hulls/'}, + {slug: 'campaign-declaration', date: '2014-11', url: 'http://www.bloomberg.com/politics/articles/2014-11-25/when-do-presidential-candidates-announce'}, + {slug: 'live-midterm-results', date: '2014-11', url: 'http://www.bloomberg.com/politics/topics/2014-midterms'}, + // {slug: 'senate-ads', date: '2014-10', url: 'http://www.bloomberg.com/politics/features/2014-10-27/the-persuadables'}, + {slug: 'midterm-ads', date: '2014-10', url: 'http://www.bloomberg.com/politics/graphics/2014-senate-ads-and-issues/'}, + {slug: 'governor-jobs', date: '2014-10', url: 'http://www.bloomberg.com/politics/graphics/2014-incumbent-governors/'}, + {slug: 'golf-paths', date: '2014-10', url: 'http://roadtolarissa.com/golf-paths/'}, + {slug: 'zhou-crackdown', date: '2014-09', url: 'http://www.bloomberg.com/infographics/2014-08-04/how-to-catch-a-tiger.html'}, + {slug: 'world-cup', date: '2014-05', url: 'http://www.bloomberg.com/visual-data/world-cup/#0,0,-1'}, + {slug: 'drawdown', date: '2014-09', url: 'http://roadtolarissa.com/drawdown/'}, + {slug: '215-teeth', date: '2014-06', url: 'http://roadtolarissa.com/215-teeth/'}, + {slug: 'joymap', date: '2014-03', url: 'http://roadtolarissa.com/population-division/'}, + {slug: 'synth-scale', date: '2014-01', url: 'http://roadtolarissa.com/synth/'}, + + {slug: 'twisters', date: '2013-10', url: 'http://roadtolarissa.com/twisters/'}, + {slug: 'nba-draft', date: '2013-06', url: 'http://roadtolarissa.com/nba-draft/'}, + {slug: 'meteors', date: '2013-05', url: 'http://roadtolarissa.com/meteors/'}, + {slug: 'film-strips', date: '2013-05', url: 'http://roadtolarissa.com/film-strips/'}, + {slug: 'whale-words', date: '2013-01', url: 'http://roadtolarissa.com/whalewords'}, + + {slug: 'unemployment-rates', date: '2013-01', url: 'http://roadtolarissa.com/unemployment'}, + {slug: 'sierpinski-triangle', date: '2012-12', url: 'http://roadtolarissa.com/triangles/'}, + {slug: 'whitehouse-petitions', date: '2012-12', url: 'http://roadtolarissa.com/whitehouse'}, + {slug: 'redditgraphs', date: '2012-10', url: 'http://roadtolarissa.com/redditgraphs'}, + {slug: 'backgammon', date: '2012-10', url: 'http://roadtolarissa.com/javascript/hangout-boardgames/'}, + {slug: 'connect-4', date: '2012-09', url: 'http://roadtolarissa.com/connect-4-ai-how-it-works/'}, +]; diff --git a/source/homepage/spiral.css b/source/homepage/spiral.css new file mode 100644 index 00000000..3174207d --- /dev/null +++ b/source/homepage/spiral.css @@ -0,0 +1,198 @@ +html{ + /*font-family: monaco, Consolas, 'Lucida Console', monospace; */ + height: 100000px; +} + +body{ + position: fixed; + width: calc(100vw - 10px); + max-width: 750px; + left: 50%; + transform: translateX(-50%); + -webkit-transform-style: preserve-3d; + -webkit-backface-visibility: hidden; +} + +#spiral{ + position: fixed; + transform-origin: 72.3606% 44.721%; + /*transform-origin: center center;*/ + /*border: 1px solid #000;*/ + margin: 0px auto; +} + +#spiral > div{ + position: absolute; + background: #000; +} + +@media (max-width: 800px){ + + #spiral > div{ + position: absolute; + background: rgba(0,0,0,0); + } + +} + +.img-container{ + background: #fff; + background-size: cover; + background-position: center center; + width: calc(100% - 2px); + height: calc(100% - 2px); + /*background-size: 350px auto;*/ + + /*background-color: #000;*/ + image-rendering: pixelated; + cursor: pointer; +} + +.index-1.img-container{ transform: translate( 1px, 1px) rotate(90deg); } +.index-2.img-container{ transform: translate( 1px, 1px) rotate(180deg); } +.index-3.img-container{ transform: translate( 1px, 1px) rotate(270deg); } +.index-0.img-container{ transform: translate( 1px, 1px) rotate(0deg); } + + + +html.is-boring{ height: auto; } +.is-boring body{ + position: static; + width: auto; + max-width: 750px; + left: 50%; + transform: translateX(0px); + +} +.is-boring #spiral{ display: none; } +.is-boring #boring{ + display: block; + overflow: visible; +} +#boring{ display: none; } + +#boring{ + margin-top: 30px; + font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif; + + overflow: hidden; + line-height: 1.9em; +} + +#boring div{ + /*white-space: nowrap;*/ +} + +.slug{ + margin-left: 20px; + text-decoration: none; + pointer-events: none; +} + +.row{ + cursor: pointer; + display: block; + text-decoration: none; + white-space: nowrap; + font-family: monospace; + /*font-family: Consolas, 'Lucida Console', monospace; */ + display: inline-block; + width: 370px; + + font-size: 14px; +} +.row:hover .slug{ + text-decoration: underline; +} +.row:hover .month, .row:hover .year{ + opacity: 1 !important; +} + +.row:hover .thumbnail{ + border-bottom-width: 2px; + margin-bottom: -1px; +} + +.row a{ + color: #333; +} +.row.is-top a{ + color: #f0f; + font-weight: 600; +} + +.row.is-top .thumbnail{ + /*border: 1px solid purple;*/ +} + + +.thumbnail{ + background-size: cover; + background-position: center center; + width: 100px; + height: 50px; + display: inline-block; + border: 1px solid #333; + position: relative; + top: 18px; + /*margin-left: 20px;*/ +} + +.month, .year{ + color: #999; + font-size: 12px; +} + +.header{ + font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif; +} + + +.button{ + position: fixed; + bottom: 0px; + left: 0px; + background: #000; + color: #fff; + z-index: 100000; + font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif; + cursor: pointer; + padding: 10px; + width: 40px; + text-align: center; + display: none; +} + +html{ + background: #f1f1f1;; +} + +.year-label{ + position: relative; + color: #333; + font-family: monospace; + left: -80px; + margin-bottom: 20px; + height: 0px; + top: 50px; + font-size: 12px; +} + +.year{ + margin-bottom: 40px; +} + +@media (max-width: 930px){ + + .row{ + width: 100%; + } + + .year-label{ + top: 0px; + left: 0px; + font-size: 14px; + } + +} + diff --git a/source/homepage/spiral.html b/source/homepage/spiral.html new file mode 100644 index 00000000..1071b1ae --- /dev/null +++ b/source/homepage/spiral.html @@ -0,0 +1,88 @@ + + + + + + + + roadtolarissa + + + + + + + +
+ +
+ Adam Pearce + + + + + + + + + + + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/source/homepage/spiral.js b/source/homepage/spiral.js new file mode 100644 index 00000000..9ff782fe --- /dev/null +++ b/source/homepage/spiral.js @@ -0,0 +1,299 @@ +var topProjects = ` +subway-crisis +spoofing +vegas-guns +who-marries-whom +race-class-white-and-black +houston-cries-for-help +what-is-code +some-trains-dont-run-at-all +2017-chart-diary +tax-calculator +twisters +you-draw-obama-legacy +new-geography-of-prisons +nba-win-loss +hot-reload +dumb-lawyer +uk-splatter +nba-minutes +nine-percent-of-america +europe-right-wing +2018-chart-diary +hurricane-how-to +uncertainty-over-space +private-and-fair + + + + +more-extreme-summer-heat + + +hurricane-harvey-rain + + +lebron-points-record + +hurricane-irma-records + +voroni-spiral + +golden-state-warriors-post-season +affirmative-action +game-of-thrones-chart +wannacry-ransomware-map +lyric-type +forecast-president +every-tax-cut-in-the-house-tax-bill +british-general-election-results-analysis +trump-climate-change +trading-analysis +lebron-james-nba-finals-streak +what-separates-voters-and-nonvoters +BASEBALL-LAUNCH-ANGLE +euro-2016-how-teams-can-advance-to-the-next-round +hurricane-irma-map +debate-moments +Bush-Rubio-and-Kasich-Donors-give-to-Clinton +how-much-people-in-the-trump-administration-are-worth-financial-disclosure +this-election-highlighted-a-growing-rural-urban-split +growing-divide-between-red-and-blue-america +television-ads +sell-strat + +trump-refugees-muslim +where-refugees-come-from +trump-cabinet-opposition +political-firsts +pope-francis-cardinals-shape-church +cabinet-removal + +how-britain-voted-brexit-referendum +`.split('\n') + + +var isBoring = true + + +nytProjects.forEach(d => d.isNYT = true) +var projects = nytProjects.concat(otherProjects) +projects.forEach((d, i) => { + if (!d.img) d.img = '/images/thumbnails/' + d.slug + '.png' +}) + +projects = _.orderBy(projects, d => d.date, 'desc') +projects = _.sortBy(projects, d => { + var i = topProjects.indexOf(d.slug) + d.forceIndex = i + d.isTop = i > -1 && i < 30 + + return i == -1 ? 10000000 : (i < 30 ? i : 10000000) +}) + + + +var numProjects = projects.length +projects = projects.concat(projects) + + +console.clear() +var phi = 1.618033988749895 +var cx = 0.7236067977499789 + +var initS = Math.min(innerWidth, 750)/phi +var s = initS +var x = 0 +var y = 0 + + +var imgs = projects.map(d =>'url("' + d.img + '")') + +var links = projects.map(d => d.url) + +var UA = navigator.userAgent +var isFF = UA.includes('Firefox') && !UA.includes('Chrome/') +var isSF = UA.includes('Safari') && !UA.includes('Chrome/') +isSF = true + +var sel = d3.select('#spiral').html('') + .st({ + width: s*phi, + height: s, + transformOrigin: `${s*phi*cx}px ${s*cx}px`, + top: `calc(50vh - ${s/2}px)`, + }) + +d3.range(innerWidth < 700 ? 13 : 18).forEach((d, i) => { + sel.append('div.step') + .st({ + transform: `translate(${x}px, ${y}px) scale(${s/initS})`, + transformOrigin: 'left top', + width: initS, + height: initS + }) + .append('div.img-container') + .classed('index-' + i % 4, 1) + + var [dx, dy] = [ + [s, 0], + [s/phi/phi, s], + [-s/phi, s - s/phi], + [0, -s/phi], + ][i % 4] + + x += dx + y += dy + + s = s/phi +}) + +var stepSel = sel.selectAll('.step') +var imgSel = sel.selectAll('.img-container') + +updateImg() + +var timerY = 0 + +function updateZoom(){ + if (isBoring) return + + var scale = (pageYOffset)/500 + 1 + + if (pageYOffset > (numProjects + 6)*500){ + window.scrollTo(0, pageYOffset - numProjects*500) + } + + var drawScale = Math.min(5, scale) + if (drawScale == 5){ + drawScale += scale % 1 + } + + updateImg(Math.floor(scale - drawScale) ) + sel.st({ + transform: ` + scale(${Math.pow(phi, drawScale)/phi}) + rotate(${-90*drawScale + 90}deg) + `, + }) + + if (timerY != pageYOffset && window.scrollTimer && pageYOffset) scrollTimer.stop() +} + +function updateImg(offset){ + imgSel + .st({backgroundImage: (d, i) => imgs[i + offset]}) + .on('click', (d, i) => { + console.log(imgs[i + offset]) + if (!links[i + offset]) return + window.open(links[i + offset], '_blank'); + }) +} + +updateZoom() +d3.select(window).on('scroll', updateZoom) + +setTimeout(startAutoScroll, 50) +setTimeout(startAutoScroll, 250) +setTimeout(startAutoScroll, 500) +setTimeout(startAutoScroll, 750) +setTimeout(startAutoScroll, 1000) + +function startAutoScroll(){ + if (isBoring) return + + var initY = pageYOffset + + if (window.scrollTimer) window.scrollTimer.stop() + scrollTimer = d3.timer(t => { + timerY = Math.round(t/12) + initY + window.scrollTo(0, timerY) + }) +} + +if (isFF){ + setTimeout(() => { + d3.select(window).on('mousemove', () => { + window.scrollTimer.stop() + }) + }, 2000) +} + + +// boring +!(function(){ + boringProjects = nytProjects.concat(otherProjects) + + boringProjects.forEach(d => { + d.year = d.date.split('-')[0] + d.month = d.date.split('-')[1] + }) + + boringProjects = _.orderBy(boringProjects, d => d.date, 'desc') + + // d3.nestBy(boringProjects.slice().reverse(), d => d.year).forEach(years => { + var byYear = d3.nestBy(boringProjects, d => d.year) + byYear.forEach(years => { + years[0].isYear = true + d3.nestBy(years, d => d.month).forEach(months => { + months[0].isMonth = true + }) + }) + + var sel = d3.select('#boring').html('') + + var yearSel = sel.appendMany('div.year', byYear) + .st({width: '100%'}) + + yearSel.append('div.year-label') + .text(d => d.key) + + var linkSel = yearSel.appendMany('a.row', d => d) + .attr('href', d => d.url) + .attr('target', '_blank') + .classed('is-top', d => d.isTop) + + // linkSel.append('span.year') + // .text(d => d.year) + // .st({opacity: d => d.isYear ? 1 : 0}) + // linkSel.append('span.month') + // .text(d => '-' + d.month) + // .st({opacity: d => d.isMonth ? 0 : 0}) + + linkSel.append('div.thumbnail') + .st({ + backgroundImage: d => `url(${d.img})`, + }) + + linkSel.append('a.slug') + .text(d => d.slug.toLowerCase().replace(/-/g, ' ').trim()) + .at({href: d => d.url}) +})() + +var buttonSel = d3.select('html') + .append('div.button').text('list') + .on('click', toggleBoring) + + +function toggleBoring(){ + isBoring = !isBoring + d3.select('html').classed('is-boring', isBoring) + + window.scrollTo(0, 0) + buttonSel.text(isBoring ? 'spiral' : 'list') + + if (isBoring){ + if (window.scrollTimer) window.scrollTimer.stop() + } else{ + startAutoScroll() + } + +} + +if (isSF) toggleBoring() + + + +// todo +// https://blocks.roadtolarissa.com/1wheel/68073eeba4d19c454a8c25fcd6e9e68a +// jetpack hot-server? diff --git a/source/homepage/update.sh b/source/homepage/update.sh new file mode 100755 index 00000000..9bc56f34 --- /dev/null +++ b/source/homepage/update.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +: ' +add screenshot + +https://1wheel.imgur.com/all/ + + +update sheet + +https://docs.google.com/spreadsheets/d/1xUvK5PGo8XPqARJIvXRnn71lJSL8CwjvjUWB9Jcy0Ho +' + + +cd "$(dirname "$0")" +cd ../../../archive-roadtolarissa/homepage-list/ + +node script.js +node script.js +yarn pub + +cd ../../roadtolarissa/source/homepage/ + +node list-bin.js +yarn pub diff --git a/source/images/posts/worlds-group-2017.png b/source/images/posts/worlds-group-2017.png index 221eb7ec..1615a84c 100644 Binary files a/source/images/posts/worlds-group-2017.png and b/source/images/posts/worlds-group-2017.png differ diff --git a/source/images/posts/worlds-group-2018.png b/source/images/posts/worlds-group-2018.png new file mode 100644 index 00000000..8e7ae80b Binary files /dev/null and b/source/images/posts/worlds-group-2018.png differ diff --git a/source/images/thumbnails/harvey-rain.png b/source/images/thumbnails/harvey-rain.png new file mode 100644 index 00000000..7e7f29bb Binary files /dev/null and b/source/images/thumbnails/harvey-rain.png differ diff --git a/source/images/thumbnails/florida-redistricting.png b/source/images/thumbnails/redistricting.png similarity index 100% rename from source/images/thumbnails/florida-redistricting.png rename to source/images/thumbnails/redistricting.png diff --git a/source/images/thumbnails/train-heat.png b/source/images/thumbnails/train-heat.png new file mode 100644 index 00000000..a6bbb1ed Binary files /dev/null and b/source/images/thumbnails/train-heat.png differ diff --git a/source/index-next.html b/source/index-next.html deleted file mode 100644 index f6f9b5e7..00000000 --- a/source/index-next.html +++ /dev/null @@ -1,332 +0,0 @@ - - - - - - - - - roadtolarissa - - - - - - - -
- -
- Adam Pearce - - - - - - - - - - - - -
-
- -
- - - - - - - - - - - - - - - - diff --git a/source/index.html b/source/index.html index 950fc6a2..38f00d78 100644 --- a/source/index.html +++ b/source/index.html @@ -1,17 +1,22 @@ - - + + + roadtolarissa + - - + + + + +
roadtolarissa @@ -19,330 +24,1041 @@
- - -

libraries

- - -

talks

- - -

posts

-
- 4096 Paths Into MSI - Buy High and Sell Low - Hackable Hot Reloading - Top Three Movies - Same-Sex Marriage Bans - Aaronson Oracle - 2017 Chart Diary - D3 to MP4 - 2017 Worlds Group - Hurricane How-To - The 64 Ways into MSI - Kindle Tracker - The Ways Out of World Group Stage - Projecting Land - NBA Win/Loss Records - Point Differentials of 2015 NBA Games - Golden State's 2015 Point Differentials - Lyric Typing - Stacked Bump Charts - Data Exploration With D3 - SVG Path Strings - Coloring Maps - Convex Hulls - Dragon Curve - Golf Paths - Drawdown - 215 Teeth / 1008 Beats - Even Fewer Lambdas - Population Division - Making Music With D3 - Twisters - NBA Draft - Meteor Map - Film Strips - Whale Words - Unemployment Rates - Zoomable Sierpinski Triangle - White House Petition Signatures - Next Project - Redditgraphs Retrospective - Reddit Comment Visualizer - Yglesias on Amazon's P/E Ratio - Speed Issues Goko Dominion - Connect 4 AI: How It Works - Maximin Connect 4 Completed - First Post - - - - + - - - - - - - - - - - - - - - - - - +
+ \ No newline at end of file diff --git a/source/javascripts/posts/golf-wl/script.js b/source/javascripts/posts/golf-wl/script.js index 9f9d7b4b..c4e8da3a 100644 --- a/source/javascripts/posts/golf-wl/script.js +++ b/source/javascripts/posts/golf-wl/script.js @@ -84,7 +84,7 @@ function addStaticSVG(){ .style('text-anchor', 'end') var textG = svg.append('g') - .attr('transform', 'translate(-10,-2)') + .attr('transform', 'translate(-5,-20)') textG.append('text') .classed('selectedText', true) diff --git a/source/javascripts/posts/golf-wl/style.css b/source/javascripts/posts/golf-wl/style.css index 32b010e0..3e647368 100644 --- a/source/javascripts/posts/golf-wl/style.css +++ b/source/javascripts/posts/golf-wl/style.css @@ -1,4 +1,11 @@ +#golf-wl{ + font-family: Helvetica, serif; + font-size: 14px; +} +svg{ + overflow: visible; +} rect{ fill-opacity: 0; diff --git a/source/javascripts/posts/synthComp/gears.js b/source/javascripts/posts/synthComp/gears.js index d2347ea5..629a160f 100644 --- a/source/javascripts/posts/synthComp/gears.js +++ b/source/javascripts/posts/synthComp/gears.js @@ -15,7 +15,7 @@ var width = 750, (function(){ - //largely borrowed from http://bl.ocks.org/mbostock/1353700 + //largely borrowed from http://blocks.roadtolarissa.com/mbostock/1353700 var svg = d3.select("#synth") .style('height', height + 'px') .append("svg") diff --git a/source/literate-blogging/_script.js b/source/literate-blogging/_script.js index ed6cdfb2..e87880e3 100644 --- a/source/literate-blogging/_script.js +++ b/source/literate-blogging/_script.js @@ -1,11 +1,9 @@ -console.clear() - var data = d3.csvParse(`date,val,source -2012-09,324000,https://www.openhub.net/p/Wordpress, -2014-02,7500,https://www.openhub.net/p/Jekyll, +2012-09,324000,https://www.openhub.net/p/WordPress, +2014-02,7500,https://www.openhub.net/p/Octopress, 2015-07,1250,https://www.openhub.net/p/Metalsmith -2017-08,65,https://www.openhub.net/p/Custom -2018-05,65,https://www.openhub.net/p/custom`) +2017-08,60,https://www.openhub.net/p/This Post +2018-05,60,https://www.openhub.net/p/custom`) data.forEach((d, i) => { @@ -24,7 +22,7 @@ data.forEach(d => { var sel = d3.select('#graph').html('') var c = d3.conventions({ sel, - margin: {left: innerWidth > 900 ? 0 : 55, top: 40, right: 5} + margin: {left: innerWidth > 820 ? 0 : 55, top: 10, right: 5} }) @@ -66,11 +64,36 @@ c.svg.appendMany('text', data.filter(d => d.mid)) .text(d => d.name) .translate(d => [c.x(d.mid), c.y(d.val)]) .at({textAnchor: 'middle', dy: '-.33em', fontSize: 12}) - -c.svg.append('text') - .text('No Code Is The Best Code') - .text('Marching Towards Nothing') - .at({x: c.width/2, y: innerWidth < 750 ? -22 : -12, textAnchor: 'middle', fontWeight: 800}) - + .st({fontWeight: d => d.name == 'This Post' ? 600 : ''}) + +// c.svg.append('text') +// .text('No Code Is The Best Code') +// .text('Marching Towards Nothing') +// .text('Decreasing The Code Building This Site') +// .at({x: c.width/2, y: -22, textAnchor: 'middle', fontWeight: 800}) +// .at({x: 0, textAnchor: 'start'}) + + + +annotations = +[ + { + "path": "M 674,142 A 197.135 197.135 0 0 1 325,-20.000057220458984", + "textOffset": [ + 332, + -299 + ] + } +] + +var swoopy = d3.swoopyDrag() + .draggable(0) + .x(d => 0) + .y(d => 0) + .annotations(annotations) + +swoopySel = c.svg.append('g.annotations').call(swoopy) + .st({opacity: innerWidth > 820 ? 1 : 0}) +swoopySel.selectAll('path').attr('marker-end', 'url(#arrow)') diff --git a/source/literate-blogging/change-date.js b/source/literate-blogging/change-date.js new file mode 100644 index 00000000..cc93e4f2 --- /dev/null +++ b/source/literate-blogging/change-date.js @@ -0,0 +1,15 @@ +var fs = require('fs') +var {_} = require('scrape-stl') + +function readdirAbs(dir){ return fs.readdirSync(dir).map(d => dir + '/' + d) } + + +readdirAbs(__dirname + '/_posts').forEach(path => { + var date = _.last(path.split('/')).split('.')[0].substr(0, 10) + var str = fs.readFileSync(path, 'utf8') + .replace('permalink: ', `date: ${date}\npermalink: `) + + fs.writeFileSync(path, str) +}) + + diff --git a/source/literate-blogging/compile.js b/source/literate-blogging/compile.js new file mode 100644 index 00000000..c2e04ebc --- /dev/null +++ b/source/literate-blogging/compile.js @@ -0,0 +1,50 @@ +// https://github.com/vijithassar/lit-web/blob/master/source/compile.js + +// test whether the line starts with backticks +const is_fence = line => line.slice(0, 3) === '```'; + +// test whether the line starts with backticks followed by a javascript language specifier +const is_javascript_fence = line => line.slice(0, 5) === '```js' || line.slice(0, 13) === '```javascript'; + +// count backtick fences to make sure they are balanced +const balanced_fences = code => code.split('```').length % 2 !== 0; + +// extract JavaScript code blocks from a Markdown string +const compile = markdown => { + // exit immediately if backticks aren't balanced + if (! balanced_fences(markdown)) { + return; + } + // split into lines + const lines = markdown.split("\n"); + // count backticks + let fence_count = 0; + // comment out Markdown + const code = lines + .map(line => { + const fence = is_fence(line); + const javascript_fence = fence ? is_javascript_fence(line) : false; + // increment the fence count if it's a valid + // opening or closing fence + if (javascript_fence || (fence && fence_count % 2 === 1)) { + fence_count += 1; + if (fence_count % 2 === 0) return '' + } + // are we currently inside a code block + // or a Markdown documentation passage? + const is_markdown = fence_count % 2 === 0 || fence; + if (is_markdown) { + return null; + } else { + return line; + } + }) + .filter(d => d !== null) + + return code.join("\n").trim(); +}; + +var fs = require('fs') + +fs.writeFileSync('index.js', compile(fs.readFileSync('../_posts/2017-11-12-literate-blogging.md', 'utf8'))) + diff --git a/source/literate-blogging/index.js b/source/literate-blogging/index.js index 3df9303a..e0d91651 100644 --- a/source/literate-blogging/index.js +++ b/source/literate-blogging/index.js @@ -1,9 +1,14 @@ -// based on -// http://ashkenas.com/journo/docs/journo.html -// https://github.com/sveltejs/svelte.technology/blob/master/scripts/prep/build-blog.js - var fs = require('fs') -var { exec, execSync } = require('child_process') +var {exec, execSync} = require('child_process') + +var public = `${__dirname}/../../public` +var source = `${__dirname}/../../source` + +function rsyncSource(){ + exec(`rsync -a --exclude _posts --exclude _templates ${source}/ ${public}/`) +} +rsyncSource() + var hljs = require('highlight.js') var marked = require('marked') marked.setOptions({ @@ -11,59 +16,43 @@ marked.setOptions({ smartypants: true }) -var public = `${__dirname}/public` -var source = `${__dirname}/source` - -// copy everything but _posts and _templates to public -function rsyncStatic(){ - exec('rsync -a --exclude _posts/ --exclude _templates/ source/ public/') -} -rsyncStatic() - -// convert _templates dir into functions var templates = {} -fs.readdirSync(`${source}/_templates`).forEach(path => { - var str = fs.readFileSync(`${source}/_templates/${path}`, 'utf8') - templates[path] = d => eval('`' + str + '`') +readdirAbs(`${source}/_templates`).forEach(path => { + var str = fs.readFileSync(path, 'utf8') + var templateName = path.split('_templates/')[1] + templates[templateName] = d => eval('`' + str + '`') }) -var posts = fs.readdirSync(`${source}/_posts`).map(parsePost) +function readdirAbs(dir){ return fs.readdirSync(dir).map(d => dir + '/' + d) } + +var posts = readdirAbs(`${source}/_posts`).map(parsePost) fs.writeFileSync(public + '/rss.xml', templates['rss.xml'](posts)) -fs.writeFileSync(public + '/atom.xml', templates['rss.xml'](posts)) fs.writeFileSync(public + '/sitemap.xml', templates['sitemap.xml'](posts)) -// read post path and write to public/ function parsePost(path){ - var slug = path.split('.')[0] - var date = slug.substr(0, 10) - - var markdown = fs.readFileSync(`${source}/_posts/${path}`, 'utf8') + var [top, body] = fs.readFileSync(path, 'utf8') + .replace('---\n', '') + .split('\n---\n') - // parse metadata from frontmatter - var [top, content] = markdown.replace('---\n', '').split('\n---\n') - var meta = {} + var post = {html: marked(body)} top.split('\n').forEach(line => { - var [key, val] = line.split(': ') - meta[key] = val + var [key, val] = line.split(/: (.+)/) + post[key] = val }) - // convert markdown to html - var html = marked(content) - var post = {slug, date, meta, html} + return post +} - var dir = public + meta.permalink +function writePost(post){ + var dir = public + post.permalink if (!fs.existsSync(dir)) execSync(`mkdir -p ${dir}`) - fs.writeFileSync(`${dir}/index.html`, templates[post.meta.template](post)) - - return post + fs.writeFileSync(`${dir}/index.html`, templates[post.template](post)) } +posts.forEach(writePost) -// copy files on change -if (process.argv.join('').includes('--watch')){ +if (process.argv.includes('--watch')){ require('chokidar').watch(source).on('change', path => { - console.log(path) - - if (path.includes('_posts/')) parsePost(path.split('_posts/')[1]) - rsyncStatic() + rsyncSource() + if (path.includes('_posts/')) writePost(parsePost(path)) }) } \ No newline at end of file diff --git a/index.js b/source/literate-blogging/index_original.js similarity index 99% rename from index.js rename to source/literate-blogging/index_original.js index a7a27abf..3df9303a 100644 --- a/index.js +++ b/source/literate-blogging/index_original.js @@ -1,3 +1,4 @@ +// based on // http://ashkenas.com/journo/docs/journo.html // https://github.com/sveltejs/svelte.technology/blob/master/scripts/prep/build-blog.js diff --git a/source/literate-blogging/style.css b/source/literate-blogging/style.css index 1569a358..4c2b5dd5 100644 --- a/source/literate-blogging/style.css +++ b/source/literate-blogging/style.css @@ -1,23 +1,25 @@ -svg { - overflow: visible; +h1{ + max-width: 750px; } -.axis, .tooltip{ - /*font-family: monaco, Consolas, 'Lucida Console', monospace; */ +@media (max-width: 590px){ + pre{ + overflow-x: scroll; + } } -.x.axis text, .y.axis text, .axis tspan{ - /*fill: #888 !important;*/ +svg { + overflow: visible; } -.axis line{ - /*stroke: #888;*/ + +.x.axis text, .y.axis text, .axis tspan{ + fill: #888 !important; } .domain{ display: none; } - .tick line{ opacity: 0; } @@ -28,6 +30,23 @@ svg { text{ fill: #000; - font-family: monaco, Consolas, 'Lucida Console', monospace; + /*font-family: monaco, Consolas, 'Lucida Console', monospace; */ + font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif; + /*font-family: 'Roboto';*/ + font-weight: 300; +} + + +body{ + /*font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica,helvetica neue,ubuntu,roboto,noto,segoe ui,arial,sans-serif;*/ +} + -} \ No newline at end of file +.annotations path{ + fill: none; + stroke: black; + stroke-width: 1.2px; +} +.annotations text{ + font-size: 12px; +} diff --git a/source/live-forecast-2022/init.js b/source/live-forecast-2022/init.js new file mode 100644 index 00000000..9b1aa52c --- /dev/null +++ b/source/live-forecast-2022/init.js @@ -0,0 +1,189 @@ +console.clear() +d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + +var maxMargin = .2 +window.renderFns = [] + +d3.loadData('https://roadtolarissa.com/data/2022-wp.json', (err, [times]) => { + window.tidy = [] + + times = times.filter(d => d.scrapeTime < '2022-11-09T11:38:35.067Z') + times.forEach(time => { + time.races.forEach(race => { + race.scrapeTime = time.scrapeTime + tidy.push(race) + + if (race.leader?.party_caucus == 'DEM'){ + race.margin = race.margin.map(d => -d).reverse() + } + + if (race.wapo?.votes){ + var {dem, gop} = race.wapo.votes + race.wapoMargin = [ + calcMargin(dem['upper_0.9'], gop['lower_0.9']), + calcMargin(dem.pred, gop.pred), + calcMargin(dem['lower_0.9'], gop['upper_0.9']), + ] + function calcMargin(d, r){ + return (r - d)/(d + r) + } + } else { + race.wapoMargin = [-maxMargin, 0, maxMargin] + race.isFakeWapo = true + } + }) + }) + + var byRace = d3.nestBy(tidy, d => d.nyt_id) + byRace = _.sortBy(byRace, d => -d.key.includes('-S-')) + + drawChamber('all') + // drawChamber('s') + // drawChamber('h') + drawSlider() + + + function renderIndex(index){ + renderFns.forEach(d => d(index)) + } + + function drawSlider(){ + var sel = d3.select('.slider').html('') + var lastIndex = times.length - 1 + + var timeSel = sel.append('div.time') + + var sliderSel = sel.append('input') + .at({ type:'range', min: 0, max: lastIndex, step: 1, value: lastIndex}) + .on('input', function(){ + renderIndex(this.value) + }) + + renderFns.push(curIndex => { + var str = new Date(times[curIndex].scrapeTime) + '' + str = str.split('GMT')[0] + timeSel.text(str) + }) + + renderIndex(lastIndex) + } + + + function drawChamber(chamber){ + var sel = d3.select('.chamber-' + chamber).html('') + + var width = innerWidth - 20 + var pWidth = d3.select('p').node().offsetWidth + var marginLeft = -(width - pWidth)/2 + sel.st({width, marginLeft}) + + var races = chamber != 'all' ? byRace.filter(d => d.key.includes(`G-${chamber.toUpperCase()}-`)) : byRace + races.forEach(race => drawRace(sel, race, width)) + } + + function drawRace(sel, race, chamberWidth){ + var nCols = Math.ceil(chamberWidth/(innerWidth > 800 ? 350 : 250)) + var margin = {left: 30, right: 30, bottom: 40} + var chartSize = Math.floor(chamberWidth/nCols - margin.left - margin.right) + + var c = d3.conventions({ + sel: sel.append('div.race'), + width: chartSize, + height: chartSize, + margin, + }) + + c.x.domain([-maxMargin, maxMargin]) + c.y.domain([-maxMargin, maxMargin]) + + var tickFmt = d => { + if (!d) return '0%' + var str = d3.format('+.0%')(d) + if (str.includes(20)) return '' + return (d < 0 ? 'D ' : 'R ') + str.replace('-', '+') + } + c.xAxis.ticks(3).tickFormat(tickFmt) + c.yAxis.ticks(3).tickFormat(tickFmt) + d3.drawAxis(c) + ggPlot(c) + addAxisLabel(c, 'nyt', 'wapo') + + c.svg.selectAll('.tick').filter(d => d == 0).raise() + .select('path').st({strokeWidth: 1.5, stroke: '#000'}) + + var raceSlug = race.key.replace('-G', '').replace('-2022-11-08', '') + c.svg.append('text.race-title').text(raceSlug) + .at({textAnchor: 'middle', x: c.width/2, y: -2, fontSize: 14, fontFamily: 'monospace'}) + + c.svg.appendMany('circle', race) + .at({r: 1, fill: '#f0f'}) + .translate(d => [c.x(d.margin[2]), c.y(d.wapoMargin[1])]) + + var line = d3.line() + .x(d => c.x(d.margin[2])) + .y(d => c.y(d.wapoMargin[1])) + c.svg.append('path.data-el').at({d: line(race), fill: 'none', stroke: '#f0f', opacity: .3}) + + var rectSel = c.svg.append('rect.data-el').st({opacity: .15}) + var rectBgSel = c.svg.append('rect.data-el').st({opacity: .15}) + var circleSel = c.svg.append('circle.data-el').at({r: 3, stroke: '#f0f', strokeWidth: 2, fill: 'none'}) + + + + renderFns.push(curIndex => { + // if (raceSlug != 'AZ-S') return + var curData = race[curIndex] + if (!curData ){ + rectSel.st({opacity: 0}) + circleSel.st({opacity: 0}) + return + } + c.svg.classed('is-disabled', curData.isFakeWapo) + + var y = c.y(curData.wapoMargin[2]) + var height = c.y(curData.wapoMargin[0]) - y + + var x = c.x(curData.margin[0]) + rectSel.at({y, height, x, width: c.x(curData.margin[4]) - x}) + + var x = c.x(curData.margin[1]) + rectBgSel.at({y, height, x, width: c.x(curData.margin[3]) - x}) + + circleSel.at({cy: c.y(curData.wapoMargin[1]), cx: c.x(curData.margin[2])}) + }) + + } +}) + + + + + + + +function addAxisLabel(c, xText, yText, xOffset=30, yOffset=-30){ + c.svg.select('.x').append('g') + .translate([c.width/2, xOffset]) + .append('text.axis-label') + .text(xText) + .at({textAnchor: 'middle'}) + + c.svg.select('.y') + .append('g') + .translate([yOffset, c.height/2]) + .append('text.axis-label') + .text(yText) + .at({textAnchor: 'middle', transform: 'rotate(-90)'}) +} + +function ggPlot(c, isBlack=true){ + c.svg.append('rect.bg-rect') + // .st({height: c.height, width: c.width, fill: '#eee'}).lower() + + c.svg.selectAll('.tick').selectAll('line').remove() + c.svg.selectAll('.y .tick') + .append('path').at({d: 'M 0 0 H ' + c.width, stroke: '#999', strokeWidth: .5}) + c.svg.selectAll('.y text').at({x: -3}) + c.svg.selectAll('.x .tick') + .append('path').at({d: 'M 0 0 V -' + c.height, stroke: '#999', strokeWidth: .5}) +} diff --git a/source/live-forecast-2022/share.png b/source/live-forecast-2022/share.png new file mode 100644 index 00000000..a0967cae Binary files /dev/null and b/source/live-forecast-2022/share.png differ diff --git a/source/live-forecast-2022/style.css b/source/live-forecast-2022/style.css new file mode 100644 index 00000000..00dfc35d --- /dev/null +++ b/source/live-forecast-2022/style.css @@ -0,0 +1,96 @@ +.tooltip { + top: -1000px; + position: fixed; + padding: 7px; + background: rgba(255, 255, 255, .94); + border: 1px solid lightgray; + pointer-events: none; + width: 300px; + font-size: 12px; + font-family: sans-serif; + line-height: 1.3em; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +.tooltip text{ + text-shadow: none; + fill: #000 !important; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.domain{ + display: none; +} + +text{ + /*pointer-events: none;*/ + text-shadow: 0 1px 0 #eee, 1px 0 0 #eee, 0 -1px 0 #eee, -1px 0 0 #eee; +} + +.axis text{ + fill: #777; +} + +#notes{ + font-family: sans-serif; + font-size: 12px; + opacity: .7; +} + + +.race{ + display: inline-block; +} + +.axis-label{ + font-size: 12px; + font-family: monospace; + fill: #777; +} + + +.slider{ + margin: 0px auto; + width: 250px; + margin-top: 20px; + margin-bottom: 20px; +} +input[type='range']{ + accent-color: #000; + width: 250px; + +} +.time{ + font-family: monospace; + text-align: center; +} + +.is-disabled{ + opacity: .4; +} +.is-disabled .axis path{ +/* opacity: 1;*/ +} + +html{ + background: #fff; +} + diff --git a/source/live-forecast-2024/init.js b/source/live-forecast-2024/init.js new file mode 100644 index 00000000..94ec46ba --- /dev/null +++ b/source/live-forecast-2024/init.js @@ -0,0 +1,187 @@ +console.clear() +d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + +var maxMargin = .2 +window.renderFns = [] + +d3.loadData('https://roadtolarissa.com/data/2024-wp.json', (err, [times]) => { + window.tidy = [] + + // times = times.filter(d => d.scrapeTime < '2024-11-09T11:38:35.067Z') + + times.forEach(time => { + time.races.forEach(race => { + race.scrapeTime = time.scrapeTime + tidy.push(race) + + if (race.leader?.party_caucus == 'DEM'){ + race.margin = race.margin.map(d => -d).reverse() + } + + if (race.wapo?.dem){ + var {dem, gop} = race.wapo + race.wapoMargin = [ + calcMargin(dem['upper_09'], gop['lower_09']), + calcMargin(dem.prediction, gop.prediction), + calcMargin(dem['lower_09'], gop['upper_09']), + ] + function calcMargin(d, r){ + return (r - d)/(d + r) + } + } else { + race.wapoMargin = [-maxMargin, 0, maxMargin] + race.isFakeWapo = true + } + }) + }) + + var byRace = d3.nestBy(tidy, d => d.state_id) + byRace = _.sortBy(byRace, d => d.key) + + drawChamber('all') + drawSlider() + + function renderIndex(index){ + renderFns.forEach(d => d(index)) + } + + function drawSlider(){ + var sel = d3.select('.slider').html('') + var lastIndex = times.length - 1 + + var timeSel = sel.append('div.time') + + var sliderSel = sel.append('input') + .at({ type:'range', min: 0, max: lastIndex, step: 1, value: lastIndex}) + .on('input', function(){ + renderIndex(this.value) + }) + + renderFns.push(curIndex => { + var str = new Date(times[curIndex].scrapeTime) + '' + str = str.split('GMT')[0] + timeSel.text(str) + }) + + renderIndex(lastIndex) + } + + + function drawChamber(chamber){ + var sel = d3.select('.chamber-' + chamber).html('') + + var width = innerWidth - 20 + var pWidth = d3.select('p').node().offsetWidth + var marginLeft = -(width - pWidth)/2 + sel.st({width, marginLeft}) + + var races = chamber != 'all' ? byRace.filter(d => d.key.includes(`G-${chamber.toUpperCase()}-`)) : byRace + races.forEach(race => drawRace(sel, race, width)) + } + + function drawRace(sel, race, chamberWidth){ + var nCols = innerWidth > 800 ? 6 : Math.ceil(chamberWidth/250) + var margin = {left: 30, right: 30, bottom: 40} + var chartSize = Math.floor(chamberWidth/nCols - margin.left - margin.right) + + var c = d3.conventions({ + sel: sel.append('div.race'), + width: chartSize, + height: chartSize, + margin, + }) + + c.x.domain([-maxMargin, maxMargin]) + c.y.domain([-maxMargin, maxMargin]) + + var tickFmt = d => { + if (!d) return '0%' + var str = d3.format('+.0%')(d) + if (str.includes(20)) return '' + return (d < 0 ? 'D ' : 'R ') + str.replace('-', '+') + } + c.xAxis.ticks(3).tickFormat(tickFmt) + c.yAxis.ticks(3).tickFormat(tickFmt) + d3.drawAxis(c) + ggPlot(c) + addAxisLabel(c, 'nyt', 'wapo') + + c.svg.selectAll('.tick').filter(d => d == 0).raise() + .select('path').st({strokeWidth: 1.5, stroke: '#000'}) + + var raceSlug = race.key.replace('-G', '').replace('-2024-11-08', '') + c.svg.append('text.race-title').text(raceSlug) + .at({textAnchor: 'middle', x: c.width/2, y: -2, fontSize: 14, fontFamily: 'monospace'}) + + c.svg.appendMany('circle', race) + .at({r: 1, fill: '#f0f'}) + .translate(d => [c.x(d.nyt[2]), c.y(d.wapoMargin[1])]) + + var line = d3.line() + .x(d => c.x(d.nyt[2])) + .y(d => c.y(d.wapoMargin[1])) + c.svg.append('path.data-el').at({d: line(race), fill: 'none', stroke: '#f0f', opacity: .3}) + + var rectSel = c.svg.append('rect.data-el').st({opacity: .15}) + var rectBgSel = c.svg.append('rect.data-el').st({opacity: .15}) + var circleSel = c.svg.append('circle.data-el').at({r: 3, stroke: '#f0f', strokeWidth: 2, fill: 'none'}) + + + + renderFns.push(curIndex => { + // if (raceSlug != 'AZ-S') return + var curData = race[curIndex] + if (!curData ){ + rectSel.st({opacity: 0}) + circleSel.st({opacity: 0}) + return + } + c.svg.classed('is-disabled', curData.isFakeWapo) + + var y = c.y(curData.wapoMargin[2]) + var height = c.y(curData.wapoMargin[0]) - y + + var x = c.x(curData.nyt[0]) + rectSel.at({y, height, x, width: c.x(curData.nyt[4]) - x}) + + var x = c.x(curData.nyt[1]) + rectBgSel.at({y, height, x, width: c.x(curData.nyt[3]) - x}) + + circleSel.at({cy: c.y(curData.wapoMargin[1]), cx: c.x(curData.nyt[2])}) + }) + + } +}) + + + + + + + +function addAxisLabel(c, xText, yText, xOffset=30, yOffset=-30){ + c.svg.select('.x').append('g') + .translate([c.width/2, xOffset]) + .append('text.axis-label') + .text(xText) + .at({textAnchor: 'middle'}) + + c.svg.select('.y') + .append('g') + .translate([yOffset, c.height/2]) + .append('text.axis-label') + .text(yText) + .at({textAnchor: 'middle', transform: 'rotate(-90)'}) +} + +function ggPlot(c, isBlack=true){ + c.svg.append('rect.bg-rect') + // .st({height: c.height, width: c.width, fill: '#eee'}).lower() + + c.svg.selectAll('.tick').selectAll('line').remove() + c.svg.selectAll('.y .tick') + .append('path').at({d: 'M 0 0 H ' + c.width, stroke: '#999', strokeWidth: .5}) + c.svg.selectAll('.y text').at({x: -3}) + c.svg.selectAll('.x .tick') + .append('path').at({d: 'M 0 0 V -' + c.height, stroke: '#999', strokeWidth: .5}) +} diff --git a/source/live-forecast-2024/share.png b/source/live-forecast-2024/share.png new file mode 100644 index 00000000..7d4783e2 Binary files /dev/null and b/source/live-forecast-2024/share.png differ diff --git a/source/live-forecast-2024/style.css b/source/live-forecast-2024/style.css new file mode 100644 index 00000000..625413e4 --- /dev/null +++ b/source/live-forecast-2024/style.css @@ -0,0 +1,95 @@ +.tooltip { + top: -1000px; + position: fixed; + padding: 7px; + background: rgba(255, 255, 255, .94); + border: 1px solid lightgray; + pointer-events: none; + width: 300px; + font-size: 12px; + font-family: sans-serif; + line-height: 1.3em; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +.tooltip text{ + text-shadow: none; + fill: #000 !important; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.domain{ + display: none; +} + +text{ + /*pointer-events: none;*/ +/* text-shadow: 0 1px 0 #eee, 1px 0 0 #eee, 0 -1px 0 #eee, -1px 0 0 #eee;*/ +} + +.axis text{ + fill: #777; +} + +#notes{ + font-family: sans-serif; + font-size: 12px; + opacity: .7; +} + + +.race{ + display: inline-block; +} + +.axis-label{ + font-size: 12px; + font-family: monospace; + fill: #777; +} + + +.slider{ + margin: 0px auto; + width: 250px; + margin-top: 20px; + margin-bottom: 20px; +} +input[type='range']{ + accent-color: #000; + width: 250px; +} +.time{ + font-family: monospace; + text-align: center; +} + +.is-disabled{ + opacity: .4; +} +.is-disabled .axis path{ +/* opacity: 1;*/ +} + +html{ + background: #fff; +} + diff --git a/source/oracle/script.js b/source/oracle/script.js index 81f57114..16777ec7 100644 --- a/source/oracle/script.js +++ b/source/oracle/script.js @@ -229,7 +229,7 @@ function toLR(d){ return +d ? '→' : '←' } '10101100101010' .split('') - .forEach(d => update(+d)) + // .forEach(d => update(+d)) diff --git a/source/paper-reading/_script.js b/source/paper-reading/_script.js new file mode 100644 index 00000000..64431eac --- /dev/null +++ b/source/paper-reading/_script.js @@ -0,0 +1,25 @@ +d3.csv('https://www.googleapis.com/drive/v3/files/1Eq6G-zkMIhrSMPwqvCqM7I0FpqRsw2iwWO01X_t_XVE/export?mimeType=text/csv&key=AIzaSyAT-ALGW_bcmcvNs1dPgcV7fF6tR1vKY44', (err, res) => { + if (err) return console.log(err) + data = res.reverse() + + console.log(data) + + d3.select('#list') + .st({marginBottom: 60}) + .html('').appendMany('div', data) + .st({marginBottom: 20, marginTop: 20}) + .append('a') + .text(d => d.title) + .at({href: d => d.link || d.pdf}) + .st({opacity: d => d.read ? .5 : 1, textDecoration: 'none'}) +}) + + +// https://www.googleapis.com/download/drive/v2/files/2PACX-1vSwFBbjJCU-J_QI2wUJXXfOmHxXRg7U90PbgmOYCfy8beA8eZxjUSICZOB3G0TAx6TgtpNVfIe6GgU3?alt=media + + + +// d3.csv('https://www.googleapis.com/drive/v3/files/1Eq6G-zkMIhrSMPwqvCqM7I0FpqRsw2iwWO01X_t_XVE/export?mimeType=text/csv&key=AIzaSyAT-ALGW_bcmcvNs1dPgcV7fF6tR1vKY44', (err, res) => { +// console.log(err) +// console.log(res) +// }) \ No newline at end of file diff --git a/source/paper-reading/style.css b/source/paper-reading/style.css new file mode 100644 index 00000000..b137680b --- /dev/null +++ b/source/paper-reading/style.css @@ -0,0 +1,152 @@ +#container{ + font-family: monaco, Consolas, 'Lucida Console', monospace; +} + +body{ + margin-bottom: -20px; +} +html.is-no-bar{ + overflow: hidden; + width:100%; +} +.is-no-bar body{ + height:100%; + position:fixed; + overflow-y:scroll; + -webkit-overflow-scrolling: touch; +} + +#container{ + margin: 0px auto; + position: relative; +} + +#panel{ + top: 0px; + width: 180px; + display: inline-block; + padding-bottom: 80vh; + font-size: 10px; + line-height: 15px; +} + +#graph{ + width: calc(100% - 180px); + height: 100vh; + position: -webkit-sticky; + position: sticky; + top: -1px; + display: inline-block; + float: right; +} + + +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; + width: 200px; + opacity: 0; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + + +.axis{ + opacity: .7; +} +.domain{ + display: none; +} + +text{ + cursor: default; + /*pointer-events: none;*/ + /*text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;*/ +} + +text.movie{ + opacity: .5; + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; +} + +text.top.movie{ + fill: #f0f; + opacity: 1; +} + + +path.movie{ + stroke: #000; + fill: none; + stroke-width: 2; + opacity: .3; + transition: opacity 1ss; + transition: stroke 1s; +} + +.top path.movie{ + stroke: #f0f; + opacity: 1; +} + +text.movie-hover{ + font-size: 10px; + opacity: 0; + transition: opacity .5s; +} + + +.top text.movie-hover{ + font-weight: 800; + + opacity: 1 !important; +} + +g:hover > path.movie{ + stroke-width: 5; + opacity: 1; +} + +g:hover > text.movie-hover{ + opacity: 1; + transition: opacity 0s; + opacity: 1 !important; + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; + font-weight: 800; + font-size: 13px; +} + + +.hide{ + display: none; +} + + +h1{ + font-size: 29px; + margin-bottom: 0px; +} + + diff --git a/source/pitchfork/albums.tsv b/source/pitchfork/albums.tsv new file mode 100644 index 00000000..5361d132 --- /dev/null +++ b/source/pitchfork/albums.tsv @@ -0,0 +1,855 @@ +rank date genre slug artist album spotify year +50 April 14, 2009 Lo-Fi Woods - Songs of Shame Woods Songs of Shame 3rzVaWFBzvC7Yg6KSC7cVx 2009 +49 June 2, 2009 Folk Cass McCombs - Catacombs Cass McCombs Catacombs 2khhyKTcinEqcjIKetZD6X 2009 +48 March 24, 2009 Hip Hop DOOM - Born Like This DOOM Born Like This 3uIl85UIv3OaNRYbz7mrnd 2009 +47 December 9, 2008 Dubstep Zomby - Where Were U in 92? Zomby Where Were U in 92? 5duVh1qXfn9Sb0R7y6eo7r 2009 +46 March 24, 2009 Indietronica Dan Deacon - Bromst Dan Deacon Bromst 6SsP7QnagGoYqMRsxryQu5 2009 +45 October 6, 2009 Indie Rock The Mountain Goats - The Life of the World to Come The Mountain Goats The Life of the World to Come 7myOX1aVACQg3tnm62U6SI 2009 +44 March 19, 2009 Freak Folk tUnE-yArDs - BiRd-BrAiNs tUnE-yArDs BiRd-BrAiNs 1EwM5spoTKiTz25if5DEER 2009 +43 January 20, 2009 Indie Rock Cymbals Eat Guitars - Why There Are Mountains Cymbals Eat Guitars Why There Are Mountains 1BqKOalH2iJdlPL7pBrf8W 2009 +42 September 15, 2009 Shoegaze A Sunny Day in Glasgow - Ashes Grammar A Sunny Day in Glasgow Ashes Grammar 6KVEo7d6PJxpJqjuZVfWlh 2009 +41 October 13, 2009 Sludge Metal Baroness - Blue Record Baroness Blue Record 489qYkpzu44u4I6W9Ldtsx 2009 +40 June 9, 2009 Hip Hop Mos Def - The Ecstatic Mos Def The Ecstatic 5Oa2WgO3Jfuw2IKYrZNzTi 2009 +39 September 8, 2009 Experimental Jim O'Rourke - The Visitor Jim O'Rourke The Visitor 2009 +38 June 16, 2009 Dancehall Major Lazer - Guns Don't Kill People...Lazers Do Major Lazer Guns Don't Kill People...Lazers Do 51mR8ZUahC3H8V6j9Guq65 2009 +37 March 3, 2009 Indie Rock The Antlers - Hospice The Antlers Hospice 6fFp2F91noBeodV88bRwTD 2009 +36 June 23, 2009 Alternative Rock Dinosaur Jr. - Farm Dinosaur Jr. Farm 6IJJRVh2DWLxyZwVCi3ImK 2009 +35 July 1, 2009 Electronic JJ - jj n° 2 JJ jj n° 2 0J0rFQDKn8v8EfAoRJ9Ojs 2009 +34 May 19, 2009 Electronic Passion Pit - Manners Passion Pit Manners 6H51jH1SuzV6ca1VxW2Tmv 2009 +33 June 23, 2009 Experimental Bibio - Ambivalence Avenue Bibio Ambivalence Avenue 7ybrct8gCd1mWsHS32ID8w 2009 +32 October 20, 2009 Experimental Rock Bear in Heaven - Beast Rest Forth Mouth Bear in Heaven Beast Rest Forth Mouth 4I84eFKsYX024qD05okY1x 2009 +31 May 26, 2009 Drone Metal Sunn O))) - Monoliths and Dimensions Sunn O))) Monoliths and Dimensions 2009 +30 March 23, 2009 Electronic Röyksopp - Junior Röyksopp Junior 6vQMbwthchxuSioACn2hcE 2009 +29 September 8, 2009 Indie Rock Yo La Tengo - Popular Songs Yo La Tengo Popular Songs 5Vi8AMs747kpnUAJRrGx0p 2009 +28 March 9, 2009 Experimental Micachu and the Shapes - Jewellery Micachu and the Shapes Jewellery 06brN4S472isNabA18BfJn 2009 +27 October 27, 2009 Dubstep Various Artists - 5: Five Years of Hyperdub Various Artists 5: Five Years of Hyperdub 6a6PyGv5ZB1kR8QPV5aLHy 2009 +26 January 20, 2009 Indie Folk Bon Iver - Blood Bank EP Bon Iver Blood Bank EP 4LJ5oHS5UGQCC3p7NqYxuB 2009 +25 June 9, 2009 Hip Hop DJ Quik & Kurupt - Blaqkout DJ Quik & Kurupt Blaqkout 1fCu4LGOOCv1PvSdsoamLW 2009 +24 April 14, 2009 Folk Bill Callahan - Sometimes I Wish We Were An Eagle Bill Callahan Sometimes I Wish We Were An Eagle 2jalkvKIskN3GecqZAuqam 2009 +23 August 25, 2009 Electronic Memory Tapes - Seek Magic Memory Tapes Seek Magic 6ZchCmgi5vVSOBc1asPQKB 2009 +22 September 8, 2009 Indie Rock Wild Beasts - Two Dancers Wild Beasts Two Dancers 6Eoj1zHUY3VYUocKZVCawO 2009 +21 March 3, 2009 Alt-Country Neko Case - Middle Cyclone Neko Case Middle Cyclone 3XwQUKck8AiuLlOVubM5J5 2009 +20 November 17, 2009 Lo-Fi Real Estate - Real Estate Real Estate Real Estate 43uj7422MLR9MRBXSki0El 2009 +19 February 3, 2009 Indie Pop The Pains of Being Pure at Heart - The Pains of Being Pure at Heart The Pains of Being Pure at Heart The Pains of Being Pure at Heart 5uWuwlHON5texRWxdgtiS2 2009 +18 October 20, 2009 Psychedelic Pop Atlas Sound - Logos Atlas Sound Logos 6hQrWdihAPfww18lmdCKFl 2009 +17 October 6, 2009 Electronic The Very Best - Warm Heart of Africa The Very Best Warm Heart of Africa 6DQo1w3d4F8MXLK1585Dc7 2009 +16 January 20, 2009 Chamber Pop Antony and the Johnsons - The Crying Light Antony and the Johnsons The Crying Light 1pC69gZrIpeghDk2pkXbn8 2009 +15 April 28, 2009 Garage Rock Japandroids - Post-Nothing Japandroids Post-Nothing 3pukt8UD5RDPxUEdiOB7O2 2009 +14 October 13, 2009 Chillwave Neon Indian - Psychic Chasms Neon Indian Psychic Chasms 01QiJw4cwNbb5yHl265uhS 2009 +13 May 5, 2009 Indie Pop St. Vincent - Actor St. Vincent Actor 6RdfrSuuoZBUcvVHlWW2Wd 2009 +12 March 31, 2009 Indie Rock Yeah Yeah Yeahs - It's Blitz! Yeah Yeah Yeahs It's Blitz! 5HBmdEPIzWtcWwH2JSv7go 2009 +11 October 20, 2009 Electronic Fuck Buttons - Tarot Sport Fuck Buttons Tarot Sport 7tilRUU7WANATJdxhMO3p8 2009 +10 September 22, 2009 Indie Pop Girls - Album Girls Album 4oo6giAIivkoxt9ZDj4FmY 2009 +9 January 12, 2009 Electropop Fever Ray - Fever Ray Fever Ray Fever Ray 17RbukXB7NQN8h59WAy9ql 2009 +8 May 26, 2009 Indie Pop Phoenix - Wolfgang Amadeus Phoenix Phoenix Wolfgang Amadeus Phoenix 7bJTscIEKaObZS61RmpviI 2009 +7 April 7, 2009 Art Pop Bat For Lashes - Two Suns Bat For Lashes Two Suns 7cj1dERc5yhFBqtxlRYGSe 2009 +6 May 26, 2009 Psychedelic Folk Grizzly Bear - Veckatimest Grizzly Bear Veckatimest 6FIFqclBriPCb0SjWDaHIk 2009 +5 September 8, 2009 Hip Hop Raekwon - Only Built 4 Cuban Linx... Pt. II Raekwon Only Built 4 Cuban Linx... Pt. II 2009 +4 October 13, 2009 Neo-Psychedelia The Flaming Lips - Embryonic The Flaming Lips Embryonic 1QaudSbWNyACrk0JGemI3r 2009 +3 October 6, 2009 Indie Pop The xx - xx The xx xx 2rmMeEq5D1Bg7YFRwtHBDr 2009 +2 June 9, 2009 Indie Pop Dirty Projectors - Bitte Orca Dirty Projectors Bitte Orca 5370y6sLDhvjsg5eaQpIB4 2009 +1 January 20, 2009 Psychedelic Pop Animal Collective - Merriweather Post Pavilion Animal Collective Merriweather Post Pavilion 3Ew40olMfd5X4BvqfuFoqF 2009 +100 March 2, 2011 Electronic Clams Casino - Instrumental Clams Casino Instrumental 2010-2014 +99 April 1, 2013 Alternative R&B Jai Paul - Jai Paul Jai Paul Jai Paul 2010-2014 +98 March 31, 2010 Hip Hop Earl Sweatshirt - EARL Earl Sweatshirt EARL 2010-2014 +97 October 11, 2011 Electronic Rustie - Glass Swords Rustie Glass Swords 13kdLV4OxOBbqnGcJqkcKS 2010-2014 +96 February 23, 2013 Hip Hop Young Thug - 1017 Thug Young Thug 1017 Thug 2010-2014 +95 January 31, 2011 Microhouse Nicolas Jaar - Space Is Only Noise Nicolas Jaar Space Is Only Noise 0tUJcqDuXHNkaPKLN0lQhT 2010-2014 +94 June 24, 2014 Alternative R&B How to Dress Well - What Is This Heart? How to Dress Well What Is This Heart? 70o7vRADMOz75vzHBla3Z0 2010-2014 +93 February 15, 2011 Drone Tim Hecker - Ravedeath, 1972 Tim Hecker Ravedeath, 1972 6cbCvUzsawFcG0aZVCJyzt 2010-2014 +92 October 15, 2012 Art Pop Bat For Lashes - The Haunted Man Bat For Lashes The Haunted Man 6rg92rv1hTB5fDfm85cWgq 2010-2014 +91 May 28, 2010 Microhouse Actress - Splazsh Actress Splazsh 5iMp6bIMnBg0UpouIXUqxX 2010-2014 +90 March 5, 2013 Singer-Songwriter Waxahatchee - Cerulean Salt Waxahatchee Cerulean Salt 4OfohS2lFQOeCXsrNcdh5s 2010-2014 +89 June 8, 2010 Drone Emeralds - Does It Look Like I'm Here? Emeralds Does It Look Like I'm Here? 0c0XkZh9vWXg473nchoArI 2010-2014 +88 October 29, 2013 Indie Rock Arcade Fire - Reflektor Arcade Fire Reflektor 4E0m7AIVc2d2QZMrMNXdMZ 2010-2014 +87 August 12, 2014 Alternative R&B FKA twigs - LP1 FKA twigs LP1 7mpQVIMZ2iNnAfqodqvdhz 2010-2014 +86 March 4, 2014 Indie Rock Real Estate - Atlas Real Estate Atlas 50idJkY6FhqUesGnGHn7wB 2010-2014 +85 May 13, 2014 Experimental Rock Swans - To Be Kind Swans To Be Kind 1Nt5SpidxOjgIpTrhrfwAF 2010-2014 +84 October 31, 2011 Hip Hop A$AP Rocky - Live. Love. A$AP A$AP Rocky Live. Love. A$AP 2010-2014 +83 May 25, 2010 Psychedelic Rock Tame Impala - Innerspeaker Tame Impala Innerspeaker 51jAc3sHdN2YG1X5AZg6Ow 2010-2014 +82 December 18, 2012 Hip Hop Chief Keef - Finally Rich Chief Keef Finally Rich 108j4R9oEcFUaVRzE8RZKI 2010-2014 +81 August 21, 2012 Psychedelic Pop Ariel Pink's Haunted Graffiti - Mature Themes Ariel Pink's Haunted Graffiti Mature Themes 2TIa83EYZBDWfmEj6VVoWu 2010-2014 +80 October 16, 2012 Singer-Songwriter Mac DeMarco - 2 Mac DeMarco 2 7mQ1elwuFdy0LBjAxv4spP 2010-2014 +79 July 6, 2010 Hip Hop Big Boi - Sir Lucious Left Foot: The Son of Chico Dusty Big Boi Sir Lucious Left Foot: The Son of Chico Dusty 0GZMsKdDsbNhxvM1VaxB0U 2010-2014 +78 October 8, 2013 Art Pop Darkside - Psychic Darkside Psychic 7jqNrm1l4wSxNYSjgK7tmF 2010-2014 +77 July 2, 2011 Hip Hop Kendrick Lamar - Section.80 Kendrick Lamar Section.80 13WjgUEEAQp0d9JqojlWp1 2010-2014 +76 August 20, 2012 Soul Jessie Ware - Devotion Jessie Ware Devotion 467ptCjichGtFduanf3jMX 2010-2014 +75 June 21, 2011 Ambient The Caretaker - An Empty Bliss Beyond This World The Caretaker An Empty Bliss Beyond This World 2010-2014 +74 April 20, 2010 Electronic Caribou - Swim Caribou Swim 74vgQmQX8dVxpw9zcPEFxE 2010-2014 +73 January 24, 2012 Indie Rock Cloud Nothings - Attack on Memory Cloud Nothings Attack on Memory 1JgpvdsSfE94eZzYBk6ph9 2010-2014 +72 October 8, 2013 Hip Hop Danny Brown - OLD Danny Brown OLD 5SC0415RIGVX9ZfL0tfbAl 2010-2014 +71 September 13, 2011 Indie Pop Girls - Father, Son, Holy Ghost Girls Father, Son, Holy Ghost 66wRO7SK0Wo1KS40en2tua 2010-2014 +70 May 15, 2012 Hip Hop Killer Mike - R.A.P. Music Killer Mike R.A.P. Music 5EAhUoAz1G3WTvIfGZvmrh 2010-2014 +69 November 18, 2013 R&B Blood Orange - Cupid Deluxe Blood Orange Cupid Deluxe 5Pn6Nexd7zbCMkdVGeml0m 2010-2014 +68 March 30, 2010 Neo-Soul Erykah Badu - New Amerykah Part Two: Return of the Ankh Erykah Badu New Amerykah Part Two: Return of the Ankh 1MOub955Uer957RVqqkF2a 2010-2014 +67 March 21, 2011 R&B The Weeknd - House of Balloons The Weeknd House of Balloons 2010-2014 +66 April 8, 2014 Nu-Disco Todd Terje - It's Album Time Todd Terje It's Album Time 4pefQ21iSk8hdnxw3WSB5Y 2010-2014 +65 May 25, 2010 Electronic Crystal Castles - Crystal Castles Crystal Castles Crystal Castles 75olNPlDKi2XQhTD9IPlVC 2010-2014 +64 September 30, 2013 Indie Rock HAIM - Days Are Gone HAIM Days Are Gone 0eXAgjcMHbFt3fS0XXp8Kw 2010-2014 +63 January 26, 2010 Electronic Four Tet - There Is Love in You Four Tet There Is Love in You 6x2gG7Pw1g54ZjQWjiAVCK 2010-2014 +62 April 8, 2013 Electronic The Knife - Shaking the Habitual The Knife Shaking the Habitual 7eDaaP6J8Q3cYVTGRmARwx 2010-2014 +61 July 10, 2012 Art Pop Dirty Projectors - Swing Lo Magellan Dirty Projectors Swing Lo Magellan 1WhF5ZiI5V5laq2nGLoF99 2010-2014 +60 February 15, 2011 Alternative Rock PJ Harvey - Let England Shake PJ Harvey Let England Shake 7f1aXd7Gd5H9IqFu36zw6m 2010-2014 +59 October 2, 2012 R&B Miguel - Kaleidoscope Dream Miguel Kaleidoscope Dream 6vgPmXpCwKpPjV6fLynFtH 2010-2014 +58 June 29, 2010 R&B The-Dream - Love King The-Dream Love King 08912w1sEZbVmc4XZr40JE 2010-2014 +57 April 19, 2011 Art Pop tUnE-yArDs - w h o k i l l tUnE-yArDs w h o k i l l 7rBLvpL7ZWi1YCSXSLUZKF 2010-2014 +56 April 30, 2013 Hip Hop Chance The Rapper - Acid Rap Chance The Rapper Acid Rap 2VBcztE58pBKjIDS5oEgFh 2010-2014 +55 October 29, 2012 Dub Techno Andy Stott - Luxury Problems Andy Stott Luxury Problems 6XpD4GhQq4olgxxgX6BXdv 2010-2014 +54 February 18, 2011 R&B Frank Ocean - Nostalgia, Ultra. Frank Ocean Nostalgia, Ultra. 2010-2014 +53 May 15, 2012 Dream Pop Beach House - Bloom Beach House Bloom 68pikiNcGlWj3XFirs5O2R 2010-2014 +52 February 8, 2011 Art Pop James Blake - James Blake James Blake James Blake 0qY6lBQSi8IMJjHYDPdAqX 2010-2014 +51 October 29, 2013 Synthpop Sky Ferreira - Night Time, My Time Sky Ferreira Night Time, My Time 1bvCVYPVl445mO690M2dOr 2010-2014 +50 October 16, 2012 Post-Rock Godspeed You! Black Emperor - 'Allelujah! Don't Bend! Ascend! Godspeed You! Black Emperor 'Allelujah! Don't Bend! Ascend! 30bTXMxsMsYn7QbwmDW3rj 2010-2014 +49 November 8, 2011 Drone Oneohtrix Point Never - Replica Oneohtrix Point Never Replica 6zUTN3bN8rsgdy1lsFXI7L 2010-2014 +48 May 21, 2013 Ambient Pop Majical Cloudz - Impersonator Majical Cloudz Impersonator 70EudAo73w9QvcF2u5K18E 2010-2014 +47 October 5, 2010 Hip Hop Waka Flocka Flame - Flockaveli Waka Flocka Flame Flockaveli 6MQtWELG7aRX7CkAzQ6nLM 2010-2014 +46 January 12, 2010 Indie Rock Vampire Weekend - Contra Vampire Weekend Contra 0zeAijecFGZOS4OaRdPVz5 2010-2014 +45 April 17, 2012 Hip Hop Future - Pluto Future Pluto 1yNIBzlvPVBALSPkUMq1ma 2010-2014 +44 March 26, 2012 Synthpop Chromatics - Kill for Love Chromatics Kill for Love 5Rz4NIqwoYGDbIh40UTylV 2010-2014 +43 August 8, 2011 Hip Hop Kanye West & Jay-Z - Watch the Throne Kanye West & Jay-Z Watch the Throne 1YwzJz7CrV9fd9Qeb6oo1d 2010-2014 +42 April 9, 2013 Folk Rock Kurt Vile - Wakin on a Pretty Daze Kurt Vile Wakin on a Pretty Daze 2imxOfDIDk2voAYCZP88u2 2010-2014 +41 September 24, 2013 Hip Hop Drake - Nothing Was the Same Drake Nothing Was the Same 2ZUFSbIkmFkGag000RWOpA 2010-2014 +40 December 16, 2013 Dubstep Burial - Rival Dealer Burial Rival Dealer 7Mpnu24WcUfiCw9sGrdp2K 2010-2014 +39 June 28, 2011 Pop Beyoncé - 4 Beyoncé 4 7cvVgT5RXbcUo9Qw4nq31D 2010-2014 +38 May 10, 2011 Experimental EMA - Past Life Martyred Saints EMA Past Life Martyred Saints 08OhqUM0MeAuHd9HdryHB4 2010-2014 +37 August 15, 2011 Hip Hop Danny Brown - XXX Danny Brown XXX 3j21GO4l4W0w7hdF8JVPmo 2010-2014 +36 November 22, 2010 Electropop Robyn - Body Talk Robyn Body Talk 7J4oxoeFQLTrHnjNu2ZaJ5 2010-2014 +35 October 22, 2013 Footwork DJ Rashad - Double Cup DJ Rashad Double Cup 4J7qkorMbPmJQy79SntDA8 2010-2014 +34 April 21, 2012 Hip Hop Death Grips - The Money Store Death Grips The Money Store 1PQDjdBpHPikAodJqjzm6a 2010-2014 +33 August 3, 2010 Indie Rock Arcade Fire - The Suburbs Arcade Fire The Suburbs 4zJBuQXo92Q7QhA5U4V8kw 2010-2014 +32 March 8, 2011 Folk Rock Kurt Vile - Smoke Ring for My Halo Kurt Vile Smoke Ring for My Halo 32a7BrNNTAu7BVb6DcsMLP 2010-2014 +31 May 21, 2013 Nu-Disco Daft Punk - Random Access Memories Daft Punk Random Access Memories 4m2880jivSbbyEGAKfITCa 2010-2014 +30 March 9, 2010 Indie Rock Titus Andronicus - The Monitor Titus Andronicus The Monitor 5U09FQWagzAU5HPmufUAlU 2010-2014 +29 June 5, 2012 Garage Rock Japandroids - Celebration Rock Japandroids Celebration Rock 2sY9WYVH022ulyAYaqvXLW 2010-2014 +28 February 3, 2013 Shoegaze My Bloody Valentine - m b v My Bloody Valentine m b v 2010-2014 +27 June 21, 2011 Indie Folk Bon Iver - Bon Iver, Bon Iver Bon Iver Bon Iver, Bon Iver 0ZMzEAuUIylHgetdWqzcHU 2010-2014 +26 June 11, 2013 Blackgaze Deafheaven - Sunbather Deafheaven Sunbather 2kKXGWaCEl06EKZ4DxBJIT 2010-2014 +25 April 5, 2011 Singer-Songwriter Bill Callahan - Apocalypse Bill Callahan Apocalypse 4RBHgVw8kR6IpXVnZhZrh0 2010-2014 +24 June 3, 2013 Electronic Disclosure - Settle Disclosure Settle 1bEnTsCvG4FPW7NpIEC5Zc 2010-2014 +23 September 13, 2011 Art Pop St. Vincent - Strange Mercy St. Vincent Strange Mercy 1Lci4bx7JIuCC8pnBNX7ds 2010-2014 +22 October 18, 2011 Dream Pop M83 - Hurry Up, We're Dreaming M83 Hurry Up, We're Dreaming 6MBuQugGuX7VMBX0uiBnAQ 2010-2014 +21 March 18, 2014 Heartland Rock The War on Drugs - Lost in the Dream The War on Drugs Lost in the Dream 6nD9jfVVX0OadsHJqmjObO 2010-2014 +20 June 8, 2010 Psychedelic Pop Ariel Pink's Haunted Graffiti - Before Today Ariel Pink's Haunted Graffiti Before Today 1dO7qBlkQXYENJaHfK7h56 2010-2014 +19 May 4, 2010 IDM Flying Lotus - Cosmogramma Flying Lotus Cosmogramma 5c7XChrHxYaqykCZLaGM5f 2010-2014 +18 February 23, 2010 Indie Folk Joanna Newsom - Have One on Me Joanna Newsom Have One on Me 2010-2014 +17 August 28, 2012 Post-Rock Swans - The Seer Swans The Seer 1ijaW4mgV7Xz7DK5cXyjoo 2010-2014 +16 January 25, 2011 Sophisti-Pop Destroyer - Kaputt Destroyer Kaputt 1clYDgHxfhzxWQJH0ieRpx 2010-2014 +15 February 21, 2012 Synthpop Grimes - Visions Grimes Visions 48a7rOjTzpD1zzJAteeveE 2010-2014 +14 December 13, 2013 R&B Beyoncé - Beyoncé Beyoncé Beyoncé 2noKUZhXwUhPQMgSr56T4G 2010-2014 +13 October 18, 2011 Indie Rock Real Estate - Days Real Estate Days 43uj7422MLR9MRBXSki0El 2010-2014 +12 November 15, 2011 Hip Hop Drake - Take Care Drake Take Care 63WdJvk8G9hxJn8u5rswNh 2010-2014 +11 May 18, 2010 Dance Punk LCD Soundsystem - This Is Happening LCD Soundsystem This Is Happening 4hnqM0JK4CM1phwfq1Ldyz 2010-2014 +10 June 19, 2012 Singer-Songwriter Fiona Apple - The Idler Wheel Fiona Apple The Idler Wheel 7uccppFArLf9dNOwDftOZa 2010-2014 +9 February 11, 2014 Singer-Songwriter Sun Kil Moon - Benji Sun Kil Moon Benji 4pC2URLdvle8V6Um4qxh46 2010-2014 +8 June 18, 2013 Hip Hop Kanye West - Yeezus Kanye West Yeezus 0XTAmejG8F97wF5MWoVbaY 2010-2014 +7 October 9, 2012 Psychedelic Rock Tame Impala - Lonerism Tame Impala Lonerism 76vY9yohh4kVwSKkyKbyEQ 2010-2014 +6 May 14, 2013 Indie Rock Vampire Weekend - Modern Vampires of the City Vampire Weekend Modern Vampires of the City 2Qi2SySN2ePZwMLDSv9Krn 2010-2014 +5 January 26, 2010 Dream Pop Beach House - Teen Dream Beach House Teen Dream 72mGz9Dnt42euozq8yBULe 2010-2014 +4 July 17, 2012 Alternative R&B Frank Ocean - Channel Orange Frank Ocean Channel Orange 623Ef2ZEB3Njklix4PC0Rs 2010-2014 +3 September 28, 2010 Indie Rock Deerhunter - Halcyon Digest Deerhunter Halcyon Digest 1HUMjB15ARg96KIypcGzYY 2010-2014 +2 October 22, 2012 Hip Hop Kendrick Lamar - good kid, m.A.A.d. city Kendrick Lamar good kid, m.A.A.d. city 1DqhWr73Fh5yoNzKLas0G3 2010-2014 +1 November 22, 2010 Hip Hop Kanye West - My Beautiful Dark Twisted Fantasy Kanye West My Beautiful Dark Twisted Fantasy 6LBiuhK7PZKjVXyMfPxPoh 2010-2014 +200 April 8, 2014 Hip Hop Ratking - So It Goes Ratking So It Goes 2ScS2EpJi2qDVFAQ2DFBq0 2010-2019 +199 June 21, 2011 Lo-Fi WU LYF - Go Tell Fire to the Mountain WU LYF Go Tell Fire to the Mountain 6WSEHis4NH2JfCimtDhClj 2010-2019 +198 March 30, 2018 Hip Hop Jean Grae & Quelle Chris - Everything's Fine Jean Grae & Quelle Chris Everything's Fine 22oHrB4SLwayyuLE02t5BD 2010-2019 +197 November 18, 2011 Fatima Al Qadiri - Genre-Specific Xperience Fatima Al Qadiri Genre-Specific Xperience 15MfIAm3A7U0W1myJaXye2 2010-2019 +196 February 19, 2013 Death Metal Portal - Vexovoid Portal Vexovoid 2010-2019 +195 May 4, 2015 Hardcore Punk Downtown Boys - Full Communism Downtown Boys Full Communism 2uOinTd9KHYUdQR5r3sDKv 2010-2019 +194 March 9, 2010 Indie Rock Titus Andronicus - The Monitor Titus Andronicus The Monitor 5U09FQWagzAU5HPmufUAlU 2010-2019 +193 September 25, 2016 Hip Hop Lil Peep - HellBoy Lil Peep HellBoy 2010-2019 +192 October 1, 2013 R&B Kelela - Cut 4 Me Kelela Cut 4 Me 0pnKj3DOUFmo0bpQVkyE3Z 2010-2019 +191 November 21, 2011 Art Pop Kate Bush - 50 Words for Snow Kate Bush 50 Words for Snow 1VAB3Xn92dPKPWzocgQqkh 2010-2019 +190 June 10, 2016 Ambient Huerco S. - For Those Of You Who Have Never (And Also Those Who Have) Huerco S. For Those Of You Who Have Never (And Also Those Who Have) 1MATu35vnVRNkhxFH3hMxP 2010-2019 +189 November 18, 2016 Country Miranda Lambert - The Weight of These Wings Miranda Lambert The Weight of These Wings 563h536tB6n8Dn62jr4RZG 2010-2019 +188 May 25, 2018 Hip Hop Pusha T - Daytona Pusha T Daytona 07bIdDDe3I3hhWpxU6tuBp 2010-2019 +187 May 4, 2018 Post-Punk Iceage - Beyondless Iceage Beyondless 5H8wFFblf7YvGc7LbBzuR9 2010-2019 +186 March 17, 2017 Ambient Various Artists - Mono No Aware Various Artists Mono No Aware 29oEVx1KwMEjdK0bsT2IZu 2010-2019 +185 February 16, 2018 Jazz Hailu Mergia - Lala Belu Hailu Mergia Lala Belu 5daH3Ogui4BqvMrMLaEJ2Q 2010-2019 +184 March 2, 2018 Indie Pop Soccer Mommy - Clean Soccer Mommy Clean 36NLDBi2kX7XRHnyLzLOS8 2010-2019 +183 August 7, 2015 Electronic Elysia Crampton - American Drift Elysia Crampton American Drift 6pArfwsBoFXoIcjNdmmfjN 2010-2019 +182 June 13, 2016 Hardcore Punk G.L.O.S.S. - Trans Day of Revenge G.L.O.S.S. Trans Day of Revenge 2010-2019 +181 May 10, 2019 Alternative R&B Jamila Woods - LEGACY! LEGACY! Jamila Woods LEGACY! LEGACY! 5NzK7S7oQQnO8eLRf7kDJx 2010-2019 +180 September 24, 2013 Synthpop CHVRCHES - The Bones of What You Believe CHVRCHES The Bones of What You Believe 1l9bp6Fz4xkN673dWN5cpy 2010-2019 +179 June 28, 2011 Hip Hop Shabazz Palaces - Black Up Shabazz Palaces Black Up 6xmJwIZr8GXrSTiYa9UYXG 2010-2019 +178 May 6, 2013 Post-Punk Savages - Silence Yourself Savages Silence Yourself 0aMC5DDAF86GvYNPaivEKd 2010-2019 +177 October 20, 2017 Country Margo Price - All American Made Margo Price All American Made 2ZxlcZ2NMgupfqGcyjnmkE 2010-2019 +176 February 9, 2010 Soul Gil Scott-Heron - I'm New Here Gil Scott-Heron I'm New Here 60JXrFsIxXP6rqd4jdTfrn 2010-2019 +175 August 20, 2012 Soul Jessie Ware - Devotion Jessie Ware Devotion 467ptCjichGtFduanf3jMX 2010-2019 +174 May 27, 2014 Singer-Songwriter Sharon Van Etten - Are We There Sharon Van Etten Are We There 69EhwEgjEWQ8GqH7wqEndn 2010-2019 +173 February 11, 2014 Singer-Songwriter Hurray For The Riff Raff - Small Town Heroes Hurray For The Riff Raff Small Town Heroes 1POMK00nh13k92KaCIVljU 2010-2019 +172 June 10, 2014 Dancehall Popcaan - Where We Come From Popcaan Where We Come From 3KxV4BKuBa5MuoETwALcUc 2010-2019 +171 January 19, 2018 Experimental Hip Hop JPEGMAFIA - Veteran JPEGMAFIA Veteran 22LKdgY3vLsAsWrOafwCM3 2010-2019 +170 May 15, 2018 Ambient Techno Skee Mask - Compro Skee Mask Compro 3yXIkSJWpudtgF0TZuB16U 2010-2019 +169 May 12, 2017 New Wave Paramore - After Laughter Paramore After Laughter 1c9Sx7XdXuMptGyfCB6hHs 2010-2019 +168 February 22, 2011 Ambient Pop Julianna Barwick - The Magic Place Julianna Barwick The Magic Place 3URSIUAf32gpsqPhp1ItuT 2010-2019 +167 June 17, 2014 Lo-Fi (Sandy) Alex G - DSU (Sandy) Alex G DSU 0zYUOExhLNlitUAdgIjHtT 2010-2019 +166 December 15, 2014 Hip Hop Nicki Minaj - The Pinkprint Nicki Minaj The Pinkprint 39WwaUn8HTVZpJJmAuIIHN 2010-2019 +165 March 10, 2017 Indie Rock Jay Som - Everybody Works Jay Som Everybody Works 1ZMtgC7o6TGbzwkv5SpThU 2010-2019 +164 May 5, 2015 Indie Rock Hop Along - Painted Shut Hop Along Painted Shut 16fimHkQtXptYBytXfHfUs 2010-2019 +163 May 7, 2012 Hip Hop Meek Mill - Dreamchasers 2 Meek Mill Dreamchasers 2 2010-2019 +162 May 13, 2014 Country Sturgill Simpson - Metamodern Sounds in Country Music Sturgill Simpson Metamodern Sounds in Country Music 19lODR6Yv1mfvvcTkrFAn2 2010-2019 +161 February 26, 2016 Synthpop The 1975 - I Like It When You Sleep, for You Are So Beautiful Yet So Unaware of It The 1975 I Like It When You Sleep, for You Are So Beautiful Yet So Unaware of It 1JFmNyVPdBF1ECvv4fhpW4 2010-2019 +160 September 14, 2018 Hip Hop Noname - Room 25 Noname Room 25 7oHM3Sj0l2nXAzGAxW0KOt 2010-2019 +159 August 19, 2014 Doom Metal Pallbearer - Foundations of Burden Pallbearer Foundations of Burden 5hcHyM3KYY97gUZzfRlNZb 2010-2019 +158 May 11, 2010 Noise Pop Sleigh Bells - Treats Sleigh Bells Treats 7l5Y3f2amyv1acMfZZQfa2 2010-2019 +157 November 30, 2018 Art Pop The 1975 - A Brief Inquiry Into Online Relationships The 1975 A Brief Inquiry Into Online Relationships 6PWXKiakqhI17mTYM4y6oY 2010-2019 +156 July 29, 2014 Pop Rock Jenny Lewis - The Voyager Jenny Lewis The Voyager 5sCsfubNchaI9RCpP7K7aB 2010-2019 +155 July 14, 2016 Hip Hop 21 Savage & Metro Boomin - Savage Mode 21 Savage & Metro Boomin Savage Mode 4I3EcXD4e3KcEoDJfFEZ5b 2010-2019 +154 March 17, 2014 Drone Mica Levi - Under the Skin Mica Levi Under the Skin 2010-2019 +153 July 12, 2019 Indie Rock Purple Mountains - Purple Mountains Purple Mountains Purple Mountains 5NCdiiTgky5PbjmCtcgwtn 2010-2019 +152 September 9, 2013 Indie Rock Arctic Monkeys - AM Arctic Monkeys AM 5bU1XKYxHhEwukllT20xtk 2010-2019 +151 November 23, 2009 Pop Lady Gaga - The Fame Monster Lady Gaga The Fame Monster 67j3NJodNRI8USUwKwTZA6 2010-2019 +150 April 14, 2017 Hip Hop Playboi Carti - Playboi Carti Playboi Carti Playboi Carti 4rJgzzfFHAVFhCSt2P4I3j 2010-2019 +149 October 16, 2012 Singer-Songwriter Mac DeMarco - 2 Mac DeMarco 2 7mQ1elwuFdy0LBjAxv4spP 2010-2019 +148 January 31, 2011 Microhouse Nicolas Jaar - Space Is Only Noise Nicolas Jaar Space Is Only Noise 0tUJcqDuXHNkaPKLN0lQhT 2010-2019 +147 May 10, 2010 Indie Rock The National - High Violet The National High Violet 59gXPxZ8CwFaeknPxtxXHZ 2010-2019 +146 January 1, 2017 Power Pop Sheer Mag - Compilation LP Sheer Mag Compilation LP 0QKFT4xWEigQjzJaAc1PPP 2010-2019 +145 December 18, 2012 Hip Hop Chief Keef - Finally Rich Chief Keef Finally Rich 108j4R9oEcFUaVRzE8RZKI 2010-2019 +144 February 15, 2011 Alternative Rock PJ Harvey - Let England Shake PJ Harvey Let England Shake 7f1aXd7Gd5H9IqFu36zw6m 2010-2019 +143 April 5, 2019 Art Pop Weyes Blood - Titanic Rising Weyes Blood Titanic Rising 53VKICyqCf91sVkTdFrzKX 2010-2019 +142 March 29, 2019 Ambient Fennesz - Agora Fennesz Agora 7JpOsq1F2A9aPr2fdacsOk 2010-2019 +141 January 6, 2012 Hip Hop Rick Ross - Rich Forever Rick Ross Rich Forever 2Yeiz6V1YuHFz7iiWCC0SE 2010-2019 +140 June 3, 2014 Garage Rock Parquet Courts - Sunbathing Animal Parquet Courts Sunbathing Animal 3ngz9FbWGxHscqvwoGOL0u 2010-2019 +139 June 8, 2010 Drone Emeralds - Does It Look Like I'm Here? Emeralds Does It Look Like I'm Here? 0c0XkZh9vWXg473nchoArI 2010-2019 +138 April 21, 2015 Southern Rock Alabama Shakes - Sound & Color Alabama Shakes Sound & Color 03nQNGFi3dIxg6ghNbtVWW 2010-2019 +137 October 31, 2011 Hip Hop A$AP Rocky - Live. Love. A$AP A$AP Rocky Live. Love. A$AP 2010-2019 +136 February 15, 2011 Drone Tim Hecker - Ravedeath, 1972 Tim Hecker Ravedeath, 1972 6cbCvUzsawFcG0aZVCJyzt 2010-2019 +135 May 3, 2011 Indie Folk Fleet Foxes - Helplessness Blues Fleet Foxes Helplessness Blues 3l7iMXJ0jqFnIYZRyCUewC 2010-2019 +134 October 18, 2011 Dream Pop M83 - Hurry Up, We're Dreaming M83 Hurry Up, We're Dreaming 6MBuQugGuX7VMBX0uiBnAQ 2010-2019 +133 March 4, 2014 Twee Pop Frankie Cosmos - Zentropy Frankie Cosmos Zentropy 6EAwEaY4Ozymphrn5kiCwd 2010-2019 +132 March 30, 2018 Neo-Psychedelia Amen Dunes - Freedom Amen Dunes Freedom 2H4G8AGta9p8yjgjVI9nZd 2010-2019 +131 October 28, 2014 Hip Hop Run the Jewels - Run the Jewels 2 Run the Jewels Run the Jewels 2 2lPYlP4eumsjz6LBG8GCbG 2010-2019 +130 October 9, 2012 Psychedelic Rock Tame Impala - Lonerism Tame Impala Lonerism 76vY9yohh4kVwSKkyKbyEQ 2010-2019 +129 October 15, 2013 Footwork Various Artists - Bangs & Works Vol. 1 Various Artists Bangs & Works Vol. 1 2010-2019 +128 May 8, 2016 Art Rock Radiohead - A Moon Shaped Pool Radiohead A Moon Shaped Pool 6vuykQgDLUCiZ7YggIpLM9 2010-2019 +127 May 20, 2016 Indie Rock Car Seat Headrest - Teens of Denial Car Seat Headrest Teens of Denial 0czBomMxaMCzanUaFhESOW 2010-2019 +126 April 12, 2011 Psychedelic Pop Panda Bear - Tomboy Panda Bear Tomboy 3SH1o5bO60CTibwxdYOFyo 2010-2019 +125 September 5, 2018 Post-Industrial Yves Tumor - Safe in the Hands of Love Yves Tumor Safe in the Hands of Love 1IpYZkYoYCjXTYMDEW8Ksk 2010-2019 +124 October 18, 2011 Indie Rock Real Estate - Days Real Estate Days 43uj7422MLR9MRBXSki0El 2010-2019 +123 June 11, 2013 Blackgaze Deafheaven - Sunbather Deafheaven Sunbather 2kKXGWaCEl06EKZ4DxBJIT 2010-2019 +122 June 8, 2018 Indie Rock Snail Mail - Lush Snail Mail Lush 2e48GqjEwCi87gQJanb1bf 2010-2019 +121 February 25, 2014 Emo The Hotelier - Home, Like Noplace is There The Hotelier Home, Like Noplace is There 0JWZYF32E2xCB7lgqEbqIp 2010-2019 +120 July 21, 2017 Experimental Hip Hop Tyler, the Creator - Flower Boy Tyler, the Creator Flower Boy 2nkto6YNI4rUYTLqEwWJ3o 2010-2019 +119 June 2, 2015 Indie Pop Girlpool - Before The World Was Big Girlpool Before The World Was Big 0iyTXByS9tabgYER06HSRA 2010-2019 +118 October 21, 2016 Singer-Songwriter Leonard Cohen - You Want It Darker Leonard Cohen You Want It Darker 3jeTB3j3QmUs8SPIVleHtU 2010-2019 +117 April 21, 2012 Hip Hop Death Grips - The Money Store Death Grips The Money Store 1PQDjdBpHPikAodJqjzm6a 2010-2019 +116 May 18, 2010 R&B Janelle Monáe - The ArchAndroid Janelle Monáe The ArchAndroid 7MvSB0JTdtl1pSwZcgvYQX 2010-2019 +115 February 16, 2018 Neo-Psychedelia U.S. Girls - In a Poem Unlimited U.S. Girls In a Poem Unlimited 5mcuyVRQmrRlfFqDDfJI1q 2010-2019 +114 February 3, 2017 Alternative R&B Sampha - Process Sampha Process 2gUSWVHCOerKhJHZRwhVtN 2010-2019 +113 June 5, 2012 Garage Rock Japandroids - Celebration Rock Japandroids Celebration Rock 2sY9WYVH022ulyAYaqvXLW 2010-2019 +112 May 21, 2013 Nu-Disco Daft Punk - Random Access Memories Daft Punk Random Access Memories 4m2880jivSbbyEGAKfITCa 2010-2019 +111 May 10, 2011 Experimental EMA - Past Life Martyred Saints EMA Past Life Martyred Saints 08OhqUM0MeAuHd9HdryHB4 2010-2019 +110 October 5, 2010 Hip Hop Waka Flocka Flame - Flockaveli Waka Flocka Flame Flockaveli 6MQtWELG7aRX7CkAzQ6nLM 2010-2019 +109 October 26, 2018 Progressive Pop Julia Holter - Aviary Julia Holter Aviary 6icpwcJQWK4nq9Xilk4yRu 2010-2019 +108 May 6, 2016 Electronic KAYTRANADA - 99.9% KAYTRANADA 99.9% 1dZZh7PvVgce1DDsDPzy8Z 2010-2019 +107 July 10, 2012 Art Pop Dirty Projectors - Swing Lo Magellan Dirty Projectors Swing Lo Magellan 1WhF5ZiI5V5laq2nGLoF99 2010-2019 +106 September 22, 2017 Art Pop Moses Sumney - Aromanticism Moses Sumney Aromanticism 30WjNaR79shSTGB52IJTw0 2010-2019 +105 June 3, 2013 Electronic Disclosure - Settle Disclosure Settle 1bEnTsCvG4FPW7NpIEC5Zc 2010-2019 +104 March 5, 2013 Singer-Songwriter Waxahatchee - Cerulean Salt Waxahatchee Cerulean Salt 4OfohS2lFQOeCXsrNcdh5s 2010-2019 +103 March 8, 2019 Art Pop Helado Negro - This Is How You Smile Helado Negro This Is How You Smile 17LsQV3q3cgTBrat3D5JSv 2010-2019 +102 December 4, 2015 R&B Jeremih - Late Nights: The Album Jeremih Late Nights: The Album 4p9QlliNvKndyKSPyuJlP8 2010-2019 +101 March 26, 2012 Synthpop Chromatics - Kill for Love Chromatics Kill for Love 5Rz4NIqwoYGDbIh40UTylV 2010-2019 +100 August 17, 2018 R&B Ariana Grande - Sweetener Ariana Grande Sweetener 3tx8gQqWbGwqIGZHqDNrGe 2010-2019 +99 December 24, 2018 Trap Rap Bad Bunny - X 100PRE Bad Bunny X 100PRE 7CjJb2mikwAWA1V6kewFBF 2010-2019 +98 April 8, 2014 Nu-Disco Todd Terje - It's Album Time Todd Terje It's Album Time 4pefQ21iSk8hdnxw3WSB5Y 2010-2019 +97 March 18, 2014 Heartland Rock The War on Drugs - Lost in the Dream The War on Drugs Lost in the Dream 6nD9jfVVX0OadsHJqmjObO 2010-2019 +96 September 14, 2018 Ambient Pop Low - Double Negative Low Double Negative 1zTkgOHx3mjrUvrhxq4osf 2010-2019 +95 June 1, 2019 Alternative R&B Jai Paul - Leak 04-13 (Bait Ones) Jai Paul Leak 04-13 (Bait Ones) 6Wsai43KQmmKlN29AWlXFr 2010-2019 +94 January 12, 2010 Indie Rock Vampire Weekend - Contra Vampire Weekend Contra 0zeAijecFGZOS4OaRdPVz5 2010-2019 +93 August 10, 2018 Art Pop Tirzah - Devotion Tirzah Devotion 15GocbF7ybkkPP03YXtLqv 2010-2019 +92 August 8, 2011 Hip Hop Kanye West & Jay-Z - Watch the Throne Kanye West & Jay-Z Watch the Throne 1YwzJz7CrV9fd9Qeb6oo1d 2010-2019 +91 September 30, 2016 Art Pop Bon Iver - 22, A Million Bon Iver 22, A Million 1PgfRdl3lPyACfUGH4pquG 2010-2019 +90 July 22, 2014 Pop Punk Joyce Manor - Never Hungover Again Joyce Manor Never Hungover Again 4B61MxQBe8RsHIlPSyjZZv 2010-2019 +89 October 7, 2014 Electronic Caribou - Our Love Caribou Our Love 233jGebCTUPAYF5MxvLB7A 2010-2019 +88 June 9, 2015 Art Pop Jenny Hval - Apocalypse, girl Jenny Hval Apocalypse, girl 3AeAZfwBgnhmbNEowNFvcB 2010-2019 +87 January 6, 2015 Hip Hop Rae Sremmurd - SremmLife Rae Sremmurd SremmLife 4ZmCBqmAmzZaw6AKnlXqQI 2010-2019 +86 September 30, 2013 Indie Rock HAIM - Days Are Gone HAIM Days Are Gone 0eXAgjcMHbFt3fS0XXp8Kw 2010-2019 +85 January 27, 2015 Singer-Songwriter Jessica Pratt - On Your Own Love Again Jessica Pratt On Your Own Love Again 7jzMs30mrCWAZKrqD5ckFd 2010-2019 +84 April 30, 2013 Hip Hop Chance The Rapper - Acid Rap Chance The Rapper Acid Rap 2VBcztE58pBKjIDS5oEgFh 2010-2019 +83 December 16, 2013 Dubstep Burial - Rival Dealer Burial Rival Dealer 7Mpnu24WcUfiCw9sGrdp2K 2010-2019 +82 September 13, 2011 Indie Pop Girls - Father, Son, Holy Ghost Girls Father, Son, Holy Ghost 66wRO7SK0Wo1KS40en2tua 2010-2019 +81 March 1, 2019 Neo-Soul Solange - When I Get Home Solange When I Get Home 4WF4HvVT7VjGnVjxjoCR6w 2010-2019 +80 September 23, 2014 IDM Aphex Twin - Syro Aphex Twin Syro 6oRuinkJdTge4hpTuClEF8 2010-2019 +79 July 17, 2015 Psychedelic Pop Tame Impala - Currents Tame Impala Currents 0rxKf57PZvWEoU8v3m5W2q 2010-2019 +78 March 8, 2011 Folk Rock Kurt Vile - Smoke Ring for My Halo Kurt Vile Smoke Ring for My Halo 32a7BrNNTAu7BVb6DcsMLP 2010-2019 +77 January 26, 2010 Electronic Four Tet - There Is Love in You Four Tet There Is Love in You 6x2gG7Pw1g54ZjQWjiAVCK 2010-2019 +76 August 3, 2010 Indie Rock Arcade Fire - The Suburbs Arcade Fire The Suburbs 4zJBuQXo92Q7QhA5U4V8kw 2010-2019 +75 March 21, 2011 R&B The Weeknd - House of Balloons The Weeknd House of Balloons 2010-2019 +74 June 15, 2018 Post-Industrial SOPHIE - OIL OF EVERY PEARL'S UN-INSIDES SOPHIE OIL OF EVERY PEARL'S UN-INSIDES 0CKiI5WdJ5rzUIK8hyD3SY 2010-2019 +73 April 6, 2018 Trap Rap Cardi B - Invasion of Privacy Cardi B Invasion of Privacy 4KdtEKjY3Gi0mKiSdy96ML 2010-2019 +72 February 10, 2015 Singer-Songwriter Father John Misty - I Love You, Honeybear Father John Misty I Love You, Honeybear 7buEcyw6fJF3WPgr06BomH 2010-2019 +71 October 26, 2018 Electropop Robyn - Honey Robyn Honey 6WZjFvrzwq8SOGe0r8R3qk 2010-2019 +70 May 5, 2017 Art Pop Perfume Genius - No Shape Perfume Genius No Shape 7awgq3vvlsIeA7dZduR9x4 2010-2019 +69 February 12, 2015 Hip Hop Drake - If You're Reading This It's Too Late Drake If You're Reading This It's Too Late 6K77I7FVTV0pxUfQikCbxj 2010-2019 +68 May 4, 2010 IDM Flying Lotus - Cosmogramma Flying Lotus Cosmogramma 5c7XChrHxYaqykCZLaGM5f 2010-2019 +67 March 24, 2015 Indie Rock Courtney Barnett - Sometimes I Sit and Think, and Sometimes I Just Sit Courtney Barnett Sometimes I Sit and Think, and Sometimes I Just Sit 5FpTrIArvT20xUSpGRXGLY 2010-2019 +66 October 27, 2017 Electropop Fever Ray - Plunge Fever Ray Plunge 3UHMhYzYnfTBEuDxb1JmxC 2010-2019 +65 July 17, 2015 Hip Hop Future - DS2 Future DS2 0fUy6IdLHDpGNwavIlhEsl 2010-2019 +64 August 17, 2018 Indie Rock Mitski - Be the Cowboy Mitski Be the Cowboy 653wRjqO0GOZPQPcXpeAXD 2010-2019 +63 August 15, 2011 Hip Hop Danny Brown - XXX Danny Brown XXX 3j21GO4l4W0w7hdF8JVPmo 2010-2019 +62 October 31, 2014 Ambient Grouper - Ruins Grouper Ruins 5ElYoVUqRQIlDekD1v6aKa 2010-2019 +61 April 7, 2017 Art Pop Arca - Arca Arca Arca 1MQO4j8QExVgmnplbIodEU 2010-2019 +60 October 13, 2017 Art Rock King Krule - The OOZ King Krule The OOZ 6Ulu31dfRTpIAud08ZIhXd 2010-2019 +59 October 22, 2012 Pop Taylor Swift - Red Taylor Swift Red 1EoDsNmgTLtmwe1BDAVxV5 2010-2019 +58 May 5, 2015 Jazz Fusion Kamasi Washington - The Epic Kamasi Washington The Epic 1jTw1oalZ4vF8jmpmILCdh 2010-2019 +57 April 14, 2017 Hip Hop Kendrick Lamar - DAMN. Kendrick Lamar DAMN. 4eLPsYPBmXABThSJ821sqY 2010-2019 +56 January 20, 2015 Art Pop Björk - Vulnicura Björk Vulnicura 1ttnHZ0HVGMSMTJdZZ7kYK 2010-2019 +55 September 13, 2011 Art Pop St. Vincent - Strange Mercy St. Vincent Strange Mercy 1Lci4bx7JIuCC8pnBNX7ds 2010-2019 +54 April 16, 2015 Hip Hop Young Thug - Barter 6 Young Thug Barter 6 0BsMZIueWsJLWng8A7sE8e 2010-2019 +53 May 19, 2017 Footwork Jlin - Black Origami Jlin Black Origami 7526bnJCkFFnAMSQ9fsva9 2010-2019 +52 May 18, 2010 Dance Punk LCD Soundsystem - This Is Happening LCD Soundsystem This Is Happening 4hnqM0JK4CM1phwfq1Ldyz 2010-2019 +51 May 4, 2018 Deep House DJ Koze - Knock Knock DJ Koze Knock Knock 0sT4nyNxsvGNQr1O8OR83O 2010-2019 +50 February 21, 2012 Synthpop Grimes - Visions Grimes Visions 48a7rOjTzpD1zzJAteeveE 2010-2019 +49 November 8, 2011 Drone Oneohtrix Point Never - Replica Oneohtrix Point Never Replica 6zUTN3bN8rsgdy1lsFXI7L 2010-2019 +48 May 6, 2016 Art Pop ANOHNI - Hopelessness ANOHNI Hopelessness 3dAx4u7AJy72a6M1ms6uYF 2010-2019 +47 August 21, 2015 Dance Pop Carly Rae Jepsen - E•MO•TION Carly Rae Jepsen E•MO•TION 49iTBKGf47W1CJMQ8W2jHB 2010-2019 +46 April 8, 2013 Electronic The Knife - Shaking the Habitual The Knife Shaking the Habitual 7eDaaP6J8Q3cYVTGRmARwx 2010-2019 +45 March 24, 2017 Singer-Songwriter Mount Eerie - A Crow Looked at Me Mount Eerie A Crow Looked at Me 5p64XgvFREt1P6mC7Xl6XN 2010-2019 +44 November 11, 2016 Hip Hop A Tribe Called Quest - We got it from Here... Thank You 4 Your service A Tribe Called Quest We got it from Here... Thank You 4 Your service 3WvQpufOsPzkZvcSuynCf3 2010-2019 +43 October 29, 2013 Synthpop Sky Ferreira - Night Time, My Time Sky Ferreira Night Time, My Time 1bvCVYPVl445mO690M2dOr 2010-2019 +42 March 30, 2010 Neo-Soul Erykah Badu - New Amerykah Part Two: Return of the Ankh Erykah Badu New Amerykah Part Two: Return of the Ankh 1MOub955Uer957RVqqkF2a 2010-2019 +41 April 23, 2016 R&B Beyoncé - Lemonade Beyoncé Lemonade 4X6b6POxbjX9inC7TWQd54 2010-2019 +40 December 15, 2017 Electropop Charli XCX - Pop 2 Charli XCX Pop 2 2HIwUmdxEl7SeWa1ndH5wC 2010-2019 +39 April 5, 2011 Singer-Songwriter Bill Callahan - Apocalypse Bill Callahan Apocalypse 4RBHgVw8kR6IpXVnZhZrh0 2010-2019 +38 May 30, 2018 Pop Rap Tierra Whack - Whack World Tierra Whack Whack World 3ogNAkUhvQy0cFOfLoR6Y8 2010-2019 +37 January 8, 2016 Art Rock David Bowie - Blackstar David Bowie Blackstar 2w1YJXWMIco6EBf0CovvVN 2010-2019 +36 November 2, 2018 Art Pop Rosalía - El Mal Querer Rosalía El Mal Querer 355bjCHzRJztCzaG5Za4gq 2010-2019 +35 November 18, 2013 R&B Blood Orange - Cupid Deluxe Blood Orange Cupid Deluxe 5Pn6Nexd7zbCMkdVGeml0m 2010-2019 +34 June 30, 2015 Hip Hop Vince Staples - Summertime '06 Vince Staples Summertime '06 59olnuVrXXgrDH4wknpDLC 2010-2019 +33 May 3, 2019 Indie Folk Big Thief - U.F.O.F. Big Thief U.F.O.F. 13arYJWgDb5xGDHU49Nlj9 2010-2019 +32 June 21, 2011 Indie Folk Bon Iver - Bon Iver, Bon Iver Bon Iver Bon Iver, Bon Iver 0ZMzEAuUIylHgetdWqzcHU 2010-2019 +31 June 28, 2011 Pop Beyoncé - 4 Beyoncé 4 7cvVgT5RXbcUo9Qw4nq31D 2010-2019 +30 June 17, 2016 Indie Rock Mitski - Puberty 2 Mitski Puberty 2 16i5KnBjWgUtwOO7sVMnJB 2010-2019 +29 September 28, 2010 Indie Rock Deerhunter - Halcyon Digest Deerhunter Halcyon Digest 1HUMjB15ARg96KIypcGzYY 2010-2019 +28 August 12, 2014 Alternative R&B FKA twigs - LP1 FKA twigs LP1 7mpQVIMZ2iNnAfqodqvdhz 2010-2019 +27 November 30, 2018 Experimental Hip Hop Earl Sweatshirt - Some Rap Songs Earl Sweatshirt Some Rap Songs 66at85wgO2pu5CccvqUF6i 2010-2019 +26 February 18, 2014 Singer-Songwriter Angel Olsen - Burn Your Fire for No Witness Angel Olsen Burn Your Fire for No Witness 0xvDtkNKJiLclVbjLvovFU 2010-2019 +25 June 1, 2015 UK Bass Jamie xx - In Colour Jamie xx In Colour 5jBKTppNIUpcrNKbr8jbsQ 2010-2019 +24 June 9, 2017 Alternative R&B SZA - Ctrl SZA Ctrl 76290XdXVF9rPzGdNRWdCh 2010-2019 +23 March 30, 2018 Country Pop Kacey Musgraves - Golden Hour Kacey Musgraves Golden Hour 7f6xPqyaolTiziKf5R5Z0c 2010-2019 +22 January 25, 2011 Sophisti-Pop Destroyer - Kaputt Destroyer Kaputt 1clYDgHxfhzxWQJH0ieRpx 2010-2019 +21 January 26, 2010 Dream Pop Beach House - Teen Dream Beach House Teen Dream 72mGz9Dnt42euozq8yBULe 2010-2019 +20 October 22, 2013 Footwork DJ Rashad - Double Cup DJ Rashad Double Cup 4J7qkorMbPmJQy79SntDA8 2010-2019 +19 August 30, 2019 Art Pop Lana Del Rey - Norman Fucking Rockwell! Lana Del Rey Norman Fucking Rockwell! 5XpEKORZ4y6OrCZSKsi46A 2010-2019 +18 October 22, 2012 Hip Hop Kendrick Lamar - good kid, m.A.A.d. city Kendrick Lamar good kid, m.A.A.d. city 1DqhWr73Fh5yoNzKLas0G3 2010-2019 +17 March 31, 2015 Indie Folk Sufjan Stevens - Carrie & Lowell Sufjan Stevens Carrie & Lowell 0U8DeqqKDgIhIiWOdqiQXE 2010-2019 +16 February 23, 2010 Indie Folk Joanna Newsom - Have One on Me Joanna Newsom Have One on Me 2010-2019 +15 June 18, 2013 Hip Hop Kanye West - Yeezus Kanye West Yeezus 0XTAmejG8F97wF5MWoVbaY 2010-2019 +14 June 16, 2017 Art Pop Lorde - Melodrama Lorde Melodrama 2B87zXm9bOWvAJdkJBTpzF 2010-2019 +13 November 15, 2011 Hip Hop Drake - Take Care Drake Take Care 63WdJvk8G9hxJn8u5rswNh 2010-2019 +12 January 27, 2016 R&B Rihanna - ANTI Rihanna ANTI 3Q149ZH46Z0f3oDR7vlDYV 2010-2019 +11 November 6, 2015 Art Pop Grimes - Art Angels Grimes Art Angels 5hB4jVN4ZHpubyiMmW81K1 2010-2019 +10 July 17, 2012 Alternative R&B Frank Ocean - Channel Orange Frank Ocean Channel Orange 623Ef2ZEB3Njklix4PC0Rs 2010-2019 +9 December 15, 2014 Neo-Soul D'Angelo - Black Messiah D'Angelo Black Messiah 5Hfbag0SsHxafx1SySFSX6 2010-2019 +8 November 22, 2010 Electropop Robyn - Body Talk Robyn Body Talk 7J4oxoeFQLTrHnjNu2ZaJ5 2010-2019 +7 May 14, 2013 Indie Rock Vampire Weekend - Modern Vampires of the City Vampire Weekend Modern Vampires of the City 2Qi2SySN2ePZwMLDSv9Krn 2010-2019 +6 September 30, 2016 Alternative R&B Solange - A Seat at the Table Solange A Seat at the Table 3Yko2SxDk4hc6fncIBQlcM 2010-2019 +5 June 19, 2012 Singer-Songwriter Fiona Apple - The Idler Wheel Fiona Apple The Idler Wheel 7uccppFArLf9dNOwDftOZa 2010-2019 +4 March 16, 2015 Hip Hop Kendrick Lamar - To Pimp a Butterfly Kendrick Lamar To Pimp a Butterfly 7ycBtnsMtyVbbwTfJwRjSP 2010-2019 +3 December 13, 2013 R&B Beyoncé - Beyoncé Beyoncé Beyoncé 2noKUZhXwUhPQMgSr56T4G 2010-2019 +2 November 22, 2010 Hip Hop Kanye West - My Beautiful Dark Twisted Fantasy Kanye West My Beautiful Dark Twisted Fantasy 6LBiuhK7PZKjVXyMfPxPoh 2010-2019 +1 August 20, 2016 Alternative R&B Frank Ocean - Blonde Frank Ocean Blonde 1PDX0hMmsSdq122EupvNZF 2010-2019 +50 August 3, 2010 Indie Rock Wavves - King of the Beach Wavves King of the Beach 0EYSLCjAJiGtGfPuENfQYc 2010 +49 May 25, 2010 Dream Pop Wild Nothing - Gemini Wild Nothing Gemini 3oPuSNDpKa84WJLWstHomV 2010 +48 March 1, 2010 Neo-Psychedelia Forest Swords - Dagger Paths Forest Swords Dagger Paths 5lAR8NUtW605yBZae8MS0J 2010 +47 September 28, 2010 Post-Punk Women - Public Strain Women Public Strain 4bOm9cju3kHhqoE5ZjGurt 2010 +46 August 17, 2010 Electronic Matthew Dear - Black City Matthew Dear Black City 6GhqAQ0NYXwrG4JIidzBso 2010 +45 February 9, 2010 Soul Gil Scott-Heron - I'm New Here Gil Scott-Heron I'm New Here 60JXrFsIxXP6rqd4jdTfrn 2010 +44 October 26, 2010 Sludge Metal Kylesa - Spiral Shadow Kylesa Spiral Shadow 5CP0zFLnRi8SAxb1NxuB6b 2010 +43 May 25, 2010 Psychedelic Rock Tame Impala - Innerspeaker Tame Impala Innerspeaker 51jAc3sHdN2YG1X5AZg6Ow 2010 +42 June 15, 2010 Hip Hop Drake - Thank Me Later Drake Thank Me Later 6jlrjFR9mJV3jd1IPSplXU 2010 +41 June 8, 2010 Indietronica Delorean - Subiza Delorean Subiza 1Xl3ibPzUoDmMrHcr8J32T 2010 +40 September 28, 2010 Indie Rock Abe Vigoda - Crush Abe Vigoda Crush 4416b7x6TolPco1AjI9gR7 2010 +39 July 27, 2010 Indie Pop Best Coast - Crazy for You Best Coast Crazy for You 0xwxWdZEI2JmZOqazm0HCU 2010 +38 July 20, 2010 Hip Hop Rick Ross - Teflon Don Rick Ross Teflon Don 0jipZxGtkTDHjVerLkzO80 2010 +37 August 23, 2010 Psychedelic Zola Jesus - Stridulum II Zola Jesus Stridulum II 79Guttnxvzx1fBh30vNWhY 2010 +36 June 8, 2010 Drone Emeralds - Does It Look Like I'm Here? Emeralds Does It Look Like I'm Here? 0c0XkZh9vWXg473nchoArI 2010 +35 March 9, 2010 Electropop Gorillaz - Plastic Beach Gorillaz Plastic Beach 2dIGnmEIy1WZIcZCFSj6i8 2010 +34 May 25, 2010 Electronic Crystal Castles - Crystal Castles Crystal Castles Crystal Castles 75olNPlDKi2XQhTD9IPlVC 2010 +33 April 13, 2010 Folk The Tallest Man On Earth - The Wild Hunt The Tallest Man On Earth The Wild Hunt 6cnZbo9GSSTTafDnoXtSGL 2010 +32 December 25, 2009 Hip Hop Tyler, the Creator - Bastard Tyler, the Creator Bastard 2010 +31 May 4, 2010 Lo-Fi Woods - At Echo Lake Woods At Echo Lake 45YGCEh09Ji6rdHjPgNdw8 2010 +30 June 29, 2010 R&B The-Dream - Love King The-Dream Love King 08912w1sEZbVmc4XZr40JE 2010 +29 October 12, 2010 Garage Rock The Fresh & Onlys - Play It Strange The Fresh & Onlys Play It Strange 6Z0GV47fGK4hcnzklj80OX 2010 +28 May 10, 2010 Indie Rock The National - High Violet The National High Violet 59gXPxZ8CwFaeknPxtxXHZ 2010 +27 January 26, 2010 Electronic Four Tet - There Is Love in You Four Tet There Is Love in You 6x2gG7Pw1g54ZjQWjiAVCK 2010 +26 September 28, 2010 New Wave Twin Shadow - Forget Twin Shadow Forget 74fmoh0ZhWbwhW4GZRW4D8 2010 +25 October 12, 2010 Chamber Pop Sufjan Stevens - The Age of Adz Sufjan Stevens The Age of Adz 4ln0tQzp4XX6o2Z8XIlVDk 2010 +24 February 9, 2010 Synthpop Hot Chip - One Life Stand Hot Chip One Life Stand 71hq2cOVNhHTFOvy2erv01 2010 +23 September 14, 2010 Hip Hop Das Racist - Sit Down, Man Das Racist Sit Down, Man 2010 +22 November 22, 2010 Indie Pop Girls - Broken Dreams Club Girls Broken Dreams Club 7BckGE6X3hGi0L1TDO43PN 2010 +21 September 14, 2010 Indie Rock The Walkmen - Lisbon The Walkmen Lisbon 1OvxCp2JiapV5L2KXV7hzW 2010 +20 June 21, 2010 Drone Oneohtrix Point Never - Returnal Oneohtrix Point Never Returnal 7fz1x7StTrizRtDT8DXLA9 2010 +19 October 19, 2010 Dream Pop How to Dress Well - Love Remains How to Dress Well Love Remains 3MxMzvb0VJZXCSgfsX0UcF 2010 +18 March 30, 2010 Neo-Soul Erykah Badu - New Amerykah Part Two: Return of the Ankh Erykah Badu New Amerykah Part Two: Return of the Ankh 1MOub955Uer957RVqqkF2a 2010 +17 April 20, 2010 Electronic Caribou - Swim Caribou Swim 74vgQmQX8dVxpw9zcPEFxE 2010 +16 May 11, 2010 Noise Pop Sleigh Bells - Treats Sleigh Bells Treats 7l5Y3f2amyv1acMfZZQfa2 2010 +15 November 22, 2010 Electropop Robyn - Body Talk Robyn Body Talk 7J4oxoeFQLTrHnjNu2ZaJ5 2010 +14 May 4, 2010 IDM Flying Lotus - Cosmogramma Flying Lotus Cosmogramma 5c7XChrHxYaqykCZLaGM5f 2010 +13 September 28, 2010 Indie Rock No Age - Everything in Between No Age Everything in Between 3silW4wJsKp3p8tJEU9SJa 2010 +12 May 18, 2010 R&B Janelle Monáe - The ArchAndroid Janelle Monáe The ArchAndroid 7MvSB0JTdtl1pSwZcgvYQX 2010 +11 August 3, 2010 Indie Rock Arcade Fire - The Suburbs Arcade Fire The Suburbs 4zJBuQXo92Q7QhA5U4V8kw 2010 +10 March 9, 2010 Indie Rock Titus Andronicus - The Monitor Titus Andronicus The Monitor 5U09FQWagzAU5HPmufUAlU 2010 +9 June 8, 2010 Psychedelic Pop Ariel Pink's Haunted Graffiti - Before Today Ariel Pink's Haunted Graffiti Before Today 1dO7qBlkQXYENJaHfK7h56 2010 +8 October 10, 2010 Dubstep James Blake - Klavierwerke EP James Blake Klavierwerke EP 5y34dFTJVbEzIZcVF0F6N0 2010 +8 May 31, 2010 Dubstep James Blake - CMYK EP James Blake CMYK EP 77ouWfDXYOYnES19CFiPKS 2010 +8 March 15, 2010 Dubstep James Blake - The Bells Sketch EP James Blake The Bells Sketch EP 2010 +7 February 23, 2010 Indie Folk Joanna Newsom - Have One on Me Joanna Newsom Have One on Me 2010 +6 January 12, 2010 Indie Rock Vampire Weekend - Contra Vampire Weekend Contra 0zeAijecFGZOS4OaRdPVz5 2010 +5 January 26, 2010 Dream Pop Beach House - Teen Dream Beach House Teen Dream 72mGz9Dnt42euozq8yBULe 2010 +4 July 6, 2010 Hip Hop Big Boi - Sir Lucious Left Foot: The Son of Chico Dusty Big Boi Sir Lucious Left Foot: The Son of Chico Dusty 0GZMsKdDsbNhxvM1VaxB0U 2010 +3 September 28, 2010 Indie Rock Deerhunter - Halcyon Digest Deerhunter Halcyon Digest 1HUMjB15ARg96KIypcGzYY 2010 +2 May 18, 2010 Dance Punk LCD Soundsystem - This Is Happening LCD Soundsystem This Is Happening 4hnqM0JK4CM1phwfq1Ldyz 2010 +1 November 22, 2010 Hip Hop Kanye West - My Beautiful Dark Twisted Fantasy Kanye West My Beautiful Dark Twisted Fantasy 6LBiuhK7PZKjVXyMfPxPoh 2010 +50 September 27, 2011 Dream Pop Youth Lagoon - The Year of Hibernation Youth Lagoon The Year of Hibernation 0mcvDycoex7ANLZOmVVRoD 2011 +49 September 13, 2011 Indie Rock Wild Flag - Wild Flag Wild Flag Wild Flag 2HcZpInX7XfjQ69EsGKrhS 2011 +48 February 22, 2011 Chillwave Toro y Moi - Underneath The Pine Toro y Moi Underneath The Pine 00wmVs9BwWaSjLXBAzwUPq 2011 +47 November 21, 2011 Dubstep Sepalcure - Sepalcure Sepalcure Sepalcure 4NAZkbgSggue9GbUVoA4rf 2011 +46 June 7, 2011 Indie Pop Cults - Cults Cults Cults 2jb0zRewft3L2AwCOMx3du 2011 +45 July 2, 2011 Hip Hop Kendrick Lamar - Section.80 Kendrick Lamar Section.80 13WjgUEEAQp0d9JqojlWp1 2011 +44 February 22, 2011 Experimental Colin Stetson - New History Warfare Vol. 2: Judges Colin Stetson New History Warfare Vol. 2: Judges 2YxOKIEvWuLnBTWGJL77aI 2011 +43 March 1, 2011 Indie Pop Lykke Li - Wounded Rhymes Lykke Li Wounded Rhymes 43uf0nTu6b5ReBCoQkLtsF 2011 +42 June 28, 2011 Dubstep SBTRKT - SBTRKT SBTRKT SBTRKT 5fP2kgfePJZF4nB1XqC1i8 2011 +41 May 10, 2011 Black Metal Liturgy - Aesthethica Liturgy Aesthethica 1cpS5zdyOqr6hXLGiEr0Bj 2011 +40 June 14, 2011 Hip Hop AraabMuzik - Electronic Dream AraabMuzik Electronic Dream 4ahBcxn9LnYUtf2u0iXVve 2011 +39 August 16, 2011 Indie Rock The War on Drugs - Slave Ambient The War on Drugs Slave Ambient 4136oTfNt4X3nw0zP1w2NG 2011 +38 October 18, 2011 Experimental Sandro Perri - Impossible Spaces Sandro Perri Impossible Spaces 0wTaLOGGUfWxT5TKrRq0Px 2011 +37 January 7, 2011 Post-Punk Iceage - New Brigade Iceage New Brigade 6vlGKE1mG5wxiqNE6fYXOF 2011 +36 November 21, 2011 Art Pop Kate Bush - 50 Words for Snow Kate Bush 50 Words for Snow 1VAB3Xn92dPKPWzocgQqkh 2011 +35 February 18, 2011 R&B Frank Ocean - Nostalgia, Ultra. Frank Ocean Nostalgia, Ultra. 2011 +34 April 5, 2011 Dance Pop Katy B - On a Mission Katy B On a Mission 6KV9kNSuC1mmzrXKx6p6vV 2011 +33 June 7, 2011 Hardcore Punk Fucked Up - David Comes to Life Fucked Up David Comes to Life 7exqkn1MEoUhfDRMjwCOgm 2011 +32 April 12, 2011 Psychedelic Pop Panda Bear - Tomboy Panda Bear Tomboy 3SH1o5bO60CTibwxdYOFyo 2011 +31 June 21, 2011 Garage Rock Ty Segall - Goodbye Bread Ty Segall Goodbye Bread 3fgoWUTlgdezy7dPA6lVaE 2011 +30 February 15, 2011 Drone Tim Hecker - Ravedeath, 1972 Tim Hecker Ravedeath, 1972 6cbCvUzsawFcG0aZVCJyzt 2011 +29 April 19, 2011 Hip Hop DJ Quik - Book of David DJ Quik Book of David 1PlWZCEmezLce2Mlyd5DQm 2011 +28 February 8, 2011 Synthpop Cut Copy - Zonoscope Cut Copy Zonoscope 5SFPr07PUPCT4YSaZjYeRR 2011 +27 June 28, 2011 Pop Beyoncé - 4 Beyoncé 4 7cvVgT5RXbcUo9Qw4nq31D 2011 +26 October 11, 2011 Minimal Techno The Field - Looping State of Mind The Field Looping State of Mind 1GO1sxNMppSD67YqZe4sKq 2011 +25 May 10, 2011 Experimental Rock Gang Gang Dance - Eye Contact Gang Gang Dance Eye Contact 3FnTGwJUmashhPZm9T87k4 2011 +24 February 22, 2011 Ambient Pop Julianna Barwick - The Magic Place Julianna Barwick The Magic Place 3URSIUAf32gpsqPhp1ItuT 2011 +23 April 5, 2011 Singer-Songwriter Bill Callahan - Apocalypse Bill Callahan Apocalypse 4RBHgVw8kR6IpXVnZhZrh0 2011 +22 June 21, 2011 Ambient The Caretaker - An Empty Bliss Beyond This World The Caretaker An Empty Bliss Beyond This World 2011 +21 August 8, 2011 Hip Hop Kanye West & Jay-Z - Watch the Throne Kanye West & Jay-Z Watch the Throne 1YwzJz7CrV9fd9Qeb6oo1d 2011 +20 January 31, 2011 Microhouse Nicolas Jaar - Space Is Only Noise Nicolas Jaar Space Is Only Noise 0tUJcqDuXHNkaPKLN0lQhT 2011 +19 August 15, 2011 Hip Hop Danny Brown - XXX Danny Brown XXX 3j21GO4l4W0w7hdF8JVPmo 2011 +18 November 8, 2011 Experimental Atlas Sound - Parallax Atlas Sound Parallax 6tMrRLV4mPCKOGZxWXF71P 2011 +17 March 2, 2011 Electronic Clams Casino - Instrumental Clams Casino Instrumental 2011 +16 March 8, 2011 Folk Rock Kurt Vile - Smoke Ring for My Halo Kurt Vile Smoke Ring for My Halo 32a7BrNNTAu7BVb6DcsMLP 2011 +15 May 3, 2011 Indie Folk Fleet Foxes - Helplessness Blues Fleet Foxes Helplessness Blues 3l7iMXJ0jqFnIYZRyCUewC 2011 +14 June 28, 2011 Hip Hop Shabazz Palaces - Black Up Shabazz Palaces Black Up 6xmJwIZr8GXrSTiYa9UYXG 2011 +13 May 10, 2011 Experimental EMA - Past Life Martyred Saints EMA Past Life Martyred Saints 08OhqUM0MeAuHd9HdryHB4 2011 +12 February 8, 2011 Art Pop James Blake - James Blake James Blake James Blake 0qY6lBQSi8IMJjHYDPdAqX 2011 +11 September 13, 2011 Art Pop St. Vincent - Strange Mercy St. Vincent Strange Mercy 1Lci4bx7JIuCC8pnBNX7ds 2011 +10 March 21, 2011 R&B The Weeknd - House of Balloons The Weeknd House of Balloons 2011 +9 October 18, 2011 Indie Rock Real Estate - Days Real Estate Days 43uj7422MLR9MRBXSki0El 2011 +8 November 15, 2011 Hip Hop Drake - Take Care Drake Take Care 63WdJvk8G9hxJn8u5rswNh 2011 +7 April 19, 2011 Art Pop tUnE-yArDs - w h o k i l l tUnE-yArDs w h o k i l l 7rBLvpL7ZWi1YCSXSLUZKF 2011 +6 November 8, 2011 Drone Oneohtrix Point Never - Replica Oneohtrix Point Never Replica 6zUTN3bN8rsgdy1lsFXI7L 2011 +5 September 13, 2011 Indie Pop Girls - Father, Son, Holy Ghost Girls Father, Son, Holy Ghost 66wRO7SK0Wo1KS40en2tua 2011 +4 February 15, 2011 Alternative Rock PJ Harvey - Let England Shake PJ Harvey Let England Shake 7f1aXd7Gd5H9IqFu36zw6m 2011 +3 October 18, 2011 Dream Pop M83 - Hurry Up, We're Dreaming M83 Hurry Up, We're Dreaming 6MBuQugGuX7VMBX0uiBnAQ 2011 +2 January 25, 2011 Sophisti-Pop Destroyer - Kaputt Destroyer Kaputt 1clYDgHxfhzxWQJH0ieRpx 2011 +1 June 21, 2011 Indie Folk Bon Iver - Bon Iver, Bon Iver Bon Iver Bon Iver, Bon Iver 0ZMzEAuUIylHgetdWqzcHU 2011 +50 February 21, 2012 Alt-Country Lambchop - Mr. M Lambchop Mr. M 5jIMNS1NP5sFkagCXvFiak 2012 +49 November 12, 2012 Electronic Crystal Castles - (III) Crystal Castles (III) 1NIfkZIYVAO6vnfmFOilHc 2012 +48 June 19, 2012 Electronic Peaking Lights - Lucifer Peaking Lights Lucifer 6ABsJ2gXpRBMjFM7oZ5xsr 2012 +47 February 21, 2012 Doom Metal Pallbearer - Sorrow and Extinction Pallbearer Sorrow and Extinction 4vcqtRCeKGALWvOg910jJi 2012 +46 April 7, 2012 Dubstep Rustie - Essential Mix Rustie Essential Mix 2012 +45 May 22, 2012 Hip Hop El-P - Cancer 4 Cure El-P Cancer 4 Cure 5zCfx4NLmvjmI3mwYJgdlT 2012 +44 October 9, 2012 Noise Rock METZ - METZ METZ METZ 5D6PdcR7qTaH6iu6WZlTOP 2012 +43 October 16, 2012 Singer-Songwriter Mac DeMarco - 2 Mac DeMarco 2 7mQ1elwuFdy0LBjAxv4spP 2012 +42 January 6, 2012 Hip Hop Rick Ross - Rich Forever Rick Ross Rich Forever 2Yeiz6V1YuHFz7iiWCC0SE 2012 +41 September 25, 2012 Lo-Fi Dum Dum Girls - End of Daze Dum Dum Girls End of Daze 2dOaomLOpKSelI1csdix04 2012 +40 June 26, 2012 Shoegaze DIIV - Oshin DIIV Oshin 7C2mLHV7cgF2l0LLzOd2FT 2012 +39 June 11, 2012 Synthpop Hot Chip - In Our Heads Hot Chip In Our Heads 31UV1XHUWAUfKH8vQQ0DD0 2012 +38 April 2, 2012 Neo-Psychedelia Lotus Plaza - Spooky Action at a Distance Lotus Plaza Spooky Action at a Distance 0F6VxVTyRvVe00d2AjylBK 2012 +37 April 17, 2012 Hip Hop Future - Pluto Future Pluto 1yNIBzlvPVBALSPkUMq1ma 2012 +36 February 7, 2012 Singer-Songwriter Sharon Van Etten - Tramp Sharon Van Etten Tramp 6E5Krpxb51P5DGM9da5lbq 2012 +35 July 24, 2012 Electronic TNGHT - TNGHT TNGHT TNGHT 4e0s9NinQo02X4exDDmW65 2012 +34 January 24, 2012 Electropop Chairlift - Something Chairlift Something 4nYzn3xOXQsltWZ5AIQns7 2012 +33 April 24, 2012 Experimental Actress - R.I.P. Actress R.I.P. 1iw1gr9TPYhIwmgoLPcRRG 2012 +32 March 12, 2012 Hip Hop Action Bronson / Party Supplies - Blue Chips Action Bronson / Party Supplies Blue Chips 2012 +31 April 17, 2012 Space Rock Spiritualized - Sweet Heart Sweet Light Spiritualized Sweet Heart Sweet Light 4O1ojhBMVNXH8Q9z2x37rX 2012 +30 March 6, 2012 Noise Rock The Men - Open Your Heart The Men Open Your Heart 4jcPjj6FuTPOKcREt9KfOs 2012 +29 September 4, 2012 Singer-Songwriter Cat Power - Sun Cat Power Sun 2JQgZJD5VKJkBMHBCkGQO0 2012 +28 September 18, 2012 Ambient Pop How to Dress Well - Total Loss How to Dress Well Total Loss 4aRufU4tMSZQ1NdmMO660O 2012 +27 February 14, 2012 Deep House John Talabot - ƒIN John Talabot ƒIN 3m779E07PwAQ3841RKHolm 2012 +26 March 6, 2012 Art Pop Julia Holter - Ekstasis Julia Holter Ekstasis 5KcephG2lFEcBpU616rsOQ 2012 +25 January 14, 2012 Hip Hop Schoolboy Q - Habits & Contradictions Schoolboy Q Habits & Contradictions 2XA9ImMQL9h3nFUMkHLL38 2012 +24 July 24, 2012 Synthpop Purity Ring - Shrines Purity Ring Shrines 7ppypgQppMf3mkRbZxYIFM 2012 +23 October 2, 2012 R&B Miguel - Kaleidoscope Dream Miguel Kaleidoscope Dream 6vgPmXpCwKpPjV6fLynFtH 2012 +22 October 2, 2012 IDM Flying Lotus - Until the Quiet Comes Flying Lotus Until the Quiet Comes 40aG9ahuLnAv96yoFG75Uy 2012 +21 August 21, 2012 Psychedelic Pop Ariel Pink's Haunted Graffiti - Mature Themes Ariel Pink's Haunted Graffiti Mature Themes 2TIa83EYZBDWfmEj6VVoWu 2012 +20 August 20, 2012 Soul Jessie Ware - Devotion Jessie Ware Devotion 467ptCjichGtFduanf3jMX 2012 +19 January 24, 2012 Indie Rock Cloud Nothings - Attack on Memory Cloud Nothings Attack on Memory 1JgpvdsSfE94eZzYBk6ph9 2012 +18 June 26, 2012 Garage Rock Ty Segall Band - Slaughterhouse Ty Segall Band Slaughterhouse 6QhMtcOQO98vzhYdzfjRAs 2012 +18 April 24, 2012 Garage Rock Ty Segall & White Fence - Hair Ty Segall & White Fence Hair 1CbEjRtaPQ1EKjYUSxf73F 2012 +17 October 15, 2012 Art Pop Bat For Lashes - The Haunted Man Bat For Lashes The Haunted Man 6rg92rv1hTB5fDfm85cWgq 2012 +16 February 12, 2012 Dubstep Burial - Kindred Burial Kindred 5OroU79gvn4zLctbiLswdI 2012 +15 July 10, 2012 Art Pop Dirty Projectors - Swing Lo Magellan Dirty Projectors Swing Lo Magellan 1WhF5ZiI5V5laq2nGLoF99 2012 +14 October 29, 2012 Dub Techno Andy Stott - Luxury Problems Andy Stott Luxury Problems 6XpD4GhQq4olgxxgX6BXdv 2012 +13 May 15, 2012 Hip Hop Killer Mike - R.A.P. Music Killer Mike R.A.P. Music 5EAhUoAz1G3WTvIfGZvmrh 2012 +12 October 16, 2012 Post-Rock Godspeed You! Black Emperor - 'Allelujah! Don't Bend! Ascend! Godspeed You! Black Emperor 'Allelujah! Don't Bend! Ascend! 30bTXMxsMsYn7QbwmDW3rj 2012 +11 June 5, 2012 Garage Rock Japandroids - Celebration Rock Japandroids Celebration Rock 2sY9WYVH022ulyAYaqvXLW 2012 +10 September 18, 2012 Indie Folk Grizzly Bear - Shields Grizzly Bear Shields 57LAEzKL94ZHwbIkUWYCDY 2012 +9 April 21, 2012 Hip Hop Death Grips - The Money Store Death Grips The Money Store 1PQDjdBpHPikAodJqjzm6a 2012 +8 March 26, 2012 Synthpop Chromatics - Kill for Love Chromatics Kill for Love 5Rz4NIqwoYGDbIh40UTylV 2012 +7 May 15, 2012 Dream Pop Beach House - Bloom Beach House Bloom 68pikiNcGlWj3XFirs5O2R 2012 +6 February 21, 2012 Synthpop Grimes - Visions Grimes Visions 48a7rOjTzpD1zzJAteeveE 2012 +5 August 28, 2012 Post-Rock Swans - The Seer Swans The Seer 1ijaW4mgV7Xz7DK5cXyjoo 2012 +4 October 9, 2012 Psychedelic Rock Tame Impala - Lonerism Tame Impala Lonerism 76vY9yohh4kVwSKkyKbyEQ 2012 +3 June 19, 2012 Singer-Songwriter Fiona Apple - The Idler Wheel Fiona Apple The Idler Wheel 7uccppFArLf9dNOwDftOZa 2012 +2 July 17, 2012 Alternative R&B Frank Ocean - Channel Orange Frank Ocean Channel Orange 623Ef2ZEB3Njklix4PC0Rs 2012 +1 October 22, 2012 Hip Hop Kendrick Lamar - good kid, m.A.A.d. city Kendrick Lamar good kid, m.A.A.d. city 1DqhWr73Fh5yoNzKLas0G3 2012 +50 October 8, 2013 Hip Hop Pusha T - My Name Is My Name Pusha T My Name Is My Name 4f0X8H8GetFqQ3SwMFpXmH 2013 +49 August 19, 2013 Art Pop Julia Holter - Loud City Song Julia Holter Loud City Song 2rbppUb6T63d41sk7dQDfL 2013 +48 July 9, 2013 Grunge Speedy Ortiz - Major Arcana Speedy Ortiz Major Arcana 5SEhuymkFPl8ZA44dMQRrN 2013 +47 October 14, 2013 IDM The Range - Nonfiction The Range Nonfiction 1fbBkO7KFv8GcO4Y0l30QI 2013 +46 November 5, 2013 Electropop M.I.A. - Matangi M.I.A. Matangi 3dAxXNscIj0p53lBMEziYR 2013 +45 July 23, 2013 Electronic Fuck Buttons - Slow Focus Fuck Buttons Slow Focus 2I1PxMR8wj1Y6k88bselsq 2013 +44 March 19, 2013 Pop Justin Timberlake - The 20/20 Experience Justin Timberlake The 20/20 Experience 41br7lBSZOr9RjJAjk0om6 2013 +43 May 21, 2013 Indie Rock The National - Trouble Will Find Me The National Trouble Will Find Me 2JhR4tjuc3MIKa8v2JaKze 2013 +42 March 5, 2013 Soul Rhye - Woman Rhye Woman 6b1HPtDuYioXwmw5xLLFQ9 2013 +41 October 25, 2013 Indie Folk Mutual Benefit - Love's Crushing Diamond Mutual Benefit Love's Crushing Diamond 7bdiB9i8o2je2kaPylQ3IF 2013 +40 August 18, 2012 Garage Rock Parquet Courts - Light Up Gold Parquet Courts Light Up Gold 2txqIXQU0GCnzg8mTxw1lY 2013 +39 January 15, 2013 Hip Hop A$AP Rocky - Long.Live.A$AP A$AP Rocky Long.Live.A$AP 5WHY4T7LcWAJPb1ddChotC 2013 +38 June 11, 2013 Electronic Boards of Canada - Tomorrow's Harvest Boards of Canada Tomorrow's Harvest 159ORixBSSemxiualv1Woj 2013 +37 June 3, 2013 Microhouse Jon Hopkins - Immunity Jon Hopkins Immunity 3T0f43xzDQQKvQgFTKjPqq 2013 +36 September 24, 2013 Synthpop CHVRCHES - The Bones of What You Believe CHVRCHES The Bones of What You Believe 1l9bp6Fz4xkN673dWN5cpy 2013 +35 March 19, 2013 Americana Phosphorescent - Muchacho Phosphorescent Muchacho 2he3CA9Gg1XLCsBiMAaXiz 2013 +34 August 26, 2013 Experimental Forest Swords - Engravings Forest Swords Engravings 74UnLsuHAp1505hHzwcjPR 2013 +33 December 17, 2012 Dubstep Burial - Truant / Rough Sleeper Burial Truant / Rough Sleeper 6kboqwpIVOGW2p5CF3LDE9 2013 +32 March 25, 2013 Deep House DJ Koze - Amygdala DJ Koze Amygdala 7vv9qusEblI1f1LcIdYgEc 2013 +31 February 26, 2013 R&B Autre Ne Veut - Anxiety Autre Ne Veut Anxiety 6b9Uh2Pp3nc3EJw9pvfsSt 2013 +30 May 3, 2013 Indie Rock Deerhunter - Monomania Deerhunter Monomania 68gYjtaIWlvCscoxuCqAiZ 2013 +29 April 15, 2013 Drone The Haxan Cloak - Excavation The Haxan Cloak Excavation 16DAUZi7MAWliYIiJvaQyk 2013 +28 June 26, 2013 Hip Hop Run the Jewels - Run the Jewels Run the Jewels Run the Jewels 5okrDWqn2b0dX1VBpaZRxQ 2013 +27 September 3, 2013 Singer-Songwriter Neko Case - The Worse Things Get ... Neko Case The Worse Things Get ... 3V7aa2rOEqNczucNlTH32c 2013 +26 April 8, 2013 Art Pop James Blake - Overgrown James Blake Overgrown 12ExNn0hx85F8UNzoKCyCD 2013 +25 September 24, 2013 Hip Hop Drake - Nothing Was the Same Drake Nothing Was the Same 2ZUFSbIkmFkGag000RWOpA 2013 +24 October 1, 2013 Electronic Oneohtrix Point Never - R Plus Seven Oneohtrix Point Never R Plus Seven 68PRq4zj7YXMwiUq6FNGvR 2013 +23 October 22, 2013 Footwork DJ Rashad - Double Cup DJ Rashad Double Cup 4J7qkorMbPmJQy79SntDA8 2013 +22 March 5, 2013 Singer-Songwriter Waxahatchee - Cerulean Salt Waxahatchee Cerulean Salt 4OfohS2lFQOeCXsrNcdh5s 2013 +21 November 18, 2013 R&B Blood Orange - Cupid Deluxe Blood Orange Cupid Deluxe 5Pn6Nexd7zbCMkdVGeml0m 2013 +20 April 1, 2013 Alternative R&B Jai Paul - Jai Paul Jai Paul Jai Paul 2013 +19 August 20, 2013 Hip Hop Earl Sweatshirt - Doris Earl Sweatshirt Doris 5vRfIDOPJHy3W2wHWbzLlE 2013 +18 September 10, 2013 R&B Janelle Monáe - The Electric Lady Janelle Monáe The Electric Lady 3bnHtSmmsgJiG82hGCmsq9 2013 +17 September 30, 2013 Indie Rock HAIM - Days Are Gone HAIM Days Are Gone 0eXAgjcMHbFt3fS0XXp8Kw 2013 +16 September 17, 2013 Singer-Songwriter Bill Callahan - Dream River Bill Callahan Dream River 09o1j1zgl0n8H6EJZXIvRi 2013 +15 October 29, 2013 Synthpop Sky Ferreira - Night Time, My Time Sky Ferreira Night Time, My Time 1bvCVYPVl445mO690M2dOr 2013 +14 April 8, 2013 Electronic The Knife - Shaking the Habitual The Knife Shaking the Habitual 7eDaaP6J8Q3cYVTGRmARwx 2013 +13 April 9, 2013 Folk Rock Kurt Vile - Wakin on a Pretty Daze Kurt Vile Wakin on a Pretty Daze 2imxOfDIDk2voAYCZP88u2 2013 +12 April 30, 2013 Hip Hop Chance The Rapper - Acid Rap Chance The Rapper Acid Rap 2VBcztE58pBKjIDS5oEgFh 2013 +11 October 8, 2013 Art Pop Darkside - Psychic Darkside Psychic 7jqNrm1l4wSxNYSjgK7tmF 2013 +10 October 29, 2013 Indie Rock Arcade Fire - Reflektor Arcade Fire Reflektor 4E0m7AIVc2d2QZMrMNXdMZ 2013 +9 May 6, 2013 Post-Punk Savages - Silence Yourself Savages Silence Yourself 0aMC5DDAF86GvYNPaivEKd 2013 +8 May 21, 2013 Ambient Pop Majical Cloudz - Impersonator Majical Cloudz Impersonator 70EudAo73w9QvcF2u5K18E 2013 +7 May 21, 2013 Nu-Disco Daft Punk - Random Access Memories Daft Punk Random Access Memories 4m2880jivSbbyEGAKfITCa 2013 +6 June 11, 2013 Blackgaze Deafheaven - Sunbather Deafheaven Sunbather 2kKXGWaCEl06EKZ4DxBJIT 2013 +5 October 8, 2013 Hip Hop Danny Brown - OLD Danny Brown OLD 5SC0415RIGVX9ZfL0tfbAl 2013 +4 February 3, 2013 Shoegaze My Bloody Valentine - m b v My Bloody Valentine m b v 2013 +3 June 3, 2013 Electronic Disclosure - Settle Disclosure Settle 1bEnTsCvG4FPW7NpIEC5Zc 2013 +2 June 18, 2013 Hip Hop Kanye West - Yeezus Kanye West Yeezus 0XTAmejG8F97wF5MWoVbaY 2013 +1 May 14, 2013 Indie Rock Vampire Weekend - Modern Vampires of the City Vampire Weekend Modern Vampires of the City 2Qi2SySN2ePZwMLDSv9Krn 2013 +50 May 26, 2014 Dark Ambient Ben Frost - A U R O R A Ben Frost A U R O R A 2hVGkLbETH03giJCwRfjaN 2014 +49 September 23, 2014 Dream Pop Mr Twin Sister - Mr Twin Sister Mr Twin Sister Mr Twin Sister 6r34aMIPIjbKLtWcbdyseT 2014 +48 November 3, 2014 IDM Clark - Clark Clark Clark 13944rqffz0QLBUe04vY0r 2014 +47 September 16, 2014 Noise Rock Shellac - Dude Incredible Shellac Dude Incredible 2ESIcPobinuSSEwX8kzhY0 2014 +46 August 25, 2014 R&B Ariana Grande - My Everything Ariana Grande My Everything 41zTgMSJC9mF6NyBkXXxZr 2014 +45 November 18, 2014 Electronic Andy Stott - Faith in Strangers Andy Stott Faith in Strangers 1BzMONuUlgUnqOrg2aQeAY 2014 +44 June 24, 2014 Shoegaze A Sunny Day in Glasgow - Sea When Absent A Sunny Day in Glasgow Sea When Absent 4rkH6ty2BmobpmERezgUBF 2014 +43 March 18, 2014 Hip Hop Freddie Gibbs & Madlib - Piñata Freddie Gibbs & Madlib Piñata 43uErencdmuTRFZPG3zXL1 2014 +42 May 27, 2014 Chamber Pop Owen Pallett - In Conflict Owen Pallett In Conflict 6qkTG87rvoVj6RqzHHymbi 2014 +41 March 17, 2014 Deep House Leon Vynehall - Music for the Uninvited Leon Vynehall Music for the Uninvited 3Eq4qTWygAzH186V3q5D8z 2014 +40 August 26, 2014 Garage Rock Ty Segall - Manipulator Ty Segall Manipulator 0aXcmVjzU9v63FhFcjgLxp 2014 +39 April 29, 2014 Post-Punk Ought - More Than Any Other Day Ought More Than Any Other Day 6Y1StvNpDomvTkBgtgHIlU 2014 +38 May 27, 2014 Art Pop Hundred Waters - The Moon Rang Like A Bell Hundred Waters The Moon Rang Like A Bell 051I8OUCXMiEnua64QQKy3 2014 +37 March 18, 2014 Noise Rock Perfect Pussy - Say Yes to Love Perfect Pussy Say Yes to Love 0vAWlFqwQXVhsW7SR3rXv1 2014 +36 October 7, 2014 Alternative R&B Tinashe - Aquarius Tinashe Aquarius 6zXUDBGLbrB9Kgkw2Y3F7L 2014 +35 July 29, 2014 Hip Hop Shabazz Palaces - Lese Majesty Shabazz Palaces Lese Majesty 76w2I0nUWKdsDe9gubdpW5 2014 +34 April 1, 2014 Indie Rock Cloud Nothings - Here and Nowhere Else Cloud Nothings Here and Nowhere Else 0hLNDK6qqmScdwxU7kvnXn 2014 +33 September 29, 2014 Hip Hop Rich Gang - Tha Tour Part 1 Rich Gang Tha Tour Part 1 2014 +32 June 17, 2014 Noise Rock White Lung - Deep Fantasy White Lung Deep Fantasy 7lsoGmTZp7mUBoAALe611f 2014 +31 October 27, 2014 Pop Taylor Swift - 1989 Taylor Swift 1989 2QJmrSgbdM35R67eoGQo4j 2014 +30 October 7, 2014 Post-Punk Iceage - Plowing Into the Field of Love Iceage Plowing Into the Field of Love 2zFpmXJsyl4HnMY1hZcfIe 2014 +29 June 24, 2014 Alternative R&B How to Dress Well - What Is This Heart? How to Dress Well What Is This Heart? 70o7vRADMOz75vzHBla3Z0 2014 +28 October 14, 2014 Death Industrial Pharmakon - Bestial Burden Pharmakon Bestial Burden 3HOtmw25fWCfpvAlxl9H8p 2014 +27 March 18, 2014 Hip Hop YG - My Krazy Life YG My Krazy Life 6cW84YyjHToN3qqBwKDKQX 2014 +26 October 7, 2014 Indie Rock Ex Hex - Rips Ex Hex Rips 6AiSmBuuVUi2xCIVJsXcQb 2014 +25 November 6, 2014 Hip Hop Azealia Banks - Broke With Expensive Taste Azealia Banks Broke With Expensive Taste 399M7b1n6aSepgtWTRi3za 2014 +24 June 3, 2014 Garage Rock Parquet Courts - Sunbathing Animal Parquet Courts Sunbathing Animal 3ngz9FbWGxHscqvwoGOL0u 2014 +23 May 5, 2014 Indie Pop Lykke Li - I Never Learn Lykke Li I Never Learn 4fGqfyineAZmulNxgitERh 2014 +22 March 25, 2014 Synthpop Future Islands - Singles Future Islands Singles 1dKh4z5Aayt8FFDWjO5FDh 2014 +21 June 30, 2014 Art Rock Eno • Hyde - High Life Eno • Hyde High Life 2014 +20 October 7, 2014 Hip Hop Vince Staples - Hell Can Wait Vince Staples Hell Can Wait 7mxpMxmMM8RN39YRlo08v7 2014 +19 May 27, 2014 Singer-Songwriter Sharon Van Etten - Are We There Sharon Van Etten Are We There 69EhwEgjEWQ8GqH7wqEndn 2014 +18 November 4, 2014 Electronic Arca - Xen Arca Xen 5FLsmazQWaDK9JGqdzHlN4 2014 +17 October 7, 2014 Electronic Flying Lotus - You're Dead! Flying Lotus You're Dead! 29luvT98TnqHjVDYSRbbrj 2014 +16 February 25, 2014 Art Pop St. Vincent - St. Vincent St. Vincent St. Vincent 2CJnMhwEEkS8R1ctgt5llf 2014 +15 February 18, 2014 Singer-Songwriter Angel Olsen - Burn Your Fire for No Witness Angel Olsen Burn Your Fire for No Witness 0xvDtkNKJiLclVbjLvovFU 2014 +14 March 4, 2014 Indie Rock Real Estate - Atlas Real Estate Atlas 50idJkY6FhqUesGnGHn7wB 2014 +13 August 5, 2014 Indie Rock Spoon - They Want My Soul Spoon They Want My Soul 5LOVEXnGFLMChzdUNRWRpB 2014 +12 April 1, 2014 Singer-Songwriter Mac DeMarco - Salad Days Mac DeMarco Salad Days 7xPhDaYZ2ejV04aNtdBdvj 2014 +11 September 23, 2014 Art Pop Perfume Genius - Too Bright Perfume Genius Too Bright 5qtHbJAK0C1IkacqJBqsa2 2014 +10 October 7, 2014 Electronic Caribou - Our Love Caribou Our Love 233jGebCTUPAYF5MxvLB7A 2014 +9 November 18, 2014 Psychedelic Pop Ariel Pink - pom pom Ariel Pink pom pom 4UhaqAS8V23KozB3dzLMax 2014 +8 April 8, 2014 Nu-Disco Todd Terje - It's Album Time Todd Terje It's Album Time 4pefQ21iSk8hdnxw3WSB5Y 2014 +7 February 11, 2014 Singer-Songwriter Sun Kil Moon - Benji Sun Kil Moon Benji 4pC2URLdvle8V6Um4qxh46 2014 +6 May 13, 2014 Experimental Rock Swans - To Be Kind Swans To Be Kind 1Nt5SpidxOjgIpTrhrfwAF 2014 +5 October 31, 2014 Ambient Grouper - Ruins Grouper Ruins 5ElYoVUqRQIlDekD1v6aKa 2014 +4 September 23, 2014 IDM Aphex Twin - Syro Aphex Twin Syro 6oRuinkJdTge4hpTuClEF8 2014 +3 March 18, 2014 Heartland Rock The War on Drugs - Lost in the Dream The War on Drugs Lost in the Dream 6nD9jfVVX0OadsHJqmjObO 2014 +2 August 12, 2014 Alternative R&B FKA twigs - LP1 FKA twigs LP1 7mpQVIMZ2iNnAfqodqvdhz 2014 +1 October 28, 2014 Hip Hop Run the Jewels - Run the Jewels 2 Run the Jewels Run the Jewels 2 2lPYlP4eumsjz6LBG8GCbG 2014 +50 January 15, 2015 Alternative R&B DAWN - Blackheart DAWN Blackheart 2IPKoEWJgWuM83zZOq3vkW 2015 +49 January 27, 2015 Chamber Pop Natalie Prass - Natalie Prass Natalie Prass Natalie Prass 1rRFEJvwq1Bdutw6q3Nz9e 2015 +48 May 19, 2015 Electropop Shamir - Ratchet Shamir Ratchet 7vX1WKSgkpG4jwOiaXDVT6 2015 +47 June 15, 2015 Downtempo DJ Koze - DJ-Kicks DJ Koze DJ-Kicks 4Ds2jfLg7gdR590E7gCIZp 2015 +46 March 17, 2015 Singer-Songwriter Tobias Jesso Jr. - Goon Tobias Jesso Jr. Goon 7elNFxdWSjWvtUP1gqyQGV 2015 +45 May 19, 2015 Chamber Pop Jim O'Rourke - Simple Songs Jim O'Rourke Simple Songs 2015 +44 January 13, 2015 R&B Jazmine Sullivan - Reality Show Jazmine Sullivan Reality Show 682dnvSPZ6lwCNodfMVcbh 2015 +43 August 28, 2015 Chamber Pop Destroyer - Poison Season Destroyer Poison Season 0X32RgsNanozjRrRd2DRLK 2015 +42 June 9, 2015 Art Pop Jenny Hval - Apocalypse, girl Jenny Hval Apocalypse, girl 3AeAZfwBgnhmbNEowNFvcB 2015 +41 December 4, 2015 R&B Jeremih - Late Nights: The Album Jeremih Late Nights: The Album 4p9QlliNvKndyKSPyuJlP8 2015 +40 March 24, 2015 Footwork Jlin - Dark Energy Jlin Dark Energy 2yHEmN9QGuFbFpWt21BJtd 2015 +39 May 19, 2015 Art Pop Holly Herndon - Platform Holly Herndon Platform 1x1agDGl1Y7npXRF7u3prS 2015 +38 November 20, 2015 Electronic Arca - Mutant Arca Mutant 5JJEl39js1CIZvzrAnX4kw 2015 +37 September 11, 2015 Electropop Empress Of - Me Empress Of Me 6Gr9l3W2dr8YZ1SzJwRRKE 2015 +36 October 2, 2015 R&B Janet Jackson - Unbreakable Janet Jackson Unbreakable 1zAcFjwYpvXq0GFm0vRDEa 2015 +35 October 16, 2015 Chillwave Neon Indian - VEGA INTL. Night School Neon Indian VEGA INTL. Night School 5jgrzAcDHJ5Nb2viW52OlR 2015 +34 August 21, 2015 Dance Pop Carly Rae Jepsen - E•MO•TION Carly Rae Jepsen E•MO•TION 49iTBKGf47W1CJMQ8W2jHB 2015 +33 December 10, 2015 Hip Hop Archy Marshall - A New Place 2 Drown Archy Marshall A New Place 2 Drown 56tUDDTfqaHqvcIyUfbuI0 2015 +32 August 7, 2015 Hip Hop Dr. Dre - Compton: A Soundtrack by Dr. Dre Dr. Dre Compton: A Soundtrack by Dr. Dre 2015 +31 October 9, 2015 Alternative R&B Kelela - Hallucinogen Kelela Hallucinogen 5vI1UvJLIPAKf1kvzgPaTO 2015 +30 October 16, 2015 Indie Rock Deerhunter - Fading Frontier Deerhunter Fading Frontier 0y5XgOUEiJSSIMxC4ALRAC 2015 +29 January 6, 2015 Hip Hop Rae Sremmurd - SremmLife Rae Sremmurd SremmLife 4ZmCBqmAmzZaw6AKnlXqQI 2015 +28 August 28, 2015 Dream Pop Beach House - Depression Cherry Beach House Depression Cherry 2dOMRQFWGtFnBVBaqMUjel 2015 +27 January 20, 2015 Indie Rock Sleater-Kinney - No Cities to Love Sleater-Kinney No Cities to Love 1l7gMbzE7RRAdYddJSfM5B 2015 +26 October 2, 2015 Atmospheric Black Metal Deafheaven - New Bermuda Deafheaven New Bermuda 2e4xOasRFhJn4x2MBM5pdu 2015 +25 March 23, 2015 Hip Hop Earl Sweatshirt - I Don't Like Shit, I Don't Go Outside Earl Sweatshirt I Don't Like Shit, I Don't Go Outside 3wUv2IjD5hPrqlPakpczQa 2015 +24 June 22, 2015 Neo-Soul Thundercat - The Beyond / Where the Giants Roam Thundercat The Beyond / Where the Giants Roam 7vgnnPWflv8g0vQYOberLn 2015 +23 September 25, 2015 Singer-Songwriter Kurt Vile - b'lieve i'm goin down Kurt Vile b'lieve i'm goin down 2uRTsStAmo7Z2UwCIvuwMv 2015 +22 January 13, 2015 Psychedelic Pop Panda Bear - Panda Bear Meets the Grim Reaper Panda Bear Panda Bear Meets the Grim Reaper 6bHsJXJoEdQTw3tUpHV8iB 2015 +21 May 29, 2015 Neo-Soul Donnie Trumpet & The Social Experiment - Surf Donnie Trumpet & The Social Experiment Surf 3eM1KTKmpqrQOvuvYY42cr 2015 +20 November 6, 2015 Electronic Floating Points - Elaenia Floating Points Elaenia 0SDLAAGQmK8DWxkX0s9OGE 2015 +19 July 17, 2015 Hip Hop Future - DS2 Future DS2 0fUy6IdLHDpGNwavIlhEsl 2015 +18 September 25, 2015 Art Pop Julia Holter - Have You in My Wilderness Julia Holter Have You in My Wilderness 1kVTV6AoeMjAOMOJyVfYOl 2015 +17 February 12, 2015 Hip Hop Drake - If You're Reading This It's Too Late Drake If You're Reading This It's Too Late 6K77I7FVTV0pxUfQikCbxj 2015 +16 August 13, 2015 Art Pop FKA twigs - M3LL155X FKA twigs M3LL155X 7rSESybi14vAuEVzkipODD 2015 +15 January 20, 2015 Art Pop Björk - Vulnicura Björk Vulnicura 1ttnHZ0HVGMSMTJdZZ7kYK 2015 +14 April 16, 2015 Hip Hop Young Thug - Barter 6 Young Thug Barter 6 0BsMZIueWsJLWng8A7sE8e 2015 +13 October 23, 2015 Singer-Songwriter Joanna Newsom - Divers Joanna Newsom Divers 2015 +12 February 10, 2015 Singer-Songwriter Father John Misty - I Love You, Honeybear Father John Misty I Love You, Honeybear 7buEcyw6fJF3WPgr06BomH 2015 +11 November 13, 2015 Electronic Oneohtrix Point Never - Garden of Delete Oneohtrix Point Never Garden of Delete 4l4tgdU69cbukMzliC4xI6 2015 +10 May 5, 2015 Jazz Fusion Kamasi Washington - The Epic Kamasi Washington The Epic 1jTw1oalZ4vF8jmpmILCdh 2015 +9 March 24, 2015 Indie Rock Courtney Barnett - Sometimes I Sit and Think, and Sometimes I Just Sit Courtney Barnett Sometimes I Sit and Think, and Sometimes I Just Sit 5FpTrIArvT20xUSpGRXGLY 2015 +8 June 30, 2015 Alternative R&B Miguel - Wildheart Miguel Wildheart 6W8ZsoinSMViZMh9aYK7gQ 2015 +7 December 15, 2014 Neo-Soul D'Angelo - Black Messiah D'Angelo Black Messiah 5Hfbag0SsHxafx1SySFSX6 2015 +6 March 31, 2015 Indie Folk Sufjan Stevens - Carrie & Lowell Sufjan Stevens Carrie & Lowell 0U8DeqqKDgIhIiWOdqiQXE 2015 +5 July 17, 2015 Psychedelic Pop Tame Impala - Currents Tame Impala Currents 0rxKf57PZvWEoU8v3m5W2q 2015 +4 June 30, 2015 Hip Hop Vince Staples - Summertime '06 Vince Staples Summertime '06 59olnuVrXXgrDH4wknpDLC 2015 +3 November 6, 2015 Art Pop Grimes - Art Angels Grimes Art Angels 5hB4jVN4ZHpubyiMmW81K1 2015 +2 June 1, 2015 UK Bass Jamie xx - In Colour Jamie xx In Colour 5jBKTppNIUpcrNKbr8jbsQ 2015 +1 March 16, 2015 Hip Hop Kendrick Lamar - To Pimp a Butterfly Kendrick Lamar To Pimp a Butterfly 7ycBtnsMtyVbbwTfJwRjSP 2015 +50 July 14, 2016 Hip Hop 21 Savage & Metro Boomin - Savage Mode 21 Savage & Metro Boomin Savage Mode 4I3EcXD4e3KcEoDJfFEZ5b 2016 +49 February 5, 2016 Synthpop Porches - Pool Porches Pool 7kNB1l6WPFlZxH5MtgtW3j 2016 +48 April 1, 2016 Indie Pop Frankie Cosmos - Next Thing Frankie Cosmos Next Thing 7mTf6AXzDt1q7Iy4Vig1U5 2016 +47 March 14, 2016 Hip Hop Kamaiyah - A Good Night in the Ghetto Kamaiyah A Good Night in the Ghetto 2016 +46 February 12, 2016 Indie Rock Pinegrove - Cardinal Pinegrove Cardinal 2SmrUzUMMOYQqoPuOhlhjw 2016 +45 June 3, 2016 Folk William Tyler - Modern Country William Tyler Modern Country 0AlKGJjZriUhapXB3hyW6h 2016 +44 January 29, 2016 Hip Hop Kevin Gates - Islah Kevin Gates Islah 5Hs43ta4vAYKRRRR7DKjt9 2016 +43 October 21, 2016 Psychedelic Folk Weyes Blood - Front Row Seat to Earth Weyes Blood Front Row Seat to Earth 5priHeMXIoEdSy731w0Zfe 2016 +42 August 25, 2016 Hip Hop Vince Staples - Prima Donna Vince Staples Prima Donna 2haR5qnQopCdVASZ92YTGn 2016 +41 June 10, 2016 Ambient Huerco S. - For Those Of You Who Have Never (And Also Those Who Have) Huerco S. For Those Of You Who Have Never (And Also Those Who Have) 1MATu35vnVRNkhxFH3hMxP 2016 +40 March 11, 2016 Avant-Garde Jazz Vijay Iyer & Wadada Leo Smith - A Cosmic Rhythm with Each Stroke Vijay Iyer & Wadada Leo Smith A Cosmic Rhythm with Each Stroke 2016 +39 February 19, 2016 Deep House Moodymann - DJ-Kicks Moodymann DJ-Kicks 26ko9KvGa0mKqByhi74nsn 2016 +38 July 8, 2016 Hip Hop Schoolboy Q - Blank Face LP Schoolboy Q Blank Face LP 0YbpATCIng8Fz2JrfHmEf7 2016 +37 February 5, 2016 R&B King - We Are King King We Are King 3FYKiMNG19UUdbs8xhpZc7 2016 +36 July 7, 2016 Alternative R&B Jamila Woods - HEAVN Jamila Woods HEAVN 2ha1TUv0o6VnQddOci7GIb 2016 +35 September 23, 2016 Indie Rock Hamilton Leithauser + Rostam - I Had A Dream That You Were Mine Hamilton Leithauser + Rostam I Had A Dream That You Were Mine 2TDuXjtpO4tdvm86KVNMk0 2016 +34 April 15, 2016 Folk Rock Kevin Morby - Singing Saw Kevin Morby Singing Saw 1s8RmcZjTuvDt9eQ4MAKLI 2016 +33 July 1, 2016 Neo-Soul Maxwell - blackSUMMERS'night Maxwell blackSUMMERS'night 6pjoIUNpMtQaSJvRUmsnSh 2016 +32 October 3, 2015 MPB Elza Soares - A Mulher do Fim do Mundo Elza Soares A Mulher do Fim do Mundo 0YwlYu2ecOk0HgjZL7L6s0 2016 +31 June 3, 2016 Indie Rock Whitney - Light Upon The Lake Whitney Light Upon The Lake 5yMCA6HdFAeL1aqUjxO3MO 2016 +30 March 4, 2016 Jazz Fusion Esperanza Spalding - Emily's D+Evolution Esperanza Spalding Emily's D+Evolution 4IHHAv4Bu45x64Lf2ezRrT 2016 +29 April 1, 2016 Folktronica Kaitlyn Aurelia Smith - EARS Kaitlyn Aurelia Smith EARS 5T9rGi62i1O7wRu3c2f3q4 2016 +28 October 21, 2016 Neo-Soul NxWorries - Yes Lawd! NxWorries Yes Lawd! 7v4ntSy3suhIs5Vy96GXVZ 2016 +27 July 31, 2016 Hip Hop Noname - Telefone Noname Telefone 18Scpsg5OV1iYNtSaCsjwz 2016 +26 April 8, 2016 Indie Rock Parquet Courts - Human Performance Parquet Courts Human Performance 4RNew41ZeRuoRNg3YAhvpe 2016 +25 May 6, 2016 Electronic KAYTRANADA - 99.9% KAYTRANADA 99.9% 1dZZh7PvVgce1DDsDPzy8Z 2016 +24 May 20, 2016 Indie Rock Car Seat Headrest - Teens of Denial Car Seat Headrest Teens of Denial 0czBomMxaMCzanUaFhESOW 2016 +23 September 30, 2016 Art Pop Jenny Hval - Blood Bitch Jenny Hval Blood Bitch 18XEoSRglCwQLGClea3686 2016 +22 June 17, 2016 Hip Hop YG - Still Brazy YG Still Brazy 4gVJj9kuG6ue5Sb8egEPyn 2016 +21 August 26, 2016 Hip Hop Young Thug - Jeffery Young Thug Jeffery 7EpUpNUkkEGnaCvkcn1j4H 2016 +20 September 30, 2016 Ambient Pop Nicolas Jaar - Sirens Nicolas Jaar Sirens 2EvZiOMBlC9b5hbjbZCjZv 2016 +19 January 27, 2016 R&B Rihanna - ANTI Rihanna ANTI 3Q149ZH46Z0f3oDR7vlDYV 2016 +18 June 17, 2016 Indie Rock Mitski - Puberty 2 Mitski Puberty 2 16i5KnBjWgUtwOO7sVMnJB 2016 +17 October 21, 2016 Singer-Songwriter Leonard Cohen - You Want It Darker Leonard Cohen You Want It Darker 3jeTB3j3QmUs8SPIVleHtU 2016 +16 March 4, 2016 Hip Hop Kendrick Lamar - untitled unmastered. Kendrick Lamar untitled unmastered. 5WSPnYTQ6YZ1UvBRi5quhO 2016 +15 September 9, 2016 Art Rock Nick Cave & The Bad Seeds - Skeleton Tree Nick Cave & The Bad Seeds Skeleton Tree 34xaLN7rDecGEK5UGIVbeJ 2016 +14 June 27, 2016 Alternative R&B Blood Orange - Freetown Sound Blood Orange Freetown Sound 3Z2XUjgVj5ZkCGpU7b2qtY 2016 +13 January 15, 2016 Neo-Soul Anderson .Paak - Malibu Anderson .Paak Malibu 4VFG1DOuTeDMBjBLZT7hCK 2016 +12 September 30, 2016 Art Pop Bon Iver - 22, A Million Bon Iver 22, A Million 1PgfRdl3lPyACfUGH4pquG 2016 +11 September 27, 2016 Hip Hop Danny Brown - Atrocity Exhibition Danny Brown Atrocity Exhibition 3e7vtKJ3m1zVh38VGq2g3H 2016 +10 May 8, 2016 Art Rock Radiohead - A Moon Shaped Pool Radiohead A Moon Shaped Pool 6vuykQgDLUCiZ7YggIpLM9 2016 +9 September 2, 2016 Indie Rock Angel Olsen - My Woman Angel Olsen My Woman 5M8xQaQZuW2LZGVXZ3mlKN 2016 +8 May 6, 2016 Art Pop ANOHNI - Hopelessness ANOHNI Hopelessness 3dAx4u7AJy72a6M1ms6uYF 2016 +7 November 11, 2016 Hip Hop A Tribe Called Quest - We got it from Here... Thank You 4 Your service A Tribe Called Quest We got it from Here... Thank You 4 Your service 3WvQpufOsPzkZvcSuynCf3 2016 +6 May 13, 2016 Hip Hop Chance The Rapper - Coloring Book Chance The Rapper Coloring Book 71QyofYesSsRMwFOTafnhB 2016 +5 February 14, 2016 Hip Hop Kanye West - The Life of Pablo Kanye West The Life of Pablo 7gsWAHLeT0w7es6FofOXk1 2016 +4 January 8, 2016 Art Rock David Bowie - Blackstar David Bowie Blackstar 2w1YJXWMIco6EBf0CovvVN 2016 +3 April 23, 2016 R&B Beyoncé - Lemonade Beyoncé Lemonade 4X6b6POxbjX9inC7TWQd54 2016 +2 August 20, 2016 Alternative R&B Frank Ocean - Blonde Frank Ocean Blonde 1PDX0hMmsSdq122EupvNZF 2016 +1 September 30, 2016 Alternative R&B Solange - A Seat at the Table Solange A Seat at the Table 3Yko2SxDk4hc6fncIBQlcM 2016 +50 March 31, 2017 Deep House Yaeji - Yaeji Yaeji Yaeji 0YO6XyVs6lvPeknLqOfWZY 2017 +50 November 3, 2017 Electronic Yaeji - EP2 Yaeji EP2 1haNHfsOFIrDKDFYr0pbsy 2017 +49 September 15, 2017 Hip Hop Open Mike Eagle - Brick Body Kids Still Daydream Open Mike Eagle Brick Body Kids Still Daydream 1VDnqZVFSg0xVF104kaIix 2017 +48 May 19, 2017 Indie Rock (Sandy) Alex G - Rocket (Sandy) Alex G Rocket 5Pq92omNLyQgGGrj2u4pur 2017 +47 January 27, 2017 Alternative R&B Kehlani - SweetSexySavage Kehlani SweetSexySavage 4B4in9QlrlYWSHlYSRebdC 2017 +46 June 23, 2017 Ambient Pop Laurel Halo - Dust Laurel Halo Dust 1hLcoVrolf5uh2coXHf01M 2017 +45 May 12, 2017 Indie Pop Girlpool - Powerplant Girlpool Powerplant 3V3Y2GCjloFhtI16vpHl2a 2017 +44 October 6, 2017 Art Pop Kaitlyn Aurelia Smith - The Kid Kaitlyn Aurelia Smith The Kid 0tkPzRqTyqB8h8ZLfZ0m9k 2017 +43 August 17, 2017 Hip Hop Lil B - Black Ken Lil B Black Ken 60Ca2ECXMePq5zJ0fjcQK1 2017 +42 February 24, 2017 Indie Rock Vagabon - Infinite Worlds Vagabon Infinite Worlds 2t1IC3t3dXjK1wIlsEjCez 2017 +41 September 8, 2017 Art Pop Zola Jesus - Okovi Zola Jesus Okovi 3xgTnei8Ry6o0VeOYeoZg0 2017 +40 January 13, 2017 Indie Pop The xx - I See You The xx I See You 2PXy9USZAoTSdtrxfkPBnl 2017 +39 June 16, 2017 Folk Fleet Foxes - Crack-Up Fleet Foxes Crack-Up 0xtTojp4zfartyGtbFKN3v 2017 +38 March 18, 2017 Hip Hop Drake - More Life Drake More Life 1lXY618HWkwYKJWBRYR4MK 2017 +37 January 27, 2017 Folk Julie Byrne - Not Even Happiness Julie Byrne Not Even Happiness 4D0slaYPm4oiwFRlv10rJz 2017 +36 February 3, 2017 Alternative R&B Syd - Fin Syd Fin 59Bbr32pMTFHlUb8Nv1Kr0 2017 +35 April 14, 2017 Hip Hop Playboi Carti - Playboi Carti Playboi Carti Playboi Carti 4rJgzzfFHAVFhCSt2P4I3j 2017 +34 September 8, 2017 Future Garage Mount Kimbie - Love What Survives Mount Kimbie Love What Survives 54FblbvyHNrWeAuEJqnyit 2017 +33 March 24, 2017 Tech House Kelly Lee Owens - Kelly Lee Owens Kelly Lee Owens Kelly Lee Owens 3Zx14dyUjtZcEas89nZZfn 2017 +32 July 21, 2017 Dream Pop Lana Del Rey - Lust for Life Lana Del Rey Lust for Life 7xYiTrbTL57QO0bb4hXIKo 2017 +31 September 29, 2017 Jazz Kamasi Washington - Harmony of Difference Kamasi Washington Harmony of Difference 1mFuFPBz9kBDdXylMNolu7 2017 +30 May 5, 2017 Dream Pop Slowdive - Slowdive Slowdive Slowdive 4nSWX5A4xVomzrOEGDKLQ6 2017 +29 December 25, 2016 Hip Hop Run the Jewels - Run the Jewels 3 Run the Jewels Run the Jewels 3 3v2GjFB9V5kHgrOCXn3sI9 2017 +28 September 29, 2017 Art Pop Ibeyi - Ash Ibeyi Ash 3AfBuChwSzSSufGjTAafzt 2017 +27 February 24, 2017 Hip Hop Future - HNDRXX Future HNDRXX 1P8NvRvykmDrKyfglMerMv 2017 +26 March 10, 2017 Indie Rock Jay Som - Everybody Works Jay Som Everybody Works 1ZMtgC7o6TGbzwkv5SpThU 2017 +25 January 27, 2017 Post-Punk Priests - Nothing Feels Natural Priests Nothing Feels Natural 4Q4PAknSA2wgEY0VPLCqAR 2017 +24 February 24, 2017 Neo-Soul Thundercat - Drunk Thundercat Drunk 0pP7fWQxwXJ52FsQww0YGx 2017 +23 June 9, 2017 Indie Rock Big Thief - Capacity Big Thief Capacity 2hOYLjoRQFXcdviMiwtgxe 2017 +22 October 13, 2017 Art Pop St. Vincent - MASSEDUCTION St. Vincent MASSEDUCTION 4RoOGpdrgfiIUyv0kLaC4e 2017 +21 April 7, 2017 Art Pop Arca - Arca Arca Arca 1MQO4j8QExVgmnplbIodEU 2017 +20 November 24, 2017 Art Pop Björk - Utopia Björk Utopia 037hz3oZyrgcJOheyhPMnC 2017 +19 January 27, 2017 Hip Hop Migos - Culture Migos Culture 2AvupjUeMnSffKEV05x222 2017 +18 October 27, 2017 Singer-Songwriter Julien Baker - Turn Out the Lights Julien Baker Turn Out the Lights 3uIsEwFYYV4rwRssSEJ8Lb 2017 +17 November 17, 2017 Art Pop Charlotte Gainsbourg - Rest Charlotte Gainsbourg Rest 4qViuTOzHQqtVppJtf96Hh 2017 +16 May 5, 2017 Art Pop Perfume Genius - No Shape Perfume Genius No Shape 7awgq3vvlsIeA7dZduR9x4 2017 +15 February 3, 2017 Alternative R&B Sampha - Process Sampha Process 2gUSWVHCOerKhJHZRwhVtN 2017 +14 March 24, 2017 Singer-Songwriter Mount Eerie - A Crow Looked at Me Mount Eerie A Crow Looked at Me 5p64XgvFREt1P6mC7Xl6XN 2017 +13 June 30, 2017 Hip Hop Jay-Z - 4:44 Jay-Z 4:44 2017 +12 September 1, 2017 Dance Punk LCD Soundsystem - American Dream LCD Soundsystem American Dream 4AF1M7bGCFL3LHCtXUUXw5 2017 +11 August 25, 2017 Heartland Rock The War on Drugs - A Deeper Understanding The War on Drugs A Deeper Understanding 4TkmrrpjlPoCPpGyDN3rkF 2017 +10 May 19, 2017 Footwork Jlin - Black Origami Jlin Black Origami 7526bnJCkFFnAMSQ9fsva9 2017 +9 October 27, 2017 Electropop Fever Ray - Plunge Fever Ray Plunge 3UHMhYzYnfTBEuDxb1JmxC 2017 +8 July 21, 2017 Experimental Hip Hop Tyler, the Creator - Flower Boy Tyler, the Creator Flower Boy 2nkto6YNI4rUYTLqEwWJ3o 2017 +7 June 23, 2017 Hip Hop Vince Staples - Big Fish Theory Vince Staples Big Fish Theory 5h3WJG0aZjNOrayFu3MhCS 2017 +6 September 22, 2017 Art Pop Moses Sumney - Aromanticism Moses Sumney Aromanticism 30WjNaR79shSTGB52IJTw0 2017 +5 June 16, 2017 Art Pop Lorde - Melodrama Lorde Melodrama 2B87zXm9bOWvAJdkJBTpzF 2017 +4 October 6, 2017 Alternative R&B Kelela - Take Me Apart Kelela Take Me Apart 6pw1XPub1bSMq03ASVqRVu 2017 +3 October 13, 2017 Art Rock King Krule - The OOZ King Krule The OOZ 6Ulu31dfRTpIAud08ZIhXd 2017 +2 June 9, 2017 Alternative R&B SZA - Ctrl SZA Ctrl 76290XdXVF9rPzGdNRWdCh 2017 +1 April 14, 2017 Hip Hop Kendrick Lamar - DAMN. Kendrick Lamar DAMN. 4eLPsYPBmXABThSJ821sqY 2017 +50 February 23, 2018 Hip Hop SOB X RBE - Gangin SOB X RBE Gangin 2PdBVnMc2vEEQU7J3CW6X9 2018.htm +49 October 26, 2018 Neo-Soul Georgia Anne Muldrow - Overload Georgia Anne Muldrow Overload 0B7fXvfjRwx643vbLB8YMz 2018.htm +48 June 22, 2018 Jazz Fusion Kamasi Washington - Heaven and Earth Kamasi Washington Heaven and Earth 2aBgwU4zIm1tekGzphKYp8 2018.htm +47 May 15, 2018 Ambient Techno Skee Mask - Compro Skee Mask Compro 3yXIkSJWpudtgF0TZuB16U 2018.htm +46 August 24, 2018 Alternative R&B Blood Orange - Negro Swan Blood Orange Negro Swan 7bvmGyFDwpHNRRRZJ0AHvn 2018.htm +45 October 5, 2018 Singer-Songwriter Cat Power - Wanderer Cat Power Wanderer 28SMXZ4p2uQGJZJpFXw8em 2018.htm +44 March 2, 2018 Indie Rock Camp Cope - How to Socialise & Make Friends Camp Cope How to Socialise & Make Friends 0GXUti86hj1UiPJmQrbFy6 2018.htm +43 April 20, 2018 Stoner Metal Sleep - The Sciences Sleep The Sciences 790MeSzafcgXNCoY2AagnP 2018.htm +42 October 5, 2018 Indie Folk Adrianne Lenker - abysskiss Adrianne Lenker abysskiss 5W6TxvIqACEfnxm0VgPhv7 2018.htm +41 October 26, 2018 R&B Jeremih & Ty Dolla $ign - Mih-Ty Jeremih & Ty Dolla $ign Mih-Ty 16rRmI5wxWWO5dnRWUKCPA 2018.htm +40 May 4, 2018 Post-Punk Iceage - Beyondless Iceage Beyondless 5H8wFFblf7YvGc7LbBzuR9 2018.htm +39 July 20, 2018 Neo-Soul The Internet - Hive Mind The Internet Hive Mind 27ThgFMUAx3MXLQ297DzWF 2018.htm +38 April 6, 2018 Neo-Soul Kali Uchis - Isolation Kali Uchis Isolation 4EPQtdq6vvwxuYeQTrwDVY 2018.htm +37 January 19, 2018 Experimental Hip Hop JPEGMAFIA - Veteran JPEGMAFIA Veteran 22LKdgY3vLsAsWrOafwCM3 2018.htm +36 June 15, 2018 Trap Rap Rico Nasty - Nasty Rico Nasty Nasty 4RKiTVGT9pCVRnqIkwKWo1 2018.htm +35 February 17, 2018 Deep House A.A.L. (Against All Logic) - 2012 - 2017 A.A.L. (Against All Logic) 2012 1uzfGk9vxMXfaZ2avqwxod 2018.htm +34 October 19, 2018 Art Pop Neneh Cherry - Broken Politics Neneh Cherry Broken Politics 0FVXJcJqnDaXMMUezuyszp 2018.htm +33 October 5, 2018 Trap Rap Sheck Wes - MUDBOY Sheck Wes MUDBOY 15Id9Jrqab8IwHFirdrrLp 2018.htm +32 June 29, 2018 Art Pop Let's Eat Grandma - I'm All Ears Let's Eat Grandma I'm All Ears 5Bnhkya5cGltQFTrnC0grx 2018.htm +31 May 11, 2018 Psychedelic Pop Arctic Monkeys - Tranquility Base Hotel & Casino Arctic Monkeys Tranquility Base Hotel & Casino 1jeMiSeSnNS0Oys375qegp 2018.htm +30 September 21, 2018 Synthpop Christine and the Queens - Chris Christine and the Queens Chris 08LcAgUEeFV4tM3WPPpbYh 2018.htm +29 October 26, 2018 Indie Folk boygenius - boygenius EP boygenius boygenius EP 5BRORKnC2HD5xhgUyR31SH 2018.htm +28 July 13, 2018 Blackgaze Deafheaven - Ordinary Corrupt Human Love Deafheaven Ordinary Corrupt Human Love 2iA7rzpQsOfAPkfH4Ekp7f 2018.htm +27 March 2, 2018 Indie Pop Soccer Mommy - Clean Soccer Mommy Clean 36NLDBi2kX7XRHnyLzLOS8 2018.htm +26 November 2, 2018 Hip Hop Vince Staples - FM! Vince Staples FM! 1HB4naIXCKcUtvBK4Gmcke 2018.htm +25 May 11, 2018 Trap Rap Playboi Carti - Die Lit Playboi Carti Die Lit 7dAm8ShwJLFm9SaJ6Yc58O 2018.htm +24 October 26, 2018 Progressive Pop Julia Holter - Aviary Julia Holter Aviary 6icpwcJQWK4nq9Xilk4yRu 2018.htm +23 August 3, 2018 Trap Rap Travis Scott - ASTROWORLD Travis Scott ASTROWORLD 41GuZcammIkupMPKH2OJ6I 2018.htm +22 February 16, 2018 Neo-Psychedelia U.S. Girls - In a Poem Unlimited U.S. Girls In a Poem Unlimited 5mcuyVRQmrRlfFqDDfJI1q 2018.htm +21 November 30, 2018 Art Pop The 1975 - A Brief Inquiry Into Online Relationships The 1975 A Brief Inquiry Into Online Relationships 6PWXKiakqhI17mTYM4y6oY 2018.htm +20 May 11, 2018 Dream Pop Beach House - 7 Beach House 7 4qftBBO7pnYlek3mRENIvM 2018.htm +19 April 6, 2018 Trap Rap Cardi B - Invasion of Privacy Cardi B Invasion of Privacy 4KdtEKjY3Gi0mKiSdy96ML 2018.htm +18 June 15, 2018 Post-Industrial SOPHIE - OIL OF EVERY PEARL'S UN-INSIDES SOPHIE OIL OF EVERY PEARL'S UN-INSIDES 0CKiI5WdJ5rzUIK8hyD3SY 2018.htm +17 May 25, 2018 Hip Hop Pusha T - Daytona Pusha T Daytona 07bIdDDe3I3hhWpxU6tuBp 2018.htm +16 May 4, 2018 Microhouse Jon Hopkins - Singularity Jon Hopkins Singularity 1nvzBC1M3dlCMIxfUCBhlO 2018.htm +15 September 14, 2018 Hip Hop Noname - Room 25 Noname Room 25 7oHM3Sj0l2nXAzGAxW0KOt 2018.htm +14 March 30, 2018 Neo-Psychedelia Amen Dunes - Freedom Amen Dunes Freedom 2H4G8AGta9p8yjgjVI9nZd 2018.htm +13 August 10, 2018 Art Pop Tirzah - Devotion Tirzah Devotion 15GocbF7ybkkPP03YXtLqv 2018.htm +12 January 5, 2018 Trap Rap cupcakKe - Ephorize cupcakKe Ephorize 5vnAsJiYwz1Cb3lydVGN9W 2018.htm +11 August 17, 2018 R&B Ariana Grande - Sweetener Ariana Grande Sweetener 3tx8gQqWbGwqIGZHqDNrGe 2018.htm +10 September 5, 2018 Post-Industrial Yves Tumor - Safe in the Hands of Love Yves Tumor Safe in the Hands of Love 1IpYZkYoYCjXTYMDEW8Ksk 2018.htm +9 May 30, 2018 Pop Rap Tierra Whack - Whack World Tierra Whack Whack World 3ogNAkUhvQy0cFOfLoR6Y8 2018.htm +8 September 14, 2018 Ambient Pop Low - Double Negative Low Double Negative 1zTkgOHx3mjrUvrhxq4osf 2018.htm +7 November 30, 2018 Experimental Hip Hop Earl Sweatshirt - Some Rap Songs Earl Sweatshirt Some Rap Songs 66at85wgO2pu5CccvqUF6i 2018.htm +6 November 2, 2018 Art Pop Rosalía - El Mal Querer Rosalía El Mal Querer 355bjCHzRJztCzaG5Za4gq 2018.htm +5 June 8, 2018 Indie Rock Snail Mail - Lush Snail Mail Lush 2e48GqjEwCi87gQJanb1bf 2018.htm +4 October 26, 2018 Electropop Robyn - Honey Robyn Honey 6WZjFvrzwq8SOGe0r8R3qk 2018.htm +3 May 4, 2018 Deep House DJ Koze - Knock Knock DJ Koze Knock Knock 0sT4nyNxsvGNQr1O8OR83O 2018.htm +2 March 30, 2018 Country Pop Kacey Musgraves - Golden Hour Kacey Musgraves Golden Hour 7f6xPqyaolTiziKf5R5Z0c 2018.htm +1 August 17, 2018 Indie Rock Mitski - Be the Cowboy Mitski Be the Cowboy 653wRjqO0GOZPQPcXpeAXD 2018.htm +50 October 18, 2019 IDM Floating Points - Crush Floating Points Crush 1WwZwdTICfaZI51BIIEN9z 2019 +49 May 24, 2019 Alt-Country Faye Webster - Atlanta Millionaires Club Faye Webster Atlanta Millionaires Club 4vt0V1SmkaK1Y440P5Nsb4 2019 +48 October 4, 2019 Abstract Hip Hop Danny Brown - uknowhatimsayin¿ Danny Brown uknowhatimsayin¿ 4G3BRVsGEpWzUdplFJ1VBl 2019 +47 September 6, 2019 Minimal Techno Barker - Utility Barker Utility 5F3YJdIjGHhnUVuD96G1mz 2019 +46 February 13, 2019 Dance Punk CHAI - PUNK CHAI PUNK 7tcbI0Qp64LaGwHYgwkbBO 2019 +45 March 1, 2019 Trap Rap DaBaby - Baby On Baby DaBaby Baby On Baby 0O1PJ0t69iTO5yWrIeIga0 2019 +44 May 10, 2019 Glitch Pop Holly Herndon - PROTO Holly Herndon PROTO 3PkYFFSJTPxOhnSYBtyZsk 2019 +43 April 25, 2019 Trap Rap Rico Nasty & Kenny Beats - Anger Management Rico Nasty & Kenny Beats Anger Management 5JbeU5WL1WAGxy1u5fsOmf 2019 +42 May 31, 2019 Bubblegum Bass 100 gecs - 1000 gecs 100 gecs 1000 gecs 4CGanXs6KlVuXXdNrf82qE 2019 +41 November 22, 2019 Death Metal Blood Incantation - Hidden History of the Human Race Blood Incantation Hidden History of the Human Race 34U0n1oAE5mwgdaIBrcIck 2019 +40 June 27, 2019 Glitch Pop Thom Yorke - ANIMA Thom Yorke ANIMA 1g4vEVvVVFvFju0gS0DMbh 2019 +39 April 26, 2019 Singer-Songwriter Aldous Harding - Designer Aldous Harding Designer 0QNJa03XQeMOuQhi9izThh 2019 +38 February 28, 2019 Ambient Techno RAP - Export RAP Export 2019 +37 March 22, 2019 Indie Pop Nilüfer Yanya - Miss Universe Nilüfer Yanya Miss Universe 4ghm3TST2NwYOIAd5QHjbU 2019 +36 May 31, 2019 Trap Rap Denzel Curry - ZUU Denzel Curry ZUU 6PkSBdx19zarn4ae1D08gA 2019 +35 June 21, 2019 Punk Rock Mannequin Pussy - Patience Mannequin Pussy Patience 1X0Na8DRV5U6G9grTPDWKF 2019 +34 May 17, 2019 Hip Hop slowthai - Nothing Great About Britain slowthai Nothing Great About Britain 2wl8jFb7m1paRmxIbwtHru 2019 +33 June 7, 2019 Trap Rap Polo G - Die a Legend Polo G Die a Legend 26ztFK3E69j5THJQdyxC5w 2019 +32 January 18, 2019 Indie Pop Sharon Van Etten - Remind Me Tomorrow Sharon Van Etten Remind Me Tomorrow 2dvXk4nacVRmDSnbKniwrS 2019 +31 June 21, 2019 Abstract Hip Hop MIKE - tears of joy MIKE tears of joy 0LTQhn97NYWef8DeDAsknh 2019 +30 August 16, 2019 Emo Oso Oso - Basking in the Glow Oso Oso Basking in the Glow 0W3zHdIMXAfAAueEWaagRH 2019 +29 February 8, 2019 R&B Ariana Grande - thank u, next Ariana Grande thank u, next 2fYhqwDWXjbpjaIJPEfKFw 2019 +28 October 4, 2019 Singer-Songwriter Nick Cave & The Bad Seeds - Ghosteen Nick Cave & The Bad Seeds Ghosteen 6UOvMBrdfOWGqSvtQohiso 2019 +27 July 26, 2019 Indie Folk Florist - Emily Alone Florist Emily Alone 2Lgoj4yzpi5YchgiVuZTcH 2019 +26 July 25, 2019 Afrobeats Burna Boy - African Giant Burna Boy African Giant 34vlTd4355ddD4q9pPsoqF 2019 +25 May 3, 2019 Indie Pop Vampire Weekend - Father of the Bride Vampire Weekend Father of the Bride 1A3nVEWRJ8yvlPzawHI1pQ 2019 +24 October 11, 2019 Noise Rock Kim Gordon - No Home Record Kim Gordon No Home Record 4WgO7FEa9fzcyOIabUIbQR 2019 +23 May 17, 2019 Neo-Soul Tyler, the Creator - IGOR Tyler, the Creator IGOR 5zi7WsKlIiUXv09tbGLKsE 2019 +22 May 24, 2019 Art Pop Cate Le Bon - Reward Cate Le Bon Reward 1et95HstXof7uDcKgCkwXw 2019 +21 March 29, 2019 Electropop Billie Eilish - WHEN WE ALL FALL ASLEEP, WHERE DO WE GO? Billie Eilish WHEN WE ALL FALL ASLEEP, WHERE DO WE GO? 0S0KGZnfBGSIssfF54WSJh 2019 +20 September 13, 2019 Art Pop Jenny Hval - The Practice of Love Jenny Hval The Practice of Love 6Ia2sw3y79k40GHeNjCfLh 2019 +19 February 8, 2019 Folk Jessica Pratt - Quiet Signs Jessica Pratt Quiet Signs 00oz3t7cI3WfwS2oEIZD6x 2019 +18 August 2, 2019 Indie Pop Clairo - Immunity Clairo Immunity 4kkVGtCqE2NiAKosri9Rnd 2019 +17 September 13, 2019 Indie Folk (Sandy) Alex G - House of Sugar (Sandy) Alex G House of Sugar 2kCDZ3gCr5hXFgbFsPMcxP 2019 +16 August 9, 2019 Art Pop Bon Iver - i,i Bon Iver i,i 54DU59anGQsdrFP7utpshG 2019 +15 June 14, 2019 Singer-Songwriter Bill Callahan - Shepherd in a Sheepskin Vest Bill Callahan Shepherd in a Sheepskin Vest 7sfkWJ14gZywjyv3wtQ5WC 2019 +14 April 17, 2019 R&B Beyoncé - HOMECOMING: THE LIVE ALBUM Beyoncé HOMECOMING: THE LIVE ALBUM 35S1JCj5paIfElT2GODl6x 2019 +13 October 11, 2019 Indie Folk Big Thief - Two Hands Big Thief Two Hands 7pg8T6pajjHVZbiyB8bGxo 2019 +12 September 20, 2019 Psychedelic Soul Brittany Howard - Jaime Brittany Howard Jaime 6fbphjr9j57oxMB2bnhzUf 2019 +11 May 10, 2019 Alternative R&B Jamila Woods - LEGACY! LEGACY! Jamila Woods LEGACY! LEGACY! 5NzK7S7oQQnO8eLRf7kDJx 2019 +10 July 12, 2019 Indie Rock Purple Mountains - Purple Mountains Purple Mountains Purple Mountains 5NCdiiTgky5PbjmCtcgwtn 2019 +9 April 5, 2019 Art Pop Weyes Blood - Titanic Rising Weyes Blood Titanic Rising 53VKICyqCf91sVkTdFrzKX 2019 +8 March 29, 2019 Ambient Fennesz - Agora Fennesz Agora 7JpOsq1F2A9aPr2fdacsOk 2019 +7 March 8, 2019 Art Pop Helado Negro - This Is How You Smile Helado Negro This Is How You Smile 17LsQV3q3cgTBrat3D5JSv 2019 +6 December 24, 2018 Trap Rap Bad Bunny - X 100PRE Bad Bunny X 100PRE 7CjJb2mikwAWA1V6kewFBF 2019 +5 March 1, 2019 Neo-Soul Solange - When I Get Home Solange When I Get Home 4WF4HvVT7VjGnVjxjoCR6w 2019 +4 October 4, 2019 Art Pop Angel Olsen - All Mirrors Angel Olsen All Mirrors 0RedX0LZkGUFoRwFntAaI0 2019 +3 May 3, 2019 Indie Folk Big Thief - U.F.O.F. Big Thief U.F.O.F. 13arYJWgDb5xGDHU49Nlj9 2019 +2 November 8, 2019 Art Pop FKA twigs - MAGDALENE FKA twigs MAGDALENE 2w8Wshbp9RCPJdPU1iOpaY 2019 +1 August 30, 2019 Art Pop Lana Del Rey - Norman Fucking Rockwell! Lana Del Rey Norman Fucking Rockwell! 5XpEKORZ4y6OrCZSKsi46A 2019 \ No newline at end of file diff --git a/source/pitchfork/script.js b/source/pitchfork/script.js new file mode 100644 index 00000000..0c399dd4 --- /dev/null +++ b/source/pitchfork/script.js @@ -0,0 +1,255 @@ +console.clear() +var ttSel = d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + + +var listURLs = `https://pitchfork.com/features/lists-and-guides/7893-the-top-50-albums-of-2010/ +https://pitchfork.com/features/lists-and-guides/8727-the-top-50-albums-of-2011/ +https://pitchfork.com/features/lists-and-guides/9017-the-top-50-albums-of-2012/ +https://pitchfork.com/features/lists-and-guides/9293-the-top-50-albums-of-2013/ +https://pitchfork.com/features/lists-and-guides/9558-the-50-best-albums-of-2014/ +https://pitchfork.com/features/lists-and-guides/9764-the-50-best-albums-of-2015/ +https://pitchfork.com/features/lists-and-guides/9980-the-50-best-albums-of-2016/ +https://pitchfork.com/features/lists-and-guides/the-50-best-albums-of-2017/ +https://pitchfork.com/features/lists-and-guides/the-50-best-albums-of-2018/ +https://pitchfork.com/features/lists-and-guides/best-albums-2019/` + .split('\n') + +var h1Html = d3.select('h1').text() + .replace('Year', `Year`) + .replace('Decade', `Decade`) +d3.select('h1') + .html(h1Html) + +d3.loadData('albums.tsv', (err, res) => { + albums = res[0] + + albums.forEach(d => { + d.releaseYear = +d.date.split(',')[1] + d.rank = +d.rank + d.year = d.year.replace('.htm', '') + + if (d.artist.includes('Ariel Pink')) d.artist = 'Ariel Pink' + }) + + byDup = d3.nestBy(albums, d => d.year + d.rank).filter(d => d.length > 1) + byDup.forEach(dup => { + dup.forEach((d, i) => d.dupRemove = i) + }) + + albums = albums.filter(d => !d.dupRemove) + + + bySlug = d3.nestBy( + albums.filter(d => d.year != '2010-2014'), + d => d.slug + ) + + bySlug.forEach(d => { + d.decadeRank = 999 + d.yearRank = 999 + + d.year = d[0].releaseYear + d.artist = d[0].artist + d.album = d[0].album + d.genre = d[0].genre + d.date = d[0].date + // d.slug = d[0].slug + delete d.key + + + d.forEach(e => { + if (e.year == '2010-2019'){ + d.decadeRank = e.rank + } else { + d.yearRank = e.rank + d.year = +e.year + } + }) + + if (d[0].slug == 'Lady Gaga - The Fame Monster') d.year = 2010 + }) + + bySlug = _.sortBy(bySlug, d => d.yearRank*10000 + d.decadeRank) + .filter(d => d.year > 2009) + + byYear = d3.nestBy(bySlug, d => d.year) + byYear = _.sortBy(byYear, d => d.key) + .filter(d => d.key > 2009) + + // byYear.forEach(year => { + // year.forEach((d, i) => { + // d.yearIndex = i + // }) + // }) + + + byArtist = d3.nestBy(bySlug, d => d.artist) + byArtist.forEach(d => { + d.minDecadeRank = d3.min(d, d => d.decadeRank) + }) + + drawYearGrid() +}) + + +function drawYearGrid(){ + var colWidth = Math.floor((Math.max(980, innerWidth) - 90)/10) + var width = colWidth*10 + var colMarginLeft = -(width - 750)/2 + 40 + + var sel = d3.select('#year-grid').html('') + .st({width, marginLeft: colMarginLeft}) + + var byYearSel = sel.appendMany('div.year-col', byYear) + .st({width: colWidth - 10, display: 'inline-block'}) + + sel.append('div.year-col') + .st({position: 'absolute', left: -25, top: 25}) + .appendMany('div.row.album.index', d3.range(1, 51)) + .text(d => d3.format('02')(d)) + + sel.append('div.year-col') + .st({position: 'absolute', left: -32, top: 1}) + .append('div.year-rank-header') + .html('year
rank') + + sel.append('div.year-col') + .st({position: 'absolute', left: -25, top: 797}) + .append('div.year-rank-header') + .html('unranked') + + byYearSel.append('b') + .text(d => d.key) + .st({cursor: 'pointer', textDecoration: 'xunderline'}) + .on('click', (d, i) => window.open(listURLs[i], '_blank')) + + var rowSel = byYearSel.appendMany('div.row.album', d => d) + .call(d3.attachTooltip) + .on('mouseover', d => { + ttSel.html('') + ttSel.append('div').append('b').text(d.artist) + ttSel.append('i').text(d.album) + // ttSel.append('div').text('' + d.date) + ttSel.append('div').text(' ') + ttSel.append('br') + + var yearRankText = d.yearRank == '999' ? 'Unranked in ' + d.year : '#' + d.yearRank + ' in ' + d.year + var decadeRankText = d.decadeRank == '999' ? 'Unranked in the 2010s' : '#' + d.decadeRank + ' in the 2010s' + + ttSel.append('div').text(yearRankText) + ttSel.append('div').text(decadeRankText) + + ttSel.append('br').parent().append('div').text('Released ' + d.date) + + if (!d[0].spotify){ + ttSel.append('br').parent().append('div').text('not streaming on spotify') + .st({fontFamily: 'monospace', fontSize: 13}) + } + + rowSel + .classed('active', 0) + .filter(e => e.artist == d.artist) + .classed('active', 1) + }) + .on('mouseout', d => rowSel.classed('active', 0)) + .st({background: '#dde', marginBottom: d => d.yearRank == 50 ? 20 : 1 }) + .on('click', d => { + if (!d[0].spotify) return + // console.log(d[0].spotify) + window.open('http://open.spotify.com/album/' + d[0].spotify, '_blank') + // window.open('spotify:show/' + d[0].spotify, '_blank') + }) + .classed('no-spotify', d => !d[0].spotify) + + var bgScale = d3.scaleLinear() + .domain([1, 200]).range([100, 10]) + .domain([1, 20, 200]).range([100, 50, 10]) + .domain([1, 10, 200, 201]).range([100, 50, 10, 0]) + + var bgScale = d3.scalePow() + .domain([1, 200]).range([100, 1]).exponent(.01) + + var bgScale = d3.scaleLog() + .domain([1, 200]).range([100, 1]) + + rowSel.append('div.color') + .st({position: 'absolute'}) + .html(' ') + .st({ + background: '#0ff', + width: d => bgScale(d.decadeRank) + '%', + opacity: d => d.decadeRank == 999 ? 0 : 1, + color: '#000', + }) + + rowSel.append('div.text') + .text(d => d.artist) + .st({ + color: d => d.decadeRank == 999 ? '#888' : '#000', + position: 'absolute' + }) + // .text(d => d.artist) + + // rowSel.filter(d => d.year == 2010) + // .append('div.year-rank') + // .text(d => d.decadeRank) + // .st({left: -20, position: 'absolute', overflow: 'visible'}) + + + + + // add swoops + d3.select('#arrow-container').html('').append('svg') + .st({zIndex: 100000, position: 'relative', pointerEvents: 'none'}) + .appendMany('path.swoop', [ + [[-5, 35], [colMarginLeft - 5 + colWidth/4, -150]], + [[145, 35], [colMarginLeft + colWidth*2 - 15, -176]], + ]) + .attr('marker-end', 'url(#arrowhead)') + .attr('d', (d, i) => swoopyArrow().angle(Math.PI/2).clockwise(1)(d)) + + // add key + + + var keyRowSel = sel.append('div.year-col') + .st({position: 'absolute', left: -32, top: -134, pointerEvents: 'none', zIndex: -1000}) + .append('div.year-rank-header') + .html('decade
rank') + .parent() + // .appendMany('div.row.album.index', [1, 4, 16, 64, 200]) + .appendMany('div.row.album.index', [1, 5, 10, 20, 50, 100]) + // .appendMany('div.row.album.index', [1, 5, 20, 50, 100, 200]) + .st({width: colWidth, position: 'relative', top: 10}) + + keyRowSel.append('div.color') + .st({position: 'absolute'}) + .html(' ') + .st({ + background: '#0ff', + width: d => bgScale(d)*.9 + '%', + color: '#000', + }) + + keyRowSel.append('div.text') + .text(d => d) + .st({color: '#000', position: 'absolute', left: 2}) + + + sel.append('div.year-col') + .st({position: 'absolute', right: 60, bottom: 40, fontSize: 12, pointerEvents: 'none'}) + .html(`
Click to open in Spotify`) + +} + + + + + + + + + + + + + diff --git a/source/pitchfork/style.css b/source/pitchfork/style.css new file mode 100644 index 00000000..92142ad4 --- /dev/null +++ b/source/pitchfork/style.css @@ -0,0 +1,184 @@ +body{ + /*font-family: menlo, Consolas, 'Lucida Console', monospace; */ + /*margin: 0px;*/ +} +html{ + /*background: #fff;*/ + background: #fff; + min-width: 980px; +} + +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; + width: 220px; + z-index: 1000000; + line-height: 1.3em; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.domain{ + display: none; +} + +text{ + /*pointer-events: none;*/ + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; +} + + +.year-col{ + overflow-x: visible !important; + vertical-align: top; + margin-right: 4px; + font-family: menlo, Consolas, 'Lucida Console', monospace; +} +.year-col b{ + text-align: center; + display: inline-block; + width: 100%; + background: #ddd; +} + +.year-col .row{ + overflow-y: hidden; + height: 1.2em; + font-size: 12px; + padding: 0px; + margin-bottom: 1px; + position: relative; + cursor: pointer; + line-height: 1.1em; + background: #f5f5f5 !important; + font-weight: 100; +} + +.row.active{ + z-index: 100; + text-decoration: underline; + background: #000 !important; + color: #fff !important; +} +.row.active .text{ + color: #fff !important; +} +.row.active .color{ + background: #3454f5 !important; +} + +.row .text{ + /*overflow-y: hidden;*/ + /*width: 100%;*/ + padding: 1px +} + +.row:hover .text{ + text-decoration: underline; +} + + +.row .color{ + padding: 1px; + /*white-space: nowrap;*/ + /*overflow-y: hidden;*/ +} + + +#year-grid{ + position: relative; + min-height: 800px; + margin-top: 40px; +} + +.year-col .index{ + background: #fff !important; + color: #999; + pointer-events: none; + +} + +.album{ + overflow-x: hidden; +} + +.year-rank-header{ + height: 1.2em; + font-size: 12px; + line-height: .95em; + color: #999; +} + + +h1 span{ + padding-left: 5px; + padding-right: 5px; + margin-left: -5px; + margin-right: -5px; +} + +.h1-year{ + background: #ddd; +} + +.h1-decade{ + background: #0ff; +} + + +.swoop{ + stroke: #888; + stroke-width: .8; + fill: none; +} + +.pointer{ + height: 0px; + position: relative; + top: -14px; + left: 14px; +} +.pointer div { + overflow: visible; + content: ""; + background-image: url(https://pair-code.github.io/interpretability/bert-tree/pointer.svg); + width: 27px; + height: 27px; + position: absolute; + left: -51px; + top:20px; +} + +.year-col b:hover{ + text-decoration: underline; +} + +.no-spotify .text{ + text-decoration: none !important; + cursor: default; + +} + + diff --git a/source/pitchfork/todo.txt b/source/pitchfork/todo.txt new file mode 100644 index 00000000..992ea8bd --- /dev/null +++ b/source/pitchfork/todo.txt @@ -0,0 +1,44 @@ +x highlight title with grey and blue +x blue key (BONUS: make it adjust scaling) + +tooltip + +x date, rank +x clear hover when leaving the chart +x "no spotify warning" + +decade rank + +x add label to column +x add unranked label (maybe a line?) + + +words at bottom of post + +x link to code +x link to lists + + +annotations + +- sun kil moon is the only one in the top 10 of albums of 2010-2014 not to make the decade end list after heckling a pitchfork reviewer on stage +- Fame Monster and Red made the best of the decade, but not the best of the year. +- James Blake and How to Dress Well both had three best of the year albums and nothing in best of the decade; their albums in the later half of the decade that scored 5.8 and 6.8 might have removed some of luster. +- Beyoncé's and Burial's December albums didn't make the end of the year list. + + + + +All of pitchfork's best of the year lists into one table. + +https://roadtolarissa.com/pitchfork/ + + +I played around with trying encode position, with slope charts and scatter plots, but settled on a table for easy scanning and density of text. Cutting off artist names isn't ideal, but couldn't figure out how else to squeeze everything in. + +https://blocks.roadtolarissa.com/1wheel/raw/5ec32afde3419ef4f741bccd7405f53b/index.html + +With more time, I would have collected the original review scores and shown artist albums that didn't make any lists on mouseover to give a better idea of their career arc. + +The log scaling for the decade bars was also a little fiddly. I wanted to make the legend control the scaling and rank the years by summing the bars ; maybe with a linear or a flat scale 2010 would score higher? + diff --git a/source/playoff-probabilities/script.js b/source/playoff-probabilities/script.js new file mode 100644 index 00000000..0a3aaa92 --- /dev/null +++ b/source/playoff-probabilities/script.js @@ -0,0 +1,551 @@ +console.clear() +d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + +// https://teamcolorcodes.com/nba-team-color-codes/ +var abv2color = { + "GS": "#FDB927", + "LAC": "#1D428A", + "HOU": "#CE1141", + "UTA": "#F9A01B", + "POR": "#000000", + "OKC": "#007AC1", + "DEN": "#0E2240", + "SA" : "#C4CED4", + "MIL": "#EEE1C6", + "DET": "#C8102E", + "BOS": "#007A33", + "IND": "#FDBB30", + "PHI": "#002D62", + "BKN": "#000000", + "TOR": "#753BBD", + "ORL": "#0077C0", + "DAL": "#B8C4CA", + "MIA": "#98002E", + "LAL": "#FDB927", +} + +var abv2Index = { + "MIL": 0, + "ORL": 1, + "IND": 2, + "MIA": 3, + "BOS": 4, + "PHI": 5, + "TOR": 6, + "BKN": 7, + "LAL": 8, + "POR": 9, + "HOU": 10, + "OKC": 11, + "DEN": 12, + "UTA": 13, + "LAC": 14, + "DAL": 15 +} + +function saturate(color, k) { + var {l, c, h} = d3.lch(color) + return d3.lch(l, c + 18 * k, h) +} + +function lighten(color){ + return d3.interpolate(color, '#fff')(.15) +} + +var abv2lcolor = {} +d3.entries(abv2color).forEach(d => abv2lcolor[d.key] = lighten(d.value)) + +var isMobile = innerWidth < 450 +var white = '#fff' //'#f5f5f5' + +d3.loadData( + 'https://roadtolarissa.com/data/538-2020-nba-forecasts.json', + 'https://roadtolarissa.com/data/538-2020-nba-games.json', (err, res) => { + forecasts = _.sortBy(res[0], d => d.last_updated) + + forecasts.forEach(d => { + d.date = d.last_updated.split('T')[0].split('-').slice(1).join('/') + + d.playoff_losses = d3.sum(d.types.carmelo, d => d.playoff_losses) + }) + + forecasts = forecasts + .filter(d => d.playoff_losses > 0) + .filter(d => d.last_updated > '2020-08-15') + + forecasts = d3.nestBy(forecasts, d => d.last_updated).map(_.last) + + snapshots = [] + forecasts.forEach((forecast, index) => { + forecast.index = index + + var teams = forecast.types.carmelo + .map(d => { + var conf = d.conference[0] + var seed = 0 + d3.range(9).forEach(i => seed = d['seed_' + i] ? i : seed) + + var abv = d.name + + var gamesPlayed = 'q s c f'.split(' ') + .map(str => +d[str + '_wins'] + d[str + '_losses']) + + var gamesWon = 'q s c f'.split(' ') + .map(str => Math.max(+d[str + '_wins'], d[str + '_losses'])) + + var team = {conf, seed, abv, gamesPlayed, gamesWon} + + team.levels = [ + d.make_conf_semis, + d.make_conf_finals, + d.make_finals, + d.win_finals, + ].map((prob, level) => + ({prob, level, team, forecast, gamesPlayed: gamesPlayed[level], gamesWon: gamesWon[level]})) + + return team + }) + .filter(d => d.seed) + + var order = [1, 8, 4, 5, 3, 6, 2, 7] + teams = _.sortBy(teams, d => order.indexOf(d.seed) + (d.conf == 'W' ? 8 : 0)) + + teams.forEach((d, i) => d.index = i) + window.teamAbv2Index = {} + teams.forEach(d => teamAbv2Index[d.abv] = d.index) + + d3.range(4).forEach(levelIndex => { + var prev = 0 + var mult = Math.pow(2, levelIndex + 1)/16 + + teams.forEach(team => { + var l = team.levels[levelIndex] + + l.val = l.prob*mult + l.prev = prev + prev += l.val + + snapshots.push({ + abv: team.abv, + val: l.val, + prev: l.prev, + mult, + levelIndex, + l, + }) + }) + }) + + forecast.teams = teams + }) + + uniqSnaps = d3.nestBy(snapshots, d => [d.abv, d.l.level]) + uniqSnaps.forEach(teamLevel => { + teamLevel.games = d3.range(7).map(gameIndex => { + var prevLevel = teamLevel[0] + + return teamLevel.map(curLevel => { + var isPlayed = curLevel.l.gamesPlayed <= gameIndex + prevLevel = isPlayed ? curLevel : prevLevel + + var isFluid = isPlayed && curLevel.l.gamesWon < 4 + + return {tl: prevLevel, gameIndex, isPlayed, isFluid} + }) + }) + }) + flatSnapGames = _.flatten(uniqSnaps.map(d => d.games)) + + flatSnapGames.forEach(d => { + d.abv = d[0].tl.abv + d.gameIndex = d[0].gameIndex + d.levelIndex = d[0].tl.levelIndex + }) + flatSnapGames.abvGameLevelLookup = Object.fromEntries(flatSnapGames.map(d => [[d.abv, d.gameIndex, d.levelIndex], d])) + + games = res[1].filter(d => d.playoff).filter(d => d.status == 'post').filter(d => d.playoff != 'p') + games.forEach(d => { + d.teams = _.sortBy([d.team1, d.team2], d => abv2Index[d]) + }) + d3.nestBy(games, d => d.teams).forEach(series => { + series.forEach((game, gameIndex) => { + game.levelIndex = 'q s c f'.split(' ').indexOf(game.playoff) + + game.gameIndex = gameIndex + var isTeam1 = game.teams[0] == game.team1 + var isTeam1Winner = game.score1 > game.score2 + + game.isTopWin = isTeam1 != isTeam1Winner + game.char = game.isTopWin ? '↑' : '↓' + // game.char = isTeam1 == isTeam1Winner ? '▲' : '▾' + // game.char = isTeam1 == isTeam1Winner ? '▲' : '▼' + + var key = [game.teams[0], gameIndex, game.levelIndex] + var m = flatSnapGames.abvGameLevelLookup[key] + if (!m) return console.log(key) + m.game = game + }) + + series.teams = series[0].teams + var key = [series[0].teams[0], 6, series[0].levelIndex] + var m = flatSnapGames.abvGameLevelLookup[key] + if (!m) return console.log(key) + m.series = series + }) + + var renders = [initRects(), initTimeline()] + initFinalsWP() + + window.curForecast = null + + window.startPlayback = () => { + var stepDuration = 300 + window.renderForecast = function(forecast, dur=stepDuration){ + window.curForecast = forecast + renders.forEach(d => d(forecast, dur)) + } + + function step(){ + var forecast = forecasts[window.curForecast ? curForecast.index + 1 : 0] + if (!forecast){ + stepInterval.end() + forecast = curForecast + } + renderForecast(forecast, stepDuration) + } + + if (window.stepInterval) window.stepInterval.end() + window.stepInterval = d3.interval(step, stepDuration) + stepInterval.isPlaying = true + stepInterval.end = () => { + stepInterval.stop() + stepInterval.isPlaying = false + } + step() + } + startPlayback() +}) + +function initRects(){ + var sel = d3.select('#graph').html('') + var c = d3.conventions({sel, margin: {left: 100, right: 0, bottom: 40, top: 50}, height: 16*32, layers: 'sd'}) + + c.y.domain([0, 1]).range([0, c.height]) + c.x.domain([0, 4]) + + updateFlatSnapGames(forecasts[0]) + + var rectSel = c.svg.appendMany('rect', flatSnapGames) + .at({ + x: d => c.x(d.tl.l.level + d.gameIndex/7), + width: c.x(1)/7 + 1, + y: d => c.y(d.tl.prev), + height: d => c.y(d.tl.val), + fill: d => abv2color[d.tl.l.team.abv], + opacity: 1, + }) + .call(d3.attachTooltip) + + + updateFlatSnapGames(_.last(forecasts)) + + var vSel = c.svg.appendMany('text.v', flatSnapGames.filter(d => d.game)) + .at({ + x: d => c.x(d.tl.l.level + (d.gameIndex + .5)/7), + y: d => c.y(d.tl.prev + d.tl.mult/2) + (d.game.isTopWin ? -8 : 8), + dy: '.33em', + textAnchor: 'middle', + }) + .text(d => d.game.char) + + var seriesSel = addLabels() + + updateFlatSnapGames(forecasts[0]) + + + function addLabels(){ + c.svg.appendMany('g', forecasts[0].teams) + .translate(d => [-c.margin.left, c.y(d.index/16) + 1]) + .append('rect') + .at({width: c.margin.left, fill: d => abv2color[d.abv]}) + .at({y: d => d.index == 0 ? -1 : 0, height: d => c.y(1/16) + (d.index == 0 ? 0 : d.index ==15 ? -.5 : -1)}) + .parent().append('text.team-abv') + .text(d => d.abv) + .translate([c.margin.left/2, c.y(.5/16)]) + .st({pointEvents: 'none'}) + .at({textAnchor: 'middle', dy: '.33em'}) + + c.svg.appendMany('line', d3.range(4)) + .translate(d => Math.round(c.x(d)) - .5, 0) + .at({ + y2: c.height, + stroke: white, + strokeWidth: 3, + }) + + var seriesSel = c.svg.appendMany('path.series', flatSnapGames.filter(d => d.series)) + .translate(d => c.x(d.tl.l.level + 1) -1.5, 0) + .at({ + stroke: d => abv2color[d.tl.val > 0 ? d.series.teams[0] : d.series.teams[1]], + strokeWidth: 1, + // strokeDasharray: '2 1', + d: d => ['M 0', c.y(d.tl.prev), 'V' + c.y(d.tl.prev + d.tl.mult)].join(' ') + }) + + c.svg.appendMany('line', d3.range(1, 8)) + .translate(d => Math.round(c.y(d/8)) + .5, 1) + .at({ + x1: d => { + var v = 0 + while (d % Math.pow(2, v) == 0) v++ + return c.x(v) + }, + stroke: white, + strokeWidth: 1 + }) + + c.svg.appendMany('text', isMobile ? ['RND 1', 'RND 2', 'RND 3', 'FINALS'] : ['ROUND 1', 'CONF SEMIS', 'CONF FINALS', 'FINALS']) + .translate((d,i) => [c.x(i + .5), -28]) + .text(d => d) + .at({ + textAnchor: 'middle', + dy: -2, + fontSize: isMobile ? 14 : '', + }) + + c.layers[1].append('div') + .translate([-c.margin.left -2, -25]) + .html('538 Advance Pct Before Game #') + .st({fontSize: 10, width: c.margin.left, textAlign: 'right'}) + + c.svg.appendMany('text', d3.range(28)) + .translate(d => [c.x(d/7 + 1/14), isMobile ? -6 : -5]) + .text(d => d % 7 + 1) + .st({fontSize: isMobile ? 9 : 10, textAnchor: 'middle'}) + + return seriesSel + } + + return (forecast, dur) => { + updateFlatSnapGames(forecast) + + rectSel + .at({ + fill: d => (d.isFluid ? abv2lcolor : abv2color)[d.tl.l.team.abv], + // fill: d => (abv2color)[d.tl.l.team.abv], + // opacity: d => d.isFluid ? .7 : 1, + }) + .transition().duration(dur) + .at({ + y: d => c.y(d.tl.prev), + height: d => c.y(d.tl.val), + }) + + vSel + // .st({opacity: d => d.tl.isPlayed}) + .st({opacity: d => d.isPlayed ? 0 : 1}) + + seriesSel + .st({opacity: d => d.isFluid ? 0 : 1}) + } + + function updateFlatSnapGames(forecast){ + flatSnapGames.forEach(d => { + d.isFluid = d[forecast.index].isFluid + d.isPlayed = d[forecast.index].isPlayed + d.tl = d[forecast.index].tl + d.gameIndex = d[0].gameIndex + }) + } + +} + + +function initTimeline(){ + var c = d3.conventions({ + sel: d3.select('#timeline').html('').append('div'), + height: 50, + margin: {left: 50, top: 0}, + layers: 'sd', + }) + + c.x.domain([0, forecasts.length - 1]).clamp(1) + + timelineSel = c.svg.append('g') + .st({cursor: 'pointer'}) + .on('click', function(){ + window.stepInterval.end() + renderForecast(forecasts[Math.round(c.x.invert(d3.mouse(this)[0]))]) + }) + .on('mousemove', function(d){ + window.stepInterval.end() + renderForecast(forecasts[Math.round(c.x.invert(d3.mouse(this)[0]))], 0) + }) + + + timelineSel.append('rect') + .at({width: c.width + 20, x: -10, height: 60, y: -20, opacity: 0}) + + + var space = c.x(1) + var dateSel = timelineSel.appendMany('text.date', forecasts) + .translate((d, i) => c.x(i), 0) + .text(d => d.date) + .at({fontSize: 12, y: 30, textAnchor: 'middle', + opacity: (d, i) => i % (space < 20 ? 6 : space < 30 ? 3 : space < 45 ? 2 : 1) == 0 ? 1 : 0}) + + var rh = 5 + var r = rh + (space < 30 ? -1 : 2) + + timelineSel.append('path') + .at({d: `M ${c.x(0)} 0 H ${c.x(forecasts.length - 1)}`, stroke: '#000', strokeWidth: rh}) + timelineSel.appendMany('circle', forecasts) + .translate((d, i) => c.x(i), 0) + .at({r, fill: '#fff', stroke: '#000'}) + timelineSel.append('path') + .at({d: `M 0 0 H ${c.width}`, stroke: '#fff', strokeWidth: rh - 2}) + + var maskSel = timelineSel.append('mask#timeline-mask') + maskSel.appendMany('circle', forecasts) + .translate((d, i) => c.x(i), 0) + .at({r, fill: '#fff', stroke: '#000'}) + maskSel.append('path') + .at({d: `M 0 0 H ${c.width}`, stroke: '#fff', strokeWidth: rh - 2}) + + var fillSel = timelineSel.append('rect') + .at({y: -r, height: r*2, x: -r, fill: '#000', mask: 'url(#timeline-mask)'}) + + var buttonSel = c.layers[1] + .st({width: 20}) + .append('div') + .translate([-c.margin.left + 10, -10]) + .append('div') + .st({fontSize: 40, verticalAlign: 'middle', display: 'inline-block', cursor: 'pointer'}) + .text('▶') + .on('click', () => { + if (stepInterval.isPlaying){ + window.stepInterval.end() + renderForecast(curForecast) + } else { + if (curForecast == _.last(forecasts)) curForecast = null + startPlayback() + } + }) + + return (forecast, dur) => { + fillSel + .transition().duration(dur) + .at({width: c.x(forecast.index) + r*2}) + + setTimeout(() => { + dateSel + .classed('active', 0) + .filter(d => d == forecast) + .classed('active', 1) + .raise() + }, dur/2) + + if (window.stepInterval?.isPlaying){ + buttonSel + .text('=') + .st({transform: 'rotate(90deg) translate(3px, 5px)'}) + } else { + buttonSel + .text('▶') + .st({transform: 'translate(-2px, 0px)'}) + } + } +} + +function initFinalsWP(){ + var c = d3.conventions({ + sel: d3.select('#finals-wp').html('').append('div'), + height: 160, + margin: {left: 50, top: 30, bottom: 45}, + layers: 's', + }) + + c.x.domain([0, forecasts.length - 1]).interpolate(d3.interpolateRound) + c.y.domain([0, 1]).range([0, c.height]) + + var space = c.x(1) + + var dateSel = c.svg.appendMany('text.date', forecasts) + .translate((d, i) => [c.x(i), c.height]) + .text(d => d.date) + .at({fontSize: 12, y: 15, textAnchor: 'middle', + opacity: (d, i) => i % (space < 20 ? 6 : space < 30 ? 3 : space < 45 ? 2 : 1) == 0 ? 1 : 0}) + + forecasts.forEach((forecast) => { + forecast.teams.forEach(d => d.forecastIndex = forecast.index) + }) + + var forecastSel = c.svg.appendMany('g', forecasts) + .translate(d => c.x(d.index), 0) + .on('mouseover', function(d){ + window.stepInterval.end() + renderForecast(forecasts[d.index], 0) + }) + + forecastSel + .appendMany('rect', d => d.teams.filter(d => d.levels[3].val)) + .at({ + width: space + 1, + x: -space/2, + y: d => c.y(d.levels[3].prev), + height: d => c.y(d.levels[3].val), + fill: d => abv2color[d.abv], + }) + + forecastSel.append('path') + .translate(-space/2, 0) + .at({stroke: '#fff', d: 'M 0 0 V ' + c.height, strokeWidth: .2}) + + c.svg.appendMany('text', [25, 50, 75]) + .text(d => d + '%') + .at({ + y: d => c.y(1 - d/100), + x: -space/2 - (isMobile || innerWidth > 1280 ? 12 : 1), + dy: '.33em', + fontSize: 10, + textAnchor: 'end', + fill: '#999', + }) + .st({fontSize: 10}) + + c.svg.append('text') + .text('538 Chance of Winning Finals') + .translate([-space/2, -4]) + .st({fontSize: 10, width: c.margin.left, textAlign: 'right'}) +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/playoff-probabilities/style.css b/source/playoff-probabilities/style.css new file mode 100644 index 00000000..a6c77f6c --- /dev/null +++ b/source/playoff-probabilities/style.css @@ -0,0 +1,111 @@ +body{ + /*margin: 0px auto;*/ + /*max-width: 1000px;*/ + overflow-x: hidden; + +} + +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.domain{ + display: none; +} + +text{ + /*pointer-events: none;*/ + /*text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;*/ +} + +text.v{ + text-shadow: 0 1px 0 #000, 1px 0 0 #000, 0 -1px 0 #000, -1px 0 0 #000; + fill: #fff; + font-weight: 200; + font-size: 16px; + /*font-family: monospace;*/ +} + +.team-abv{ + fill: #fff; + text-shadow: 0 1px 0 #000, 1px 0 0 #000, 0 -1px 0 #000, -1px 0 0 #000; +} + +.date{ + font-weight: 300; + fill: #999; + font-size: 10px; + +} +.date.active{ + font-weight: 700; + opacity: 1; + text-shadow: 0 2px 0 #f5f5f5, 2px 0 0 #f5f5f5, 0 -2px 0 #f5f5f5, -2px 0 0 #f5f5f5; + fill: #000; + font-size: 14px; +} + + +#timeline ::selection{ + background: none; +} + + + + +.full-width { + width: 100vw; + position: relative; + left: 50%; + margin-left: -50vw; + box-sizing: border-box; +} + +.full-width > div { + max-width: 1280px; + margin: 0px auto; +} + + + +.full-width{ + line-height: .7em; + font-family: menlo, Consolas, 'Lucida Console', monospace; + +} + + +#notes{ + font-family: sans-serif; + font-size: 12px; + opacity: .7; +} + +#graph{ + height: 600px; +} \ No newline at end of file diff --git a/source/playoff-probabilities/todo b/source/playoff-probabilities/todo new file mode 100644 index 00000000..1da0e646 --- /dev/null +++ b/source/playoff-probabilities/todo @@ -0,0 +1,5 @@ +- text +- share img +- top over time +- inset images +- tooltip \ No newline at end of file diff --git a/source/regression-discontinuity/days.js b/source/regression-discontinuity/days.js new file mode 100644 index 00000000..4498c740 --- /dev/null +++ b/source/regression-discontinuity/days.js @@ -0,0 +1,104 @@ +window.days = d3.csvParse(`DATE,TOTAL_TESTS,POSITIVE_TESTS,PERCENT_POSITIVE,PERCENT_POSITIVE_3DAYS_AGG +03/01/2020,0,0,0, +03/02/2020,0,0,0, +03/03/2020,0,0,0, +03/04/2020,32,5,0.16, +03/05/2020,65,4,0.06,0.09 +03/06/2020,85,8,0.09,0.09 +03/07/2020,66,8,0.12,0.09 +03/08/2020,110,21,0.19,0.14 +03/09/2020,403,60,0.15,0.15 +03/10/2020,466,73,0.16,0.16 +03/11/2020,809,163,0.2,0.18 +03/12/2020,1606,368,0.23,0.21 +03/13/2020,2202,626,0.28,0.25 +03/14/2020,1764,662,0.38,0.3 +03/15/2020,2397,1050,0.44,0.37 +03/16/2020,5061,2184,0.43,0.42 +03/17/2020,5709,2519,0.44,0.44 +03/18/2020,6468,3052,0.47,0.45 +03/19/2020,7923,3783,0.48,0.47 +03/20/2020,8020,4101,0.51,0.49 +03/21/2020,4937,2721,0.55,0.51 +03/22/2020,4638,2694,0.58,0.54 +03/23/2020,5968,3733,0.63,0.59 +03/24/2020,7551,4654,0.62,0.61 +03/25/2020,8012,5063,0.63,0.62 +03/26/2020,7757,5254,0.68,0.64 +03/27/2020,7895,5292,0.67,0.66 +03/28/2020,5128,3649,0.71,0.68 +03/29/2020,5299,3711,0.7,0.69 +03/30/2020,9306,6434,0.69,0.7 +03/31/2020,8565,5729,0.67,0.69 +04/01/2020,9116,5734,0.63,0.66 +04/02/2020,10214,6116,0.6,0.63 +04/03/2020,10400,5987,0.58,0.6 +04/04/2020,6892,4079,0.59,0.59 +04/05/2020,7005,4026,0.57,0.58 +04/06/2020,12096,6797,0.56,0.57 +04/07/2020,11736,6442,0.55,0.56 +04/08/2020,11449,6024,0.53,0.55 +04/09/2020,10325,5454,0.53,0.53 +04/10/2020,8916,4916,0.55,0.53 +04/11/2020,7814,4012,0.51,0.53 +04/12/2020,6797,3238,0.48,0.52 +04/13/2020,8136,3725,0.46,0.48 +04/14/2020,11292,4782,0.42,0.45 +04/15/2020,11601,4366,0.38,0.41 +04/16/2020,11308,4004,0.35,0.38 +04/17/2020,12259,4108,0.34,0.35 +04/18/2020,7784,2550,0.33,0.34 +04/19/2020,8177,2777,0.34,0.33 +04/20/2020,13922,4469,0.32,0.33 +04/21/2020,11977,3696,0.31,0.32 +04/22/2020,13971,4188,0.3,0.31 +04/23/2020,12501,3481,0.28,0.3 +04/24/2020,11956,3202,0.27,0.28 +04/25/2020,7728,1994,0.26,0.27 +04/26/2020,5722,1312,0.23,0.26 +04/27/2020,13151,2967,0.23,0.24 +04/28/2020,15272,3478,0.23,0.23 +04/29/2020,15081,3082,0.2,0.22 +04/30/2020,13854,2665,0.19,0.21 +05/01/2020,13282,2559,0.19,0.2 +05/02/2020,7661,1467,0.19,0.19 +05/03/2020,6200,1111,0.18,0.19 +05/04/2020,14632,2252,0.15,0.17 +05/05/2020,14971,2258,0.15,0.16 +05/06/2020,15328,2122,0.14,0.15 +05/07/2020,14933,1917,0.13,0.14 +05/08/2020,13935,1791,0.13,0.13 +05/09/2020,7249,1041,0.14,0.13 +05/10/2020,6149,750,0.12,0.13 +05/11/2020,18255,2015,0.11,0.12 +05/12/2020,19844,2103,0.11,0.11 +05/13/2020,19975,2080,0.1,0.11 +05/14/2020,19099,1911,0.1,0.1 +05/15/2020,16024,1577,0.1,0.1 +05/16/2020,9033,852,0.09,0.1 +05/17/2020,8092,673,0.08,0.09 +05/18/2020,21603,1582,0.07,0.08 +05/19/2020,21119,1643,0.08,0.08 +05/20/2020,25520,1698,0.07,0.07 +05/21/2020,25444,1657,0.07,0.07 +05/22/2020,26024,1601,0.06,0.06 +05/23/2020,12894,766,0.06,0.06 +05/24/2020,12833,741,0.06,0.06 +05/25/2020,15101,769,0.05,0.06 +05/26/2020,34204,1738,0.05,0.05 +05/27/2020,25431,1282,0.05,0.05 +05/28/2020,26188,1099,0.04,0.05 +05/29/2020,25967,1058,0.04,0.04 +05/30/2020,13331,567,0.04,0.04 +05/31/2020,10119,379,0.04,0.04 +06/01/2020,30731,1044,0.03,0.04 +06/02/2020,28686,910,0.03,0.03 +06/03/2020,24970,824,0.03,0.03 +06/04/2020,24997,784,0.03,0.03 +06/05/2020,21778,547,0.03,0.03 +06/06/2020,11137,328,0.03,0.03 +06/07/2020,8851,289,0.03,0.03 +06/08/2020,24738,653,0.03,0.03 +06/09/2020,20448,501,0.02,0.03 +06/10/2020,13286,362,0.03,0.03 +06/11/2020,10654,277,0.03,0.03`) \ No newline at end of file diff --git a/source/regression-discontinuity/script.js b/source/regression-discontinuity/script.js new file mode 100644 index 00000000..c9083d5c --- /dev/null +++ b/source/regression-discontinuity/script.js @@ -0,0 +1,368 @@ + +d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + +var maxDate = 100 + +days.forEach((d, i) => { + d.date = d3.timeParse('%m/%d/%Y')(d.DATE) + d.cases = d.cases || +d.POSITIVE_TESTS || 0 + d.i = i +}) + +days = days + .filter(d => d.i <= maxDate) + .map(d => { + var rv = [d.i, d.cases] // fmt for simple stats + rv.DATE = d.DATE + rv.cases = d.cases + rv.dateStr = d3.timeFormat('%b %e')(d.date) + rv.i = d.i + return rv + }) + +var sel = d3.select('#graph').html('') + +var bars = [ + {i: 23}, + {i: 47}, + {i: 69}, +] + +var lines = [ + {i: bars[0].i, j: bars[1].i, color: d3.color('darkorange').darker(.2) + ''}, + {i: bars[1].i, j: bars[2].i, color: 'green'}, +] +lines.forEach(d => d.days = days.map(d => ({i: d.i}))) + +d3.selectAll('.underline').data(lines) + .st({borderBottom: d => `2px ${d3.color(d.color).brighter(.3)} dotted`}) + + +function initCases(){ + var c = d3.conventions({ + sel: sel.append('div').st({position: 'relative', zIndex: 1}), + margin: {left: 40}, + totalHeight: 250, + totalWidth: 960, + }) + + c.x.domain([0, maxDate]) + c.y.domain([0, 6500]) + + c.xAxis.tickSizeOuter(0).tickValues(d3.range(0, maxDate, 7)).tickFormat(d => days[d].dateStr) + c.yAxis.ticks(5) + d3.drawAxis(c) + + c.svg.select('.y .tick:last-child text') + .select(function() { return this.parentNode.insertBefore(this.cloneNode(0), this.nextSibling) }) + .text('New Cases in NYC') + .at({textAnchor: 'start', x: -6}) + .parent().select('line').remove() + + + var barWidth = Math.floor(c.x(1)) - 1 + var barPathFn = d => ['M', d.x || Math.round(c.x(d.i)), c.y(d.cases), 'V', c.height].join(' ') + c.svg.appendMany('path', days) + .at({ + d: barPathFn, + stroke: '#ddd', + strokeWidth: barWidth, + }) + + bars.forEach(d => { + d.x = c.x(d.i) + d.initI = d.i + d.cases = c.y.domain()[1] + }) + + var drag = d3.drag() + .on('start', d => { + bars.forEach(e => e.isDragging = e == d) + sel.st({cursor: 'pointer'}) + __timer.stop() + }) + .on('drag', d => { + d.x = d3.clamp(0, d3.event.x, c.x.range()[1]) + d.i = d3.clamp(0, Math.round(c.x.invert(d.x)), maxDate) + // console.log(d.x) + render() + }) + .on('end', d => { + d.x = c.x(d.i) + barSel.filter(d => d.isDragging).transition().duration(300) + .at({d: barPathFn}) + sel.st({cursor: 'auto'}) + }) + + var barSel = c.svg.appendMany('path', bars) + .at({ + d: barPathFn, + strokeWidth: barWidth + 11, + opacity: .5, + stroke: 'steelblue', + cursor: 'pointer', + }) + .call(drag) + + var lineSel = c.svg.appendMany('g', lines).st({pointerEvents: 'none'}) + + lineSel.append('path') + .at({stroke: d => d.color,strokeDasharray: '2 2',strokeWidth: 2}) + .each(function(d){ d.pathSel = d3.select(this) }) + + lineSel.append('path') + .at({id: d => d.color}) + .each(function(d){ d.pathSel2 = d3.select(this) }) + + lineSel.append('text') + .at({dy: '-.33em'}) + .append('textPath') + .at({fill: d => d.color, href: d => '#' + d.color, dy: '1em', textAnchor: 'middle'}) + .attr('startOffset', '50%') + .each(function(d){ d.textSel = d3.select(this) }) + + var dropSel = c.svg.append('path') + .at({stroke: '#000', d: ['M .5 ', c.height, 'v', 250].join(' ')}) + .st({pointerEvents: 'none'}) + + window.annos = [ + { + "path": "M -35,37 A 33.857 33.857 0 0 0 -1,3", + "text": "Mandatory 😷", + "textOffset": [ + -125, + 41 + ] + } + ] + + var swoopy = d3.swoopyDrag() + .draggable(1) + .draggable(0) + .x(d => c.x(47)) + .y(d => c.height) + .annotations(annos) + + var swoopySel = c.svg.append('g.annotations') + swoopySel.call(swoopy) + swoopySel.selectAll('path').attr('marker-end', 'url(#arrowhead)') + // swoopySel.selectAll('text') + // .each(function(d){ + // d3.select(this) + // .text('') //clear existing text + // .tspans(d3.wordwrap(d.text)) //wrap after 20 char + // }) + + var rv = () => { + bars.forEach(d => { + if (d.isDragging) d.i = d3.clamp(0, Math.round(c.x.invert(d.x)), maxDate) + }) + + barSel.filter(d => d.isDragging).transition().duration(0) + .at({d: barPathFn}) + + var [min, mid, max] = _.sortBy(bars.map(d => d.i)) + bars.mid = mid + + dropSel.translate(c.x(mid), 0) + + days.forEach((day) => { + var [i, j, k] = _.sortBy([min, day.i, max]) + + updateLine(i, j, lines[0]) + updateLine(j, k, lines[1]) + function updateLine(i, j, line){ + var d = line.days[day.i] + d.i = i + d.j = j + d.reg = ss.linearRegression(days.slice(i, j + 1)) + } + }) + + lines.forEach(line => { + var {i, j, reg} = line.days[mid] + var rl = ss.linearRegressionLine(reg) + + line.pathSel.at({d: [ + 'M', c.x(i), c.y(rl(i)), + 'L', c.x(j), c.y(rl(j)), + ].join(' ') + }) + + line.pathSel2.at({d: [ + 'M', c.x(i - 10), c.y(rl(i - 10)), + 'L', c.x(j + 10), c.y(rl(j + 10)), + ].join(' ') + }) + + var midX = (i + j)/2 + line.textSel + .at({opacity: i != midX ? 1 : 0}) + .text(d3.format('+')(Math.round(reg.m)) + ' New Cases/Day') + }) + } + + rv.shiftBars = offset => { + var initIs = _.sortBy(bars.map(d => d.initI)) + _.sortBy(bars, d => d.i).forEach((d, i) => { + d.prevX = d.x + d.nextX = c.x(initIs[i] + offset) + d.isDragging = true + }) + + window.__timer.stop() + if (window.__shiftTimer) window.__shiftTimer.stop() + window.__shiftTimer = d3.timer(t => { + var s = d3.easeCubicInOut(Math.min(t/800, 1)) + + bars.forEach((d, i) => { + d.x = lerp(d.prevX, d.nextX, s) + }) + + render() + + if (s == 1){ + window.__shiftTimer.stop() + bars.forEach(d => d.isDragging = false) + } + + }) + + function lerp(a, b, t) { + return (1 - t)*a + t*b; + } + } + + return rv +} + + +function initSlope(){ + var c = d3.conventions({ + sel: sel.append('div').st({position: 'relative', zIndex: 0}), + margin: {left: 40, top: 40, bottom: 100}, + height: 190, + totalWidth: 960, + }) + + c.svg.append('rect').at({width: c.width, height: c.height, fillOpacity: 0}) + c.svg + .on('click', function(){ + __timer.stop() + var mid = _.sortBy(bars, d => d.i)[1] + mid.isDragging = true + mid.x = d3.mouse(this)[0] + render() + }) + .st({cursor: 'pointer'}) + + c.svg.parent().st({overflow: 'hidden'}) + + c.x.domain([0, maxDate]) + c.y.domain([-150, 150]).interpolate(d3.interpolateRound) + + var line = d3.line() + .x((d, i) => c.x(i)) + .y(d => c.y(d.reg.m)) + .curve(d3.curveStep) + + var lineSel = c.svg.appendMany('path', lines) + .st({ + stroke: d => d.color, + fill: 'none', + strokeWidth: 2, + pointerEvents: 'none', + }) + + var circleSel = c.svg.appendMany('circle', lines) + .at({ + r: 4, + fill: d => d.color, + pointerEvents: 'none', + }) + + c.svg.append('linearGradient') + .at({id: 'top', y2: 1, x1: 0, x2: 0, y1: 0}) + .append('stop') + .at({offset: '0%', stopColor: 'rgba(245,245,245,1)'}) + .parent().append('stop') + .at({offset: '100%', stopColor: 'rgba(245,245,245,.1)'}) + c.svg.append('rect') + .at({width: c.width, height: c.margin.top, y: -c.margin.top, fill: 'url(#top)'}) + + c.svg.append('linearGradient') + .at({id: 'bot', y2: 0, x1: 0, x2: 0, y1: 1}) + .append('stop') + .at({offset: '0%', stopColor: 'rgba(245,245,245,1)'}) + .parent().append('stop') + .at({offset: '100%', stopColor: 'rgba(245,245,245,.1)'}) + c.svg.append('rect') + .at({width: c.width, height: c.margin.bottom, y: c.height, fill: 'url(#bot)'}) + + + c.xAxis.tickSizeOuter(0).tickValues(d3.range(0, maxDate, 7)).tickFormat(d => days[d].dateStr) + c.yAxis.ticks(5).tickFormat(d3.format('+')) + + d3.drawAxis(c) + c.svg.select('.y .tick:last-child text') + .select(function() { return this.parentNode.insertBefore(this.cloneNode(0), this.nextSibling) }) + .text('New Cases/Day') + .at({textAnchor: 'start', x: -6}) + .parent().select('line').remove() + + return () => { + lineSel.at({d: d => line(d.days)}) + + circleSel.translate(d => [c.x(bars.mid), c.y(d.days[bars.mid].reg.m)]) + } + +} + + + +var renderCases = initCases() +var renderSlope = initSlope() + +function render(){ + renderCases() + renderSlope() +} +render() + +if (window.__timer) window.__timer.stop() +window.__timer = d3.timer(t => { + var s = (Math.sin(Math.PI*2*t/10000))/2 + .5 + + var scale = d3.scaleLinear().range([420 - 70, 420 + 70]) + bars[1].x = scale(s) + bars[1].isDragging = true + render() +}) + + +function resize(){ + var width = innerWidth > 960 ? 960 : d3.select('p').node().offsetWidth + var r = width/960 + var isMobile = r != 1 + d3.select('#graph') + .st({ + transform: `scale(${r})`, + transformOrigin: 0 + 'px ' + 0 + 'px', + height: isMobile ? r*500 : '', + marginLeft: isMobile ? 0 : '', + marginBottom: isMobile ? 20 : '' + }) + .classed('mobile', isMobile) +} + +d3.select(window).on('resize', _.debounce(resize, 250)) +resize() + + + +d3.selectAll('.lag').data([7, 0]) + .on('click', renderCases.shiftBars) + + + + diff --git a/source/regression-discontinuity/style.css b/source/regression-discontinuity/style.css new file mode 100644 index 00000000..fbfb3a9a --- /dev/null +++ b/source/regression-discontinuity/style.css @@ -0,0 +1,159 @@ +body{ + /*font-family: menlo, Consolas, 'Lucida Console', monospace; */ + /*margin: 0px;*/ +} + +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + +.y .domain{ + display: none; +} + +text{ + /*pointer-events: none;*/ + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; + user-select: none; + font-family: sans-serif; + +} + + +.axis{ + opacity: .7; +} + +.axis line, .axis path{ + opacity: .5; +} + +.axis text{ + text-shadow: 0 1px 0 rba(245,245,245), 1px 0 0 rba(245,245,245), 0 -1px 0 rba(245,245,245), -1px 0 0 rba(245,245,245); +} + + +.paper-img{ + max-width: 683px; + margin: 0px auto; +} + +.paper-img img{ + width: 100%; +} + +#graph{ + /*margin-bottom: 40px;*/ + margin-bottom: -60px; + width: 960px; + margin-left: -105px; +} + +p{ + position: relative; + /*z-index: -2;*/ +} + + +.annotations path{ + fill: none; + stroke: black; + stroke-width: .6px; +} +.annotations text{ + font-size: 14px; + font-family: sans-serif; +} + + + +.mobile text{ + font-size: 25px; +} +.mobile .axis text, .mobile .annotations text{ + font-size: 14px; +} + + +.mobile .x .tick:nth-child(odd) text{ + opacity: 0; +} + +@media (min-width: 760px){ + .mobile text{ + font-size: 20px; + } + +} + +#graph{ + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + + + +.lag{ + cursor: pointer; + border: 1px solid #000; + padding-left: 3px; + padding-right: 3px; + margin-left: 2px; + padding-top: 1px; + padding-bottom: 1px; + margin-right: 2px; + user-select: none; + background: #4682b480; + white-space: nowrap; +} + +.lag:hover{ + background: #4682b4ff; +} + +h1{ + margin: 0 auto; + margin-bottom: 1.2em; + margin-top: 1.2em; + max-width: 600px; + text-align: center; +} + + +#notes{ + font-family: sans-serif; + font-size: 12px; + opacity: .7; +} + + diff --git a/source/regression-discontinuity/todo b/source/regression-discontinuity/todo new file mode 100644 index 00000000..ee2b88b1 --- /dev/null +++ b/source/regression-discontinuity/todo @@ -0,0 +1,15 @@ +- utd colors + +photoshop img +- dates +- bars +- colored lines + +text, here's what it does + +viewbox mobile +up stroke on rect + +links + +While the curves are suggestive, case counts have risen and fallen in other regions that \ No newline at end of file diff --git a/source/same-sex-legal/style.css b/source/same-sex-legal/style.css index 63d1f2d3..1aa755a5 100644 --- a/source/same-sex-legal/style.css +++ b/source/same-sex-legal/style.css @@ -1,3 +1,7 @@ +body{ + max-width: 465px; +} + .chart{ font-family: monaco, Consolas, 'Lucida Console', monospace; height: 1550px; diff --git a/source/scan-sorted/script.js b/source/scan-sorted/script.js new file mode 100644 index 00000000..08d2a930 --- /dev/null +++ b/source/scan-sorted/script.js @@ -0,0 +1,217 @@ +console.clear() +d3.select('body').selectAppend('div.tooltip.tooltip-hidden') + + +var sel = d3.select('#graph').html('').append('div') +var c = d3.conventions({ + sel: sel.append('div'), + margin: {left: 0, right: 0, top: 0, bottom: 0}, + height: 500, +}) +var r = 3 +var data = d3.range(0, c.width, r*2).map(x => { return {x, y: Math.random()} }) + + +c.x.domain(d3.extent(data, d => d.x)) +c.y.domain(d3.extent(data, d => d.y)) + +data.forEach(d => { + d.px = d3.clamp(r, c.x(d.x), c.width - r) + d.py = d3.clamp(r, c.y(d.y), c.height - r) +}) +var bisect = d3.bisector(d => d.px) + +var lRect = c.svg.append('rect.overlay').at({height: c.height}) +var rRect = c.svg.append('rect.overlay').at({height: c.height}) +var mRect = c.svg.append('rect.overlay').at({height: c.height}) + +c.svg.append('path') + .at({ + d: 'M' + data.map(d => [d.px, d.py]).join('L'), + stroke: '#000', + fill: 'none', + strokeWidth: .2 + }) + +var circleSel = c.svg.appendMany('circle', data) + .translate(d => [d.px, d.py]) + .at({ + fill: '#000', + stroke: '#f5f5f5', + r, + strokeWidth: .5, + }) + + +var areaCircle = c.svg.append('circle') + .at({r: 0, stroke: '#f0f', fill: 'none', strokeDasharray: '1 0'}) + +var overlayCircle = c.svg.appendMany('circle', [0, 0, 1]) + .at({stroke: d => d ? '#f0f' : '#ff0', strokeWidth: 1.5, r, fill: 'none'}) + +var lineSel = c.svg.appendMany('path', [0, 0, 1]) + .at({stroke: d => d ? '#f0f' : '#ff0', strokeWidth: 1}) + +c.svg.append('rect') + .at({opacity: 0, width: c.width, height: c.height}) + .st({cursor: 'pointer'}) + .on('click', function(){ + var [px, py] = d3.mouse(this) + animatePoint(px, py, true) + }) + +var isFirst = true +animatePoint() +function animatePoint(px=Math.random()*c.width, py=Math.random()*c.height, isManual){ + + var dur = isFirst ? 0 : 500 + + areaCircle.transition('pos').duration(dur) + .translate([px, py]) + + steps = genSteps(px, py) + if (!isManual && steps.length < 5) return animatePoint() + _.last(steps).isLast = true + steps[0].isFirst = true + + var minDist = steps[0].minDist + steps.forEach(d => { + if (d.minDist != minDist) d.minChanged = true + minDist = d.minDist + }) + + var curStepIndex = 0 + var lp = data[steps[0].index] + var rp = data[steps[0].index] + if (!lp || !rp) return animatePoint() + + animateStep() + + function animateStep(){ + if (window.__timeoutPoint) window.__timeoutPoint.stop() + if (window.__timeoutStep) window.__timeoutStep.stop() + + var curStep = steps[curStepIndex] + if (!curStep){ + return window.__timeoutPoint = d3.timeout(animatePoint, 2000) + } + var delay2 = curStep.isFirst ? 0 : dur*1.5 + + lp = data[curStep.index + (0 + curStep.i)*-1] || lp + rp = data[curStep.index + (0 + curStep.i)*1] || rp + + areaCircle.transition().duration(dur).delay(delay2) + .at({r: curStep.minDist}) + + lRect.transition().duration(dur).delay(delay2) + .at({width: Math.max(0, px - curStep.minDist)}) + + var rWidth = Math.max(0, c.width - px - curStep.minDist) + rRect.transition().duration(dur).delay(delay2) + .at({width: rWidth, x: c.width - rWidth}) + + var pad = curStep.isLast ? r*2 : r + mRect.transition().duration(dur).delay(0) + .at({width: rp.px - lp.px + pad*2 , x: lp.px - pad}) + + lineSel.data([lp, rp, curStep.minPoint]) + .transition().duration(dur).delay((d, i) => i != 2 ? 0 : delay2) + .at({d: d => ['M', px, py, 'L', d.px, d.py].join(' ')}) + + overlayCircle.data([lp, rp, curStep.minPoint]) + .transition().duration(dur).delay((d, i) => i != 2 ? 0 : delay2) + .translate(d => [d.px, d.py]) + + curStepIndex++ + + window.__timeoutStep = d3.timeout(animateStep, curStep.minChanged ? 1000 + delay2 : 1000) + } + + isFirst = false + dur = 500 + +} + + +function genSteps(px, py){ + var steps = [] + var index = bisect.left(data, px) + var index = d3.scan(data, (a, b) => Math.abs(a.px - px) - Math.abs(b.px - px)) + + var minPoint = null + var minDist = Infinity + var lxDist = 0 + var rxDist = 0 + var i = 0 + while (lxDist < minDist && rxDist < minDist){ + lxDist = checkPoint(data[index - i]) + rxDist = checkPoint(data[index + i]) + steps.push({index, i, minDist, minPoint, lxDist, rxDist}) + i++ + } + + function checkPoint(d){ + if (!d) return Infinity + + var dx = d.px - px + var dy = d.py - py + var dist = Math.sqrt(dx*dx + dy*dy) + + if (dist < minDist){ + minDist = dist + minPoint = d + } + + return Math.abs(px - d.px) + } + + return steps +} + + +function timeThings(){ + // time delaunay + var points = d3.range(1000000).map(() => [Math.random(), Math.random()]) + + console.time('delaunay') + var delaunay = d3.Delaunay.from(points) + console.timeEnd('delaunay') + var voronoi = delaunay.voronoi([0, 0, 1, 1]) + + + console.time('scan') + var px = Math.random() + var py = Math.random() + var minPoint = d3.scan(points, d => { + var dx = d[0] - px + var dy = d[0] - py + + return dx*dx + dy*dy + }) + console.timeEnd('scan') +} + +// timeThings() + + + + + + +console.log('hsdsdfdsfdddasdf') + + + + + + + + + + + + + + + + diff --git a/source/scan-sorted/style.css b/source/scan-sorted/style.css new file mode 100644 index 00000000..2073f5e5 --- /dev/null +++ b/source/scan-sorted/style.css @@ -0,0 +1,93 @@ +body{ + max-width: 600px; +} + + + +.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +@media (max-width: 590px){ + div.tooltip{ + bottom: -1px; + width: calc(100%); + left: -1px !important; + right: -1px !important; + top: auto !important; + width: auto !important; + } +} + +svg{ + overflow: visible; +} + + + + +.full-width { + width: 100vw; + position: relative; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; +} + +#graph{ + height: 500px; +} +#graph > div{ + margin-bottom: 45px; +} + +pre{ + overflow-x: auto; +} + +.purple,.yellow{ + padding: 2px; + padding-left: 4px; + padding-right: 4px; + border-radius: 5px; +} +.purple{ + background: #f0f; +} +.yellow{ + background: #ff0; +} + + + +code{ + background: #f5f5f5 !important; +} + + + + + + + + + + + + + + + + + diff --git a/source/scan-sorted/todo b/source/scan-sorted/todo new file mode 100644 index 00000000..e69de29b diff --git a/source/shared/chromatic.js b/source/shared/chromatic.js new file mode 100644 index 00000000..90b8e695 --- /dev/null +++ b/source/shared/chromatic.js @@ -0,0 +1,2 @@ +// https://d3js.org/d3-scale-chromatic/ v1.5.0 Copyright 2019 Mike Bostock +!function(f,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("d3-interpolate"),require("d3-color")):"function"==typeof define&&define.amd?define(["exports","d3-interpolate","d3-color"],e):e((f=f||self).d3=f.d3||{},f.d3,f.d3)}(this,function(f,e,d){"use strict";function a(f){for(var e=f.length/6|0,d=new Array(e),a=0;a1)&&(f-=Math.floor(f));var e=Math.abs(f-.5);return wf.h=360*f-100,wf.s=1.5-1.5*e,wf.l=.8-.9*e,wf+""},f.interpolateRdBu=x,f.interpolateRdGy=g,f.interpolateRdPu=N,f.interpolateRdYlBu=v,f.interpolateRdYlGn=C,f.interpolateReds=hf,f.interpolateSinebow=function(f){var e;return f=(.5-f)*Math.PI,Af.r=255*(e=Math.sin(f))*e,Af.g=255*(e=Math.sin(f+Pf))*e,Af.b=255*(e=Math.sin(f+Bf))*e,Af+""},f.interpolateSpectral=I,f.interpolateTurbo=function(f){return f=Math.max(0,Math.min(1,f)),"rgb("+Math.max(0,Math.min(255,Math.round(34.61+f*(1172.33-f*(10793.56-f*(33300.12-f*(38394.49-14825.05*f)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+f*(557.33+f*(1225.33-f*(3574.96-f*(1073.77+707.56*f)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+f*(3211.1-f*(15327.97-f*(27814-f*(22569.18-6838.66*f)))))))+")"},f.interpolateViridis=xf,f.interpolateWarm=yf,f.interpolateYlGn=Z,f.interpolateYlGnBu=U,f.interpolateYlOrBr=ff,f.interpolateYlOrRd=df,f.schemeAccent=b,f.schemeBlues=af,f.schemeBrBG=u,f.schemeBuGn=L,f.schemeBuPu=q,f.schemeCategory10=c,f.schemeDark2=t,f.schemeGnBu=T,f.schemeGreens=bf,f.schemeGreys=nf,f.schemeOrRd=k,f.schemeOranges=pf,f.schemePRGn=y,f.schemePaired=n,f.schemePastel1=r,f.schemePastel2=o,f.schemePiYG=w,f.schemePuBu=E,f.schemePuBuGn=W,f.schemePuOr=P,f.schemePuRd=H,f.schemePurples=of,f.schemeRdBu=G,f.schemeRdGy=R,f.schemeRdPu=K,f.schemeRdYlBu=Y,f.schemeRdYlGn=O,f.schemeReds=mf,f.schemeSet1=i,f.schemeSet2=l,f.schemeSet3=m,f.schemeSpectral=S,f.schemeTableau10=h,f.schemeYlGn=X,f.schemeYlGnBu=Q,f.schemeYlOrBr=$,f.schemeYlOrRd=ef,Object.defineProperty(f,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/source/shared/d3-color.v2.min.js b/source/shared/d3-color.v2.min.js new file mode 100644 index 00000000..2ed02865 --- /dev/null +++ b/source/shared/d3-color.v2.min.js @@ -0,0 +1,2 @@ +// https://d3js.org/d3-color/ v2.0.0 Copyright 2020 Mike Bostock +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).d3=t.d3||{})}(this,function(t){"use strict";function e(t,e,n){t.prototype=e.prototype=n,n.constructor=t}function n(t,e){var n=Object.create(t.prototype);for(var i in e)n[i]=e[i];return n}function i(){}var r="\\s*([+-]?\\d+)\\s*",a="\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*",s="\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*",o=/^#([0-9a-f]{3,8})$/,h=new RegExp("^rgb\\("+[r,r,r]+"\\)$"),l=new RegExp("^rgb\\("+[s,s,s]+"\\)$"),u=new RegExp("^rgba\\("+[r,r,r,a]+"\\)$"),c=new RegExp("^rgba\\("+[s,s,s,a]+"\\)$"),g=new RegExp("^hsl\\("+[a,s,s]+"\\)$"),f=new RegExp("^hsla\\("+[a,s,s,a]+"\\)$"),d={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};function p(){return this.rgb().formatHex()}function b(){return this.rgb().formatRgb()}function y(t){var e,n;return t=(t+"").trim().toLowerCase(),(e=o.exec(t))?(n=e[1].length,e=parseInt(e[1],16),6===n?w(e):3===n?new M(e>>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?m(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?m(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=h.exec(t))?new M(e[1],e[2],e[3],1):(e=l.exec(t))?new M(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=u.exec(t))?m(e[1],e[2],e[3],e[4]):(e=c.exec(t))?m(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=g.exec(t))?R(e[1],e[2]/100,e[3]/100,1):(e=f.exec(t))?R(e[1],e[2]/100,e[3]/100,e[4]):d.hasOwnProperty(t)?w(d[t]):"transparent"===t?new M(NaN,NaN,NaN,0):null}function w(t){return new M(t>>16&255,t>>8&255,255&t,1)}function m(t,e,n,i){return i<=0&&(t=e=n=NaN),new M(t,e,n,i)}function N(t){return t instanceof i||(t=y(t)),t?new M((t=t.rgb()).r,t.g,t.b,t.opacity):new M}function k(t,e,n,i){return 1===arguments.length?N(t):new M(t,e,n,null==i?1:i)}function M(t,e,n,i){this.r=+t,this.g=+e,this.b=+n,this.opacity=+i}function v(){return"#"+q(this.r)+q(this.g)+q(this.b)}function x(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function q(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function R(t,e,n,i){return i<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new H(t,e,n,i)}function E(t){if(t instanceof H)return new H(t.h,t.s,t.l,t.opacity);if(t instanceof i||(t=y(t)),!t)return new H;if(t instanceof H)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,a=Math.min(e,n,r),s=Math.max(e,n,r),o=NaN,h=s-a,l=(s+a)/2;return h?(o=e===s?(n-r)/h+6*(n0&&l<1?0:o,new H(o,h,l,t.opacity)}function $(t,e,n,i){return 1===arguments.length?E(t):new H(t,e,n,null==i?1:i)}function H(t,e,n,i){this.h=+t,this.s=+e,this.l=+n,this.opacity=+i}function j(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}e(i,y,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:p,formatHex:p,formatHsl:function(){return E(this).formatHsl()},formatRgb:b,toString:b}),e(M,k,n(i,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new M(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new M(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:v,formatHex:v,formatRgb:x,toString:x})),e(H,$,n(i,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new H(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new H(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,i=n+(n<.5?n:1-n)*e,r=2*n-i;return new M(j(t>=240?t-240:t+120,r,i),j(t,r,i),j(t<120?t+240:t-120,r,i),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const O=Math.PI/180,P=180/Math.PI,I=.96422,S=1,_=.82521,z=4/29,C=6/29,L=3*C*C,A=C*C*C;function B(t){if(t instanceof F)return new F(t.l,t.a,t.b,t.opacity);if(t instanceof V)return W(t);t instanceof M||(t=N(t));var e,n,i=Q(t.r),r=Q(t.g),a=Q(t.b),s=G((.2225045*i+.7168786*r+.0606169*a)/S);return i===r&&r===a?e=n=s:(e=G((.4360747*i+.3850649*r+.1430804*a)/I),n=G((.0139322*i+.0971045*r+.7141733*a)/_)),new F(116*s-16,500*(e-s),200*(s-n),t.opacity)}function D(t,e,n,i){return 1===arguments.length?B(t):new F(t,e,n,null==i?1:i)}function F(t,e,n,i){this.l=+t,this.a=+e,this.b=+n,this.opacity=+i}function G(t){return t>A?Math.pow(t,1/3):t/L+z}function J(t){return t>C?t*t*t:L*(t-z)}function K(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Q(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function T(t){if(t instanceof V)return new V(t.h,t.c,t.l,t.opacity);if(t instanceof F||(t=B(t)),0===t.a&&0===t.b)return new V(NaN,0=Math.abs(t[a])?e+=n-r+t[a]:e+=t[a]-r+n,n=r;return n+e}function g(t){if(0===t.length)throw new Error("mean requires at least one data point");return r(t)/t.length}function n(t,r){var n,e,a=g(t),o=0;if(2===r)for(e=0;er&&(r=t[n]);return r}function i(t,r){var n=t.length*r;if(0===t.length)throw new Error("quantile requires at least one data point.");if(r<0||1f&&p(t,n,e);sf;)l--}t[n]===f?p(t,n,l):p(t,++l,e),l<=r&&(n=l+1),r<=l&&(e=l-1)}}function p(t,r,n){var e=t[r];t[r]=t[n],t[n]=e}function s(t,r){var n=t.slice();if(Array.isArray(r)){!function(t,r){for(var n=[0],e=0;et[t.length-1])return 1;var n=function(t,r){var n=0,e=0,a=t.length;for(;e>>1]?a=n:e=-~n;return e}(t,r);if(t[n]!==r)return n/t.length;n++;var e=function(t,r){var n=0,e=0,a=t.length;for(;e=t[n=e+a>>>1]?e=-~n:a=n;return e}(t,r);if(e===n)return n/t.length;var a=e-n+1;return a*(e+n)/2/a/t.length}function m(t){var r=s(t,.75),n=s(t,.25);if("number"==typeof r&&"number"==typeof n)return r-n}function d(t){return+s(t,.5)}function b(t){for(var r=d(t),n=[],e=0;e=e[n][u]);--g)(s=x(h,u,o,i)+e[n-1][h-1])n&&(n=t[e]),t[e]t.length)throw new Error("cannot generate more classes than there are data values");var n=f(t);if(1===y(n))return[n];var e=S(r,n.length),a=S(r,n.length);!function(t,r,n){for(var e,a=r[0].length,o=t[Math.floor(a/2)],i=[],u=[],h=0;h=Math.abs(a)&&(c+=1);else if("greater"===n)for(h=0;h<=e;h++)o[h]>=a&&(c+=1);else for(h=0;h<=e;h++)o[h]<=a&&(c+=1);return c/e},t.bisect=function(t,r,n,e,a){if("function"!=typeof t)throw new TypeError("func must be a function");for(var o=0;o + M 200,50 Moves pen to (200,50); + a draws elliptical arc; + 50,50 following a degenerate ellipse, r1 == r2 == 50; + i.e. a circle of radius 50; + 0 with no x-axis-rotation (irrelevant for circles); + 0,1 with large-axis-flag=0 and sweep-flag=1 (clockwise); + 100,0 to a point +100 in x and +0 in y, i.e. (300,50). + */ + var path = "M " + data[0][0] + "," + data[0][1] + + " a " + r + "," + r + + " 0 0," + (clockwise ? "1" : "0") + " " + + (data[1][0]-data[0][0]) + "," + (data[1][1]-data[0][1]); + + return path + } + + function hypotenuse(a, b) { + return Math.sqrt( Math.pow(a,2) + Math.pow(b,2) ); + } + + render.angle = function(_) { + if (!arguments.length) return angle; + angle = Math.min(Math.max(_, 1e-6), Math.PI-1e-6); + return render; + }; + + render.clockwise = function(_) { + if (!arguments.length) return clockwise; + clockwise = !!_; + return render; + }; + + render.x = function(_) { + if (!arguments.length) return xValue; + xValue = _; + return render; + }; + + render.y = function(_) { + if (!arguments.length) return yValue; + yValue = _; + return render; + }; + + return render; +} diff --git a/source/style.css b/source/style.css index 4a436f63..c0246ba5 100644 --- a/source/style.css +++ b/source/style.css @@ -10,7 +10,7 @@ body{ line-height: 1.55em; font-size: 16px; margin-top: 5px; - margin-bottom: 40px; + margin-bottom: 80px; } @media (max-width: 760px){ @@ -44,7 +44,7 @@ a{ float: right; } .header span{ - opacity: .5; + opacity: .6; } @media (max-width: 760px){ .header-right span{ @@ -52,11 +52,12 @@ a{ } } .header a{ - opacity: .5; + opacity: .6; text-decoration: none; } .header a:hover{ - opacity: 1 + opacity: 1; + /*text-decoration: underline;*/ } .header img{ width: 16px; @@ -88,13 +89,20 @@ h2,h3,h4,h5{ padding-left: .2em; padding-right: .2em; border-radius: 2px; - font-family: Consolas, monaco, 'Lucida Console', monospace; + font-family: monospace; } strong{ font-weight: bold; } +/*wp classes*/ +img.aligncenter { + display: block; + margin: auto; + max-width: 750px; +} + /* highlight css @@ -115,7 +123,7 @@ pre > code { .hljs-comment, .diff .hljs-header { - color: #998; + color: #666; font-style: italic; } @@ -193,7 +201,7 @@ pre > code { } .hljs-built_in { - color: #0086b3; + color: #02559a; } .hljs-preprocessor, @@ -215,7 +223,7 @@ pre > code { } .diff .hljs-change { - background: #0086b3; + background: #02559a; } .hljs-chunk { diff --git a/source/worlds-group-2018/annotations.json b/source/worlds-group-2018/annotations.json new file mode 100644 index 00000000..132e07c4 --- /dev/null +++ b/source/worlds-group-2018/annotations.json @@ -0,0 +1,62 @@ +[ + { + "path": "M -36,279 A 45.026 45.026 0 0 0 43,310", + "text": "Tap to see how the first match affects the group", + "team": "PVB", + "lw": 21, + "textOffset": [ + -47, + 244 + ] + }, + { + "path": "M -1000 0", + "text": "FW and G2 advance with two wins", + "team": "FW", + "lw": 22, + "textOffset": [ + 280, + 31 + ] + }, + { + "path": "M -1000 0", + "text": "PVB and AFs go home if they lose twice", + "team": "PVB", + "lw": 22, + "textOffset": [ + 280, + 31 + ] + }, + { + "path": "M 265,227 A 49.476 49.476 0 0 1 170,228", + "text": "RNG still gets a tiebreaker if they lose 3", + "team": "RNG", + "lw": 15, + "textOffset": [ + 269, + 186 + ] + }, + { + "path": "M 249,11 A 36.581 36.581 0 0 1 177,10.99999713897705", + "text": "A 3-0 finish doesn't guarantee advancement for 100", + "team": "100", + "lw": 24, + "textOffset": [ + 222, + -26 + ] + }, + { + "path": "M 315,21 A 80.989 80.989 0 0 1 179,88", + "text": "TL could be eliminated even if they beat EDG and KT", + "team": "TL", + "lw": 24, + "textOffset": [ + 259, + -7 + ] + } +] \ No newline at end of file diff --git a/source/worlds-group-2018/matches.tsv b/source/worlds-group-2018/matches.tsv new file mode 100644 index 00000000..a67ea8fe --- /dev/null +++ b/source/worlds-group-2018/matches.tsv @@ -0,0 +1,49 @@ +group t1 t2 winner date +C RNG FB 1 2017-10-05 15:30 HKT +C G2 SSG 2 2017-10-05 16:30 HKT +B LZ IMT 1 2017-10-05 17:30 HKT +B FNC GAM 2 2017-10-05 18:30 HKT +A SKT C9 1 2017-10-05 19:30 HKT +A EDG AHQ 2 2017-10-05 20:30 HKT +D FW TSM 2 2017-10-06 15:00 HKT +D WE MSF 1 2017-10-06 16:00 HKT +B IMT FNC 1 2017-10-06 17:00 HKT +B LZ GAM 1 2017-10-06 18:00 HKT +A AHQ C9 2 2017-10-06 19:00 HKT +A EDG SKT 2 2017-10-06 20:00 HKT +D TSM WE 1 2017-10-07 15:00 HKT +D FW MSF 2 2017-10-07 16:00 HKT +C G2 FB 1 2017-10-07 17:00 HKT +C SSG RNG 2 2017-10-07 18:00 HKT +A SKT AHQ 1 2017-10-07 19:00 HKT +A C9 EDG 1 2017-10-07 20:00 HKT +D WE FW 1 2017-10-08 15:00 HKT +D MSF TSM 1 2017-10-08 16:00 HKT +B GAM IMT 2 2017-10-08 17:00 HKT +B FNC LZ 2 2017-10-08 18:00 HKT +C FB SSG 2 2017-10-08 19:00 HKT +C RNG G2 1 2017-10-08 20:00 HKT +B IMT GAM 2017-10-12 12:00 HKT +B LZ FNC 2017-10-12 13:00 HKT +B FNC IMT 2017-10-12 14:00 HKT +B GAM LZ 2017-10-12 15:00 HKT +B GAM FNC 2017-10-12 16:00 HKT +B IMT LZ 2017-10-12 17:00 HKT +C G2 RNG 2017-10-13 12:00 HKT +C SSG FB 2017-10-13 13:00 HKT +C SSG G2 2017-10-13 14:00 HKT +C FB RNG 2017-10-13 15:00 HKT +C FB G2 2017-10-13 16:00 HKT +C RNG SSG 2017-10-13 17:00 HKT +D MSF FW 2017-10-14 12:00 HKT +D WE TSM 2017-10-14 13:00 HKT +D FW WE 2017-10-14 14:00 HKT +D TSM MSF 2017-10-14 15:00 HKT +D TSM FW 2017-10-14 16:00 HKT +D MSF WE 2017-10-14 17:00 HKT +A AHQ SKT 2017-10-15 12:00 HKT +A EDG C9 2017-10-15 13:00 HKT +A AHQ EDG 2017-10-15 14:00 HKT +A C9 SKT 2017-10-15 15:00 HKT +A C9 AHQ 2017-10-15 16:00 HKT +A SKT EDG 2017-10-15 17:00 HKT \ No newline at end of file diff --git a/source/worlds-group-2018/matches2.tsv b/source/worlds-group-2018/matches2.tsv new file mode 100644 index 00000000..448fd8ed --- /dev/null +++ b/source/worlds-group-2018/matches2.tsv @@ -0,0 +1,49 @@ +group t1 t2 winner date +C KT TL 1 10-10 +C EDG MAD 1 10-10 +A PVB FW 2 10-10 +A AFs G2 2 10-10 +B RNG C9 1 10-10 +B GEN VIT 2 10-10 +A FW AFs 1 10-11 +A PVB G2 1 10-11 +D 100 FNC 2 10-11 +D iG GRX 1 10-11 +B VIT C9 2 10-11 +B GEN RNG 2 10-11 +C MAD KT 2 10-12 +C TL EDG 2 10-12 +D FNC iG 2 10-12 +D 100 GRX 1 10-12 +B RNG VIT 1 10-12 +B C9 GEN 2 10-12 +A AFs PVB 1 10-13 +A G2 FW 1 10-13 +D iG 100 1 10-13 +D GRX FNC 2 10-13 +C TL MAD 1 10-13 +C KT EDG 1 10-13 +B VIT RNG TBD 10-14 +B GEN C9 TBD 10-14 +B VIT GEN TBD 10-14 +B C9 RNG TBD 10-14 +B C9 VIT TBD 10-14 +B RNG GEN TBD 10-14 +A AFs FW TBD 10-15 +A G2 PVB TBD 10-15 +A FW G2 TBD 10-15 +A PVB AFs TBD 10-15 +A FW PVB TBD 10-15 +A G2 AFs TBD 10-15 +C TL KT TBD 10-16 +C MAD EDG TBD 10-16 +C MAD TL TBD 10-16 +C EDG KT TBD 10-16 +C EDG TL TBD 10-16 +C KT MAD TBD 10-16 +D FNC 100 TBD 10-17 +D GRX iG TBD 10-17 +D FNC GRX TBD 10-17 +D 100 iG TBD 10-17 +D GRX 100 TBD 10-17 +D iG FNC TBD 10-17 \ No newline at end of file diff --git a/source/worlds-group-2018/script.js b/source/worlds-group-2018/script.js new file mode 100644 index 00000000..14ea4f36 --- /dev/null +++ b/source/worlds-group-2018/script.js @@ -0,0 +1,270 @@ +// http://esports-assets.s3.amazonaws.com/production/files/rules/2017-World-Championship-Rules-v-17-3.pdf +// https://esports-assets.s3.amazonaws.com/production/files/rules/2017-MSI-Ruleset.pdf + +var ƒ = d3.f +console.clear() +d3.loadData('annotations.json', 'matches2.tsv', function(err, res){ + d3.selectAll('.group-header').st({opacity: 1}) + + annotations = res[0] + matches = res[1] + + teams2wins = {} + + matches.forEach(function(d, i){ + d.winner = +d.winner + d.actualWinner = !(d.winner === 0) ? d.winner : 3 + d.complete = i < 24 + d.allTeams = d.t1 + '-' + d.t2 + d.wName = d['t' + d.winner] + + if (!teams2wins[d.t1]) teams2wins[d.t1] = 0 + if (!teams2wins[d.t2]) teams2wins[d.t2] = 0 + teams2wins[d.wName]++ + }) + + byGroup = d3.nestBy(matches, ƒ('group')) + byGroup.forEach(drawGroup) +}) + + +function scoreMatches(matches){ + var teams = d3.nestBy(matches, ƒ('t1')).map(function(d){ + return {name: d.key, w: 0} + }) + var nameToTeam = {} + teams.forEach(function(d){ nameToTeam[d.name] = d }) + matches.forEach(addMatchWins) + + teams.forEach(function(d){ + d.wins = d.w + d.w = 0 + }) + + d3.nestBy(teams, ƒ('wins')).forEach(function(d){ + if (d.length == 1 || d.length == 4) return + + + var tiedTeams = d.map(ƒ('name')).join('-') + var tiedMatches = matches + .filter(function(d){ + return ~tiedTeams.indexOf(d.t1) && ~tiedTeams.indexOf(d.t2) }) + tiedMatches.forEach(addMatchWins) + + // in 3-way tie, only head2head winning record gets out of tiebreaker + if (d.length != 3) return + console.log(d.length, d.map(d => d.w).join(' ')) + // console.log(d.length, JSON.parse(JSON.stringify(d))) + d.forEach(function(d){ d.w = d.w > 2 ? d.w : 0 }) + }) + + + var advanceSlots = 2 + d3.nestBy(teams, function(d){ return d.w + d.wins*10 }) + .sort(d3.descendingKey('key')) + .forEach(function(d){ + if (d.length <= advanceSlots){ + d.forEach(function(d){ d.advance = 't'}) + } else if (advanceSlots > 0){ + d.forEach(function(d){ d.advance = 'm'}) + } else{ + d.forEach(function(d){ d.advance = 'f' }) + } + advanceSlots -= d.length + }) + + function addMatchWins(d){ nameToTeam[d['t' + d.winner]].w++ } + + + return teams +} + +function drawGroup(gMatches){ + var sel = d3.select('#group-' + gMatches.key.toLowerCase()).html('') + + var complete = gMatches.filter(ƒ('winner')) + var incomplete = gMatches.filter(function(d){ return !d.complete }) + + scenarios = d3.range(64).map(function(i){ + incomplete.forEach(function(d, j){ + d.winner = (i >> j) % 2 ? 1 : 2 + d.wName = d['t' + d.winner] + }) + + return { + str: incomplete.map(ƒ('winner')).join(''), + teams: scoreMatches(gMatches), + incomplete: JSON.parse(JSON.stringify(incomplete))} + }) + + var teams = d3.nestBy(gMatches, ƒ('t1')).map(function(d){ + return {name: d.key, w: 0, actualWins: teams2wins[d.key]} + }).sort(d3.descendingKey('actualWins')) + + sel.appendMany('div.team', teams) + .each(function(d){ drawResults(d3.select(this), scenarios, d.name, complete, incomplete) }) + + incomplete.forEach(function(d){ d.clicked = (+d.actualWinner || 3) - 1 }) + var gameSel = sel.append('div.matches') + .st({marginTop: 50}) + .appendMany('div.game', incomplete) + .on('click', function(d){ + d.clicked = (d.clicked + 1) % 3 + + d3.select(this).selectAll('.teamabv') + .classed('won', function(e, i){ return i + 1 == d.clicked }) + d3.select(this).classed('active', d.clicked) + + var str = incomplete.map(ƒ('clicked')).join('') + sel.selectAll('circle.scenario').classed('hidden', function(d){ + return d.incomplete.some(function(d, i){ + return str[i] != '0' && str[i] != d.winner + }) + }) + }) + gameSel.append('span.teamabv').text(ƒ('t1')) + gameSel.append('span').text(' v. ') + gameSel.append('span.teamabv').text(ƒ('t2')) + gameSel.each(function(d){ d3.select(this).on('click').call(this, d) }) +} + + +function drawResults(sel, scenarios, name, complete, incomplete){ + scenarios.forEach(function(d){ + d.team = d.teams.filter(function(d){ return d.name == name })[0] + d.wins = d.team.wins + + d.playedIn = d.incomplete.filter(function(d){ + d.currentWon = name == d.wName + return name == d.t1 || name == d.t2 }) + d.recordStr = d.playedIn.map(function(d){ return +d.currentWon }).join('') + }) + + var against = [] + scenarios[0].playedIn.forEach(function(d){ + var otherTeam = name == d.t1 ? d.t2 : d.t1 + against.push(otherTeam) + }) + + var completeIn = complete.filter(function(d){ + d.currentWon = name == d.wName + d.otherTeam = name == d.t1 ? d.t2 : d.t1 + return name == d.t1 || name == d.t2 }) + + var pBeat = completeIn.filter(ƒ('currentWon')) + var pLost = completeIn.filter(function(d){ return !d.currentWon }) + + var pStr = 'Lost to ' + pStr += pLost.map(ƒ('otherTeam')).join(' and ') + + if (pBeat.length){ + pStr += ' // Beat ' + pBeat.map(ƒ('otherTeam')).join(' and ') + } else{ + pStr = pStr.replace(' and ', ', ') + } + if (!pLost.length) pStr = pStr.replace('Lost to //', '').replace(' and ', ', ') + // pStr += ' previously' + + var byWins = d3.nestBy(scenarios, ƒ('wins')) + byWins.forEach(function(d, i){ + d.sort(d3.descendingKey(ƒ('team', 'advance'))) + d.byRecordStr = _.sortBy(d3.nestBy(d, ƒ('recordStr')), 'key') + if (i == 1) d.byRecordStr.reverse() + }) + + var width = 400, height = 300 + var svg = sel.append('svg').at({width, height}).st({margin: 20}) + .append('g').translate([0, 100]) + var gSel = d3.select(sel.node().parentNode) + + var swoopySel = svg.append('g.annotations') + + svg.append('text').text(name) + .translate([10*3.5 + 100, -60]).at({textAnchor: 'middle', fontSize: 20}) + + svg.append('text').text(pStr) + .translate([10*3.5 + 100, -45]).at({textAnchor: 'middle', fontSize: 12, fill: '#888'}) + + + var winsSel = svg.appendMany('g', byWins.sort(d3.descendingKey('key'))) + .translate(function(d, i){ return [0, i*80 + (i == 3 ? -15*2 : i > 0 ? -8 : 0)] }) + + winsSel.append('text') + .text(function(d, i){ return i == 1 ? 'Only Lose To...' : i == 2 ? 'Only Beat...' : '' }) + .at({textAnchor: 'middle', x: 10*3.5 + 100, y: -30, fill: '#888', fontSize: 12}) + + var recordSel = winsSel.appendMany('g', ƒ('byRecordStr')) + .translate(function(d, i){ return [d.key == '000' || d.key == '111' ? 100 : i*100, 0] }) + + recordSel.append('text') + .text(function(d){ + var s + if (d.key == '111') s = 'Win Next Three' + if (d.key == '000') s = 'Lose Next Three' + if (d.key == '001') s = against[2] + if (d.key == '010') s = against[1] + if (d.key == '100') s = against[0] + if (d.key == '011') s = against[0] + if (d.key == '101') s = against[1] + if (d.key == '110') s = against[2] + return s + }) + .at({textAnchor: 'middle', x: 10*3.5, y: -10}) + + recordSel.appendMany('circle.scenario', ƒ()) + .at({r: 5, fill: ƒ('team', color), cx: function(d, i){ return i*10} }) + .call(d3.attachTooltip) + .on('mouseout', function(){ gSel.selectAll('circle.scenario').classed('active', false).at('r', 5) }) + .on('mouseover', function(d){ + gSel.selectAll('circle.scenario') + .classed('active', 0) + .attr('r', 5) + .filter(function(e){ return d.str == e.str }) + .classed('active', 1) + .attr('r', 8) + .raise() + + var tt = d3.select('.tooltip').html('') + var gameSel = tt.appendMany('div.game', incomplete) + gameSel.append('span').text(ƒ('t1')).classed('won', function(e, i){ return d.str[i] == 1 }) + gameSel.append('span').text(' v. ') + gameSel.append('span').text(ƒ('t2')).classed('won', function(e, i){ return d.str[i] == 2 }) + + var byAdvanceSel = tt.appendMany('div.advance', d3.nestBy(d.teams, ƒ('advance')).sort(d3.descendingKey('key'))) + .text(function(d){ + return d.map(ƒ('name')).join(' and ') + {t: ' advance', m: ' tie', f: (d.length > 1 ? ' are' : ' is') + ' eliminated'}[d.key] + }) + }) + + var swoopy = d3.swoopyDrag() + .draggable(1) + .draggable(0) + .x(function(){ return 0 }) + .y(function(){ return 0 }) + .annotations(annotations.filter(function(d){ return d.team == name })) + + swoopySel.call(swoopy) + swoopySel.selectAll('path').attr('marker-end', 'url(#arrow)') + swoopySel.selectAll('text') + .each(function(d){ + d3.select(this) + .text('') //clear existing text + .tspans(d3.wordwrap(d.text, d.lw || 20)) //wrap after 20 char + }) + +} + + +function color(d){ return {t: '#4CAF50', m: '#FF9800', f: '#F44336'}[d.advance] } + +d3.select('html').selectAppend('div.tooltip').classed('tooltip-hidden', 1) + +d3.select('html').selectAppend('svg.marker') + .append('marker') + .attr('id', 'arrow') + .attr('viewBox', '-10 -10 20 20') + .attr('markerWidth', 20) + .attr('markerHeight', 20) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M-6.75,-6.75 L 0,0 L -6.75,6.75') diff --git a/source/worlds-group-2018/style.css b/source/worlds-group-2018/style.css new file mode 100644 index 00000000..49c22c50 --- /dev/null +++ b/source/worlds-group-2018/style.css @@ -0,0 +1,112 @@ +svg{ + /*margin: 0px;*/ + /*background: black;*/ +} + +svg{ + font-family: monospace; + overflow: visible; + font-size: 14px; +} + +svg text{ + /*-webkit-text-stroke: 4px navy;*/ + -webkit-text-shadow: 0 1px 0 #F5F5F5, 1px 0 0 #F5F5F5, 0 -1px 0 #F5F5F5, -1px 0 0 #F5F5F5; + +} + +html { + min-width: 760px; + background-color: #F5F5F5; + font-weight: normal; + +} + +.group{ + width: 800px; + margin: 0px auto; + display: inline-block; +} + +.group-header{ + text-align: center; + margin-bottom: -0px; + margin-top: 80px; + opacity: 0; +} + +.team{ + display: inline-block; + width: 400px; + position: relative; + z-index: 0; +} + +.active{ + stroke: black; + stroke-width: 4; +} + +.hidden{ + opacity: .2; +} + + +div.tooltip { + top: -1000px; + position: fixed; + padding: 10px; + background: rgba(255, 255, 255, .90); + border: 1px solid lightgray; + pointer-events: none; + font-family: monospace; + width: 200px; + +} +.tooltip-hidden{ + opacity: 0; + transition: all .3s; + transition-delay: .1s; +} + +.game{ + display: inline-block; + width: 100px; + margin-bottom: 10px; + +} +.game .won{ + font-weight: 700; + text-decoration: underline; +} + +.matches{ + font-size: 16px; + text-align: center; +} + +.matches .game{ + width: 115px; + cursor: pointer; + border: 1px solid black; + margin-right: 10px; + opacity: .6; + font-family: monospace; +} +.matches .game.active{ + opacity: 1; +} +.matches .game span{ + pointer-events: none; + -webkit-user-select: none; + user-select: none; +} + +.annotations path{ + fill: none; + stroke: black; + stroke-width: .6px; +} +.annotations text{ + font-size: 12px; +} \ No newline at end of file diff --git a/source/worlds-group-2018/todo b/source/worlds-group-2018/todo new file mode 100644 index 00000000..da1ca546 --- /dev/null +++ b/source/worlds-group-2018/todo @@ -0,0 +1,22 @@ +todo +- update data +- new share img +- new anno +- better form? no timee + +annos +B +- C9 avoids elimination with a win against SKT +- Without two wins, IM and FW are out +- If SKT loses to C9 and IM, it could be eliminated + +C +- EDG's loss to the 1-2 INT means it has larger chance of elimination than AHQ +- Likewise, INT's win over EDG gives it more ways to advance than H2k +- If INT lose the first game to EDG, almost all their advancement options disappear. + +D +- 3 way tie if RNG, SSG and TSM each beat each other once again +- A win for SSG almost ensures that they won't be eliminated + +