Public
Edited
Feb 15, 2023
Importers
Insert cell
# We Are The Data: Geometric People Prototype and Design Explorations
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Force directed layout (defaults to using phylotaxxis)
weePeopleChartForce = function(data, renderer, chartWidth=defaultWidth, chartHeight=defaultHeight){
const scale = chartWidth / Math.sqrt(data.length) / 23;
// Create chart
const svg = d3.select(DOM.svg(chartWidth, chartHeight));
const simulation = d3.forceSimulation(data)
const people = svg.selectAll('g')
.data(data)
.enter()
.append('g')
.attr("transform", d => `translate(${d.x*scale + chartWidth/2},
${d.y*scale + chartHeight/2})`);

// Simple tooltips with race + Fenty codes
people.append("svg:title")
.text(d => d.race + "|" + d.fenty);
renderer(people, chartWidth, chartHeight);
return svg.node()
}
Insert cell
weePeopleGrid = function(data, renderer, sortCategory=null, chartWidth=defaultWidth, chartHeight=defaultHeight){
const scale = chartWidth / Math.sqrt(data.length) / 23;
// Create chart
let copyOfData = copyArray(data); // Create a copy so sort doesn't alter other visualizations of the same dataset.
if(sortCategory)
copyOfData = copyOfData.sort(function(a,b){return a[sortCategory] > b[sortCategory] ? 1 : -1});
const numRows = Math.floor(copyOfData.length / 10);
const numColumns = Math.floor(copyOfData.length / numRows);
const svg = d3.select(DOM.svg(chartWidth, chartHeight));
const people = svg.selectAll('g')
.data(copyOfData)
.enter()
.append('g')
.attr("transform", function(d,i){
let x = (i%numRows+1)*(chartWidth/(numColumns+2));
let y = (Math.floor(i/numRows)+1)*(chartHeight/numRows+2);
return `translate(${x},${y})`});

// Simple tooltips with race + Fenty codes
people.append("svg:title")
.text(d => d.race + "|" + d.fenty);
renderer(people, chartWidth, chartHeight);
return svg.node()
}
Insert cell
weePeoplePlusHistogram = function(title="",data, weeRenderer, chartWidth=defaultWidth, chartHeight=defaultHeight){
// Generate a wee people chart (D3) using the data and renderer provided
let weeChart = weePeopleChartForce(data, weeRenderer, chartWidth, chartHeight);

// Create a holder div and drop the wee people chart into it
let chartsHolder = html`<div style="display:inline-flex;">${weeChart}</div>`;

// Collect a set of the dominantColor codes (renderers should write these back to the data objects)
let uniqueColors = [... new Set(data.map(v=>v.dominantColor))].sort();
// Instantiate the histogram (Vega Lite), then add to the holder once it renders
let histogram = vl.markBar()
.data(data)
.height(chartHeight-50)
.width(100)
.encode(
vl.y().count().title("number of people"),
vl.x().fieldN("race").sort("-y"),
vl.color().field("dominantColor").scale({"range":uniqueColors}).legend(null),
vl.stroke().field("dominantColor").scale({"range":uniqueColors}).legend(null),
vl.tooltip("data").fieldN("dominantColor")
)
.render({renderer: 'svg'})
.then(chart => {
chart.style.cssText += "display:inline;";
chartsHolder.prepend(chart);
});

let outerHolder = html`<div style="display:inline-block;"><h4>${title}</h4></div>`
outerHolder.append(chartsHolder);
return outerHolder;
}
Insert cell
Insert cell
// Renders data points as circles, takes two functions that should return:
// - a fill color (will be set as 'dominantColor back on the data object)
// - a stroke
function circleRenderer(fillFunction=(d=>"#bbb"),strokeFunction=(d=>null)){
return function(people, width, height) {
let numPeople = people._groups[0].length;
let headSize = Math.sqrt(width * height / numPeople / 30);
people.append("circle")
.attr("stroke", strokeFunction)
.attr("fill", d=>d.dominantColor=fillFunction(d)) //Also adds fill as 'dominantColor' back on data object
.attr("r", 1.5*headSize)
.attr("transform", "translate(0,"+0.5*headSize+")");
}
}
Insert cell
// Renders data points as symbols, takes three functions that should return:
// - a d3 symbol
// - a fill color (will be set as 'dominantColor back on the data object)
// - a stroke color
function symbolRenderer(symbolFunction=(d=>d3.symbols[0]),
fillFunction=(d=>"#fff"),
strokeFunction=(d=>"#000")){
return function(people, width, height) {
let numPeople = people._groups[0].length;
let headSize = Math.sqrt(width * height / numPeople / 30);
people.append('path')
.attr('d',d=> d3.symbol().type(symbolFunction(d)).size(headSize*15)())
.attr("fill", d=>d.dominantColor=fillFunction(d)) //Also adds fill as 'dominantColor' back on data object
.attr('stroke',strokeFunction);
}
}
Insert cell
// Renders data points as simple geometric people, takes three functions that should return:
// - a skin color (will be set as 'dominantColor back on the data object)
// - a hair color
// - a clothing color
function geometricPeopleRenderer(skinColorFunction=(d=>"#bbb"),
hairColorFunction=(d=>"#666"),
clothesColorFunction=(d=>"#999")){
return function(people, width, height) {
// Internal variables
let numPeople = people._groups[0].length;
let headSize = Math.sqrt(width * height / numPeople / 30);
let clipRand = Math.random(); //random number to uniquely identify this chart (for clipping masks)
// clipping path
people.append("clipPath")
.attr("id", d => "clip-" + d.id + "-" + clipRand)
.append("circle")
.attr("r", 2*headSize)
.attr("transform", "translate(0,0)");
// outside circle
// people.append("circle")
// .attr("clip-path", d => "url(#clip-" + d.id + "-" + clipRand + ")")
// .attr("stroke", "#eee")
// .attr("fill", "#fff")
// .attr("r", 1.9*headSize)
// .attr("transform", "translate(0,"+0.5*headSize+")");
//body
const body = d3.path();
body.moveTo(-1.25*headSize, 2.5*headSize);
body.bezierCurveTo(-1.25*headSize, 0, 1.25*headSize, 0, 1.25*headSize, 2.5*headSize);
people.append("path")
.attr("clip-path", d => "url(#clip-" + d.id + "-" + clipRand + ")")
.attr("fill", clothesColorFunction)
.attr("d", body);
//hair
people.append("circle")
.attr("fill", hairColorFunction)
.attr("r", 1.1*headSize);
//head
people.append("circle")
.attr("fill", d=>d.dominantColor=skinColorFunction(d)) //+adds skin color as 'dominantColor' back on data object
.attr("r", headSize)
.attr("transform", "translate(0,"+0.1*headSize+")");
}
}
Insert cell
Insert cell
simulatedUSCensusPeople = {
let simulatedUSCensusPeople = [];

for(let i=0; i < numPeeps; i++){
// Sample US Census Data to determine race
const randRoll = Math.random() * raceDataUSlookup[0].max;
let r = 0;
while(randRoll <= raceDataUSlookup[r].min) r++;
let race = raceDataUSlookup[r].race;

// Assign a Fitzpatrick shade based on assigned race category
let fitzShade = fitzpatrick_dict[race];

// Assign a Fenty shade based on assigned race category
// ⚠️ Contains some fairly arbitrary/problematic binning ⚠️
let allFentyShades = Object.keys(fenty_hex_dict);
// By default, randomly pick any Fenty share (all categories other than B & W)
let fentyShade = parseInt(allFentyShades[Math.floor(Math.random()*allFentyShades.length)]);
// If White, pick only 100 and 200-level shades
let whiteFentyShades = allFentyShades.filter(shade=>shade[0]<=2).map(shade=>parseInt(shade));
if (race =='White') fentyShade = whiteFentyShades[Math.floor(Math.random()*whiteFentyShades.length)];
// If Black, pick only 300- and 400-level shades
let blackFentyShadesExperiment = allFentyShades.filter(shade=>shade[0]>2).map(shade=>parseInt(shade));
if (race =='Black') fentyShade = blackFentyShadesExperiment[Math.floor(Math.random()*blackFentyShadesExperiment.length)];
// Also assigne each data point a random Fitzpatrick shade and Fenty shade
let randomFitz = Math.floor(Math.random()*fitzpatrick_hex.length);
let randomFenty = parseInt(allFentyShades[Math.floor(Math.random()*allFentyShades.length)]);

// Construct a data item for each person we'll visualize
simulatedUSCensusPeople.push({id: i,
race: race, // census race category
fitz: fitzShade, //fitpatrick color assigned based on the race category
rfitz:randomFitz, //a random fitzpatrick color
fenty: fentyShade, //fenty assigned based on race category
rfenty:randomFenty}); //a random fenty color
}
return randomizeArray(simulatedUSCensusPeople);
}
Insert cell
// Copy of simulated data with more extreme Fenty assumptions
// ⚠️ Contains some fairly arbitrary/problematic binning ⚠️
simulatedUSCensusPeopleExtremeFenty = {
const copyOfSimulated = copyArray(simulatedUSCensusPeople);

for(let i=0; i < copyOfSimulated.length; i++){
let person = copyOfSimulated[i];
let allFentyShades = Object.keys(fenty_hex_dict);
// If White, pick only 100-level shades
let whiteFentyShades = allFentyShades.filter(shade=>shade[0]<=1).map(shade=>parseInt(shade));
if (person.race =='White')
person.fenty = whiteFentyShades[Math.floor(Math.random()*whiteFentyShades.length)];
// If Black, pick only 400-level shades
let blackFentyShadesExperiment = allFentyShades.filter(shade=>shade[0]>3).map(shade=>parseInt(shade));
if (person.race =='Black')
person.fenty = blackFentyShadesExperiment[Math.floor(Math.random()*blackFentyShadesExperiment.length)];
}
return copyOfSimulated;
}
Insert cell
Insert cell
fenty_hex = Object.values(fenty_hex_dict);
Insert cell
Insert cell
Insert cell
randomHairColor = (d => hair_hex[Math.floor(Math.random()*hair_hex.length)])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
copyArray = function(arrayToCopy){
return JSON.parse(JSON.stringify(arrayToCopy));
}
Insert cell
function randomizeArray(randArray){
for(var j = randArray.length - 1; j > 0; j--) {
var k = Math.floor(Math.random() * (j + 1));
var temp = randArray[j];
randArray[j] = randArray[k];
randArray[k] = temp;
}
return randArray;
}
Insert cell
// Extract just the set of color codes used by this set of items (for Vega Lite bar chart coloring)
getUniqueCodes = function(items, key, lookup){
let uniques = [... new Set(items.map(v=>v[key]).sort())];
if(lookup) return uniques.map(v=>lookup[v]);
else return uniques;
}
Insert cell
// Extract just the fenty codes for the current set of items (for Vega Lite bar chart coloring)
debugUniqueFentyCodes = getUniqueCodes(simulatedUSCensusPeople, 'fenty', fenty_hex_dict)
Insert cell
import { vl } from "@vega/vega-lite-api"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more