Web Application & Visualization
I built a web application that converts any song on Spotify into a bouquet of flowers based on its audio analysis.
Spotify API
Photoshop
pandas
flask
D3.js
Heroku
I worked on this project in 2 phases: Proof of Concept and Productionizing. Phase 1 includes creating the standalone visual, which took roughly 4 weeks. Phase 2 includes using Python flask
to build the application, generalizing layout of the flower visual, and deploying, which took roughly another 4 weeks. I started Phase 2 6 months after completing Phase 1 because I learned about deploying data applications from the Data Engineering team at work. In that 6 month gap, I improved as a developer with the help of my coworkers and my series of personal projects that I felt ready to productionize my work.
In my standalone visual, I knew I wanted to work with a song from the Electroswing genre. It’s jazzy, groovy, and more complex than mainstream pop songs. I chose Lone Digger by Caravan Palace because I was familiar enough with how the song goes to be able to work with it.
I began my design process by browsing the Spotify API to see what data was available. I used Jupyter Notebook to check out samples of data from the relevant endpoints, like artist, track, audio analysis. Everything was straightforward to me except for the chroma vector. The data structure looked so odd, and there was a lack of examples online. Wikipedia actually had the best explanation of the chroma feature because of the visual. This allowed me to replicate it using matplotlib
.
In this process, I realized that music is an opportunistic space with interesting data and not much existing visualization. Most of the visuals I saw were research-based, and didn’t look aesthetic. It would be fun to make this chroma feature look pretty and fascinating at the same time.
I started by drawing random objects on a piece of paper to visualize data with the hierarchal structure of sections, bars, and individual notes. I decided to go with flowers (inspired by film flowers by Shirley Wu) because I could split it into 3 layers (flower, petals, petal colors). I hard coded the positions based on the order on the x-axis and section pitches on the y-axis.
After building the flower skeleton, I used Photoshop in my planning stage to figure out how to paint the flowers using individual notes. Based on my Photoshop mindset, I used a blur brush and clipping mask, which I would replicate with code using the <defs>
element.
The biggest challenge I had was passing data from one level of the hierarchy to the next. Not having much experience working with JSON, I defaulted to exporting my data as a .csv flat file, but it made the link between flowers, petals (subset of flower), and notes (subset of petal) impossible. Doing other tasks like creating a clipping mask was difficult in combination with solving the hierarchal data structure. I redid my data format by converting it to JSON, and my data’s subset-building functions magically worked with the following usage of .data()
in D3.
d3.selectAll("g")
.data(flowerJSON)
.enter()
.append("g")
.attr("x", d => d.x) // x-position of pistil
.attr("y", d => d.y) // y-position of pistil
.selectAll("path")
.data(f => f.petals)
.enter()
.append("path")
.attr("d", "M0,0 Z"); // insert path for drawing a single petal
Besides things not working the way I wanted for a long time, another mental drain was the visual not looking as good as I planned. I couldn’t make my results look like the one I had photoshopped because I was “using real data” (and needed to stay true to it). I experimented with different backgrounds, chroma thresholds, colors, and the feature that made it look GREAT ended up being the computationally expensive Gaussian blur. This visual loads pretty slow if I ran a simulation on it, so I loaded the filter after the simulation when I worked on Phase 2.
The following 2 images are my drafts that didn’t look good with the chroma vector. The first image doesn’t have enough blur and has a background that acts as additive light, so it’s hard to see the pitches if the flower has many petals. The second image has the blur I want but didn’t have the right background; the flowers look like they have holes.
We are done after a lot of editing!
I also added my interpretation notes when you hover over the button “hover for notes” on the bottom left.
My main goal in refactoring was the following:
class
for better organization.pandas
and return a JSON.Flask
object that would feed the generated data from the server into the javascript, client-side script.See my GitHub for the final result!
I used this YouTube tutorial by Corey Schafer to get started on using flask
.
Since I already made a standalone version, I created a templates
subdirectory, a default location for flask
and replaced a lot of my index.html
code with expressions enclosed in double brackets.
To send the data JSON over from the server (python web application) to the client (javascript), I added the data as a parameter in flask
’s render_template
function.
@app.route("/grow")
def grow():
"""
This code does stuff before returning.
"""
return render_template(
"index.html",
dataset=dataset,
)
My index.html
has this line to receive the data (the parameter dataset
should match between the 2 files). This actually took 5ever to figure out…
<script>var data = {{ dataset|tojson }};</script>
The following line in the HTML script uses the data
variable to construct the visualization.
While I didn’t have to change much of the core logic in my visualization script, I had to find a new way to generate flower positions dynamically. D3’s simulation has forceCollide
and forceCenter
(or forceX
and forceY
) that would help center the flowers while avoiding collision.
My result below accomplished the spaced-out flowers that I wanted, but introduced a new problem: the x-axis was no longer ordered. I created a flower stems that showed what each section’s order was, and this randomness from the simulation made it look tangled and messy.
I changed the simulation so that it would position flowers along the x-axis before centering them. I had some trouble with their starting positions because D3 would ignore the x and y positions I set for my group elements before running the simulation. This is why all the stems unintentionally pointed towards the (0, 0)
coordinate below. I also replaced the flowers with black circles because the flowers' gaussian blur filters made everything lag.
There were 2 ways to set the starting positions along the x-axis:
d.fx
instead of d.x
. D3’s source code has a method called initalizeNodes
that would check if the node positions should be fixed. I would set the fixed position and then make it null
after the simulation’s alpha (“entropy level”) is under some threshold.I went with option #2 and coded everything within the d3.simulation.on("tick")
block. There were 3 possible actions coded as an if
statement:
My code is generalized below.
var iteration = 1; // We are on Simulation 1 of 2
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX().x(d => d.x)) // Gravitate toward their x-positions
.force("collide", d3.forceCollide().radius(d => d.radius)) // Avoid overlapping
.on("tick", () => {
if (simulation.alpha() > 0.1) {
flowers
.attr("transform", d => `translate(${d.x},${d.y})`); // continue running simulation
} else if (iteration == 1) {
simulation.alpha(.5).restart(); // restart simulation with new forces
simulation
.force("center", d3.forceCenter().x(width/2).y(height/4)) // Modify this force
.force("x", d3.forceX().x(width/2)); // Create this new force
iteration = 2; // We are moving to Simulation 2 of 2
} else {
simulation.stop(); // stop simulation
doRemainingStuff(); // run the remaining steps AFTER simulation
};
});
Now the flowers and stems are positioned where they should be thanks to timing forces correctly!
In case you were wondering why I didn’t choose option #1, I actually didn’t know of it at the time! In retrospect, I think it would have shaved a few ticks off the clock if I had fixed their starting positions. :P
I deployed on Heroku because the service has a free tier and several examples of deploying Flask
applications. The requirements are pretty simple once you have done it at least once, and it does most of the infrastructure work for you. Following this guide, I created a requirements.txt
, Procfile
(Heroku-specific), and runtime.txt
(also Heroku-specific).
One difference I had with the guide was that I added my main script in an app
subdirectory. I specified the script’s location with a --pythonpath app
tag in my Procfile
.
# Conventional command in Procfile
web: gunicorn run:app
# My command in Procfile because I had an `app` subdirectory
web: gunicorn --pythonpath app run:app
Try your favorite Spotify song here! I like showcasing Hamilton
because the bouquet is very rich like the song. :P