Overview:
This week our task was to either make some sort of word art or create a data visualization. Pursing the latter, I decided the most relevant data that is out there right now is information relating to COVID-19. My goal was to create a color valued map, the most intuitive data visualization in my mind.
Process:
The trickiest part in my opinion was creating the map itself. To create shapes which could colored in processing I would have to create a shape through a series of points. At first I considered loading a map and the pixel values of the boarders by hand but I quickly realized how time consuming and inaccurate this would be when considering features such as rivers. I had a look at an SVG picture of the US and realized that to render the vector based image all the coordinates were already stored there by state. I then had to create a reader to take in this SVG path data, and convert this information to processing equivalents. I found what each svg command meant, C, L, M, z, and translated those into Processing shape functions.
With each path stored in a custom State object. I could easily draw a shape with a simple function call, and set the fill of it with a method.
I then had to take in my COVID data from the CDC, and process it to each state. Using the state codes that the data is associated with, I stored the respective data with the respective state in a table. This way when the state is drawn, it could easily find a value for a certain data point such as total cases.
With the data stored, I had to map it to a fill color. To do so I found the max value and min value of each type of data by iterating through the states. I then mapped a HSB saturation value from the min to the max. This gave me a range of colors from white to red, with the darkest red being the highest values.
I then used the min and max values and color mapping to create a scale that displayed is there was null data points, the min color, the max color, and the third and two third colors and their respective values. This gives viewers a sense of severity.
I then made this interactive where the right and left arrow keys allow users to toggle between data types such as deaths, cases, etc.
Results:
Here is my code and screenshots of the resulting maps.
//Variables for laoding state data file String[] lines; //Used to process svg file ArrayList<State> stateArray = new ArrayList<State>(); //Holds all states ArrayList<Data> dataArray = new ArrayList<Data>(); //Holds data for states int col = 0; //Which col of the dataArray is being shown int xOffset, yOffset; void setup() { size(1280, 720); lines = loadStrings("mapData.txt"); loadStateData(); loadCovidData(); colorMode(HSB, 100); } void loadStateData() { int index = 0; //Loop through states while (index < lines.length) { int stringIndex = 0; String[] words = split(lines[index], ' '); //Split each cord or State newState = new State(words[stringIndex]); stringIndex++; //Loop through data and save into the state's table while (stringIndex < words.length) { //See what the command is if (words[stringIndex].equals("M")) { String[] points = split(words[stringIndex+1], ','); newState.saveSVGData(words[stringIndex], float(points[0]), float(points[1])); stringIndex += 2; //For command and point pair } else if (words[stringIndex].equals("L")) { String[] points = split(words[stringIndex+1], ','); newState.saveSVGData(words[stringIndex], float(points[0]), float(points[1])); stringIndex += 2; //For command and point pair } else if (words[stringIndex].equals("C")) { String[] pointOne = split(words[stringIndex+1], ','); String[] pointTwo = split(words[stringIndex+2], ','); String[] pointThree = split(words[stringIndex+3], ','); newState.saveSVGData(words[stringIndex], float(pointOne[0]), float(pointOne[1]), float(pointTwo[0]), float(pointTwo[1]), float(pointThree[0]), float(pointThree[1])); stringIndex += 4; //For command and 3 point pairs } else if (words[stringIndex].equals("z")) { newState.saveSVGData(words[stringIndex]); stringIndex += 1; //For command and point pair } else { stringIndex++; println("Error loading data"); } } stateArray.add(newState); index++; } //Move US to center of screen (hardcoded cause translate doesnt work) xOffset = width/7; yOffset = height/15; } void loadCovidData() { // Load CSV file into a Table objects Table covidTable = new Table(); covidTable = loadTable("united_states_covid19_cases_and_deaths_by_state.csv", "csv"); //Loop through data columns for (int j = 1; j < 13; j++) { Table subTable = new Table(); subTable.addColumn("State"); subTable.addColumn("Data"); //Loop through all states for (int i = 1; i < covidTable.getRowCount(); i++) { TableRow newRow = subTable.addRow(); newRow.setString("State", covidTable.getRow(i).getString(0)); //Check for no data points if (covidTable.getRow(i).getString(j).equals("null") || covidTable.getRow(i).getFloat(j) == 0) { newRow.setFloat("Data", -1); } else { newRow.setFloat("Data", covidTable.getRow(i).getFloat(j)); } } Data newDataPoint = new Data(covidTable.getRow(0).getString(j), subTable); //Create new datapoint dataArray.add(newDataPoint); } } void draw() { background(0, 0, 100); //Draw states for (State state : stateArray) { float stateData = dataArray.get(col).returnData(state.getState()); color stateColor = dataArray.get(col).returnColor(stateData); state.drawState(xOffset, yOffset, stateColor); } //Write title textAlign(CENTER); fill(0); textSize(50); text(dataArray.get(col).getHeader(), width/2, 50); //Draw scale dataArray.get(col).drawScale(width - width/5,height - height/8); } //Change what data is being displayed void keyPressed() { if (keyCode == LEFT) { if (col == 0) { col = 11; } else { col--; } } if (keyCode == RIGHT) { col = (col+1)%12; } }
class Data { String header; //What is the data of Table dataTable; //Table of the data values with the state identifier float min = -1; float max = -1; //min and max val variables int scaleWidth = 50; //how wide is the scale int hue = 1; //HSB hue value, 0-100 Data(String header, Table data) { this.header = header; //Data type dataTable = data; //Find max, min for (int i = 0; i < dataTable.getRowCount(); i++) { //If Max if (max == -1 || dataTable.getFloat(i, "Data") > max) { max = dataTable.getFloat(i, "Data"); } //If min if (min == -1 || dataTable.getFloat(i, "Data") < min) { min = dataTable.getFloat(i, "Data"); } } } //Returns float associated with state float returnData(String state) { return dataTable.matchRow(state, "State").getFloat("Data"); } String getHeader() { return header; } //Return the mapped color color returnColor(float data) { //If no data if (data == -1) { colorMode(HSB, 100); return color(100, 100, 0); } else { colorMode(HSB, 100); float s = map(data, min, max, 0, 100); return color(hue, s, 100); } } //Draw the scale being used void drawScale(int x, int y) { colorMode(HSB, 100); //Draw null data fill(color(100, 100, 0)); rect(x, y, scaleWidth, scaleWidth); //Draw min fill(returnColor(min)); rect(x+scaleWidth, y, scaleWidth, scaleWidth); //Draw 1/3 fill(returnColor(min+((max-min) * 0.333))); rect(x+scaleWidth*2, y, scaleWidth, scaleWidth); //Draw 2/3 fill(returnColor(min+((max-min) * 0.666))); rect(x+scaleWidth*3, y, scaleWidth, scaleWidth); //Draw max fill(returnColor(max)); rect(x+scaleWidth*4, y, scaleWidth, scaleWidth); //Text fill(color(100, 100, 0)); textAlign(CENTER); textSize(10); text("No data", x+(scaleWidth/2), y + (1.2 * scaleWidth)); text(int(min), x+(scaleWidth*1)+(scaleWidth/2), y + (1.2 * scaleWidth)); text(int(min+((max-min) * 0.333)), x+(scaleWidth*2)+(scaleWidth/2), y + (1.2 * scaleWidth)); text(int(min+((max-min) * 0.666)), x+(scaleWidth*3)+(scaleWidth/2), y + (1.2 * scaleWidth)); text(int(max), x+(scaleWidth*4)+(scaleWidth/2), y + (1.2 * scaleWidth)); } }
class State { String code; //2 Character code to identify state Table data = new Table(); //Table of svg command at points //Default constructor State() { code = ""; //Populate table columns data.addColumn("Command"); data.addColumn("x1"); data.addColumn("y1"); data.addColumn("x2"); data.addColumn("y2"); data.addColumn("x3"); data.addColumn("y3"); } State(String code) { this.code = code; //Populate table columns data.addColumn("Command"); data.addColumn("x1"); data.addColumn("y1"); data.addColumn("x2"); data.addColumn("y2"); data.addColumn("x3"); data.addColumn("y3"); } //Getter and setter for state code String getCode() { return code; } void setCode(String code) { this.code = code; } //Passed a stiring with an SVG command and series of points, processes them into the table, three methods store different amound of cords void saveSVGData(String command) { TableRow newRow = data.addRow(); newRow.setString("Command", command); } void saveSVGData(String command, float x1, float y1) { TableRow newRow = data.addRow(); newRow.setString("Command", command); newRow.setFloat("x1", x1); newRow.setFloat("y1", y1); } void saveSVGData(String command, float x1, float y1, float x2, float y2) { TableRow newRow = data.addRow(); newRow.setString("Command", command); newRow.setFloat("x1", x1); newRow.setFloat("y1", y1); newRow.setFloat("x2", x2); newRow.setFloat("y2", y2); } void saveSVGData(String command, float x1, float y1, float x2, float y2, float x3, float y3) { TableRow newRow = data.addRow(); newRow.setString("Command", command); newRow.setFloat("x1", x1); newRow.setFloat("y1", y1); newRow.setFloat("x2", x2); newRow.setFloat("y2", y2); newRow.setFloat("x3", x3); newRow.setFloat("y3", y3); } //Draw state from given SVG path data void drawState(int xOffset, int yOffset, color fillColor) { fill(fillColor); //Iterate through data table and draw shape based on symbols. for (TableRow row : data.rows()) { //Get command //Move if(row.getString("Command").equals("M")){ beginShape(); vertex(row.getFloat("x1")+xOffset, row.getFloat("y1")+yOffset); } //Line else if(row.getString("Command").equals("L")){ vertex(row.getFloat("x1")+xOffset, row.getFloat("y1")+yOffset); } //Bezier Curve else if(row.getString("Command").equals("C")){ bezierVertex(row.getFloat("x1")+xOffset, row.getFloat("y1")+yOffset, row.getFloat("x2")+xOffset, row.getFloat("y2")+yOffset, row.getFloat("x3")+xOffset, row.getFloat("y3")+yOffset); } //End shape else if(row.getString("Command").equals("z")){ endShape(); } } } //getter for state name, no setter as wont change String getState(){ return code; } }
Data Distortion / Misrepresentation:
This data is all taken from the CDC so can be considered fairly reliable in my opinion. The issue with any data visualization is that misrepresentation is fairly easy. For starters, some states don’t report on the same data or with the same accuracy, hence why in the last image “Confirmed Deaths”, there are several states blacked out with no data points. Furthermore, color can easily distort the severity of a situation. With a simple mapping from the min to max, in the map “Death Rate per 100k in the Last 7 Days”, the state of Ohio looks much more severe than the rest of the country, when in reality its value is at 5, only slightly higher than most other states around 3. The type of data being displayed is also important. In the total case and total death maps, large and high population states appear far more severe. This may be in terms of quantity, but in reality per 100k is a much more useful metric. Would a city with 5 cases and 10 residents really be considered safer to be in that a city with 10 times as many cases but 1000 times as many residents?
Great job on processing the SVG Cole! The visualization choices are clear and easy to understand.