Published unlisted
Edited
Jul 6, 2022
Insert cell
# Multiple Suns
Insert cell
d3=require('d3')
Insert cell
_ = require("lodash")
Insert cell
d3c=require('d3-collection')
Insert cell
jsonURL = "https://gist.githubusercontent.com/ewilzon/568098ac32278c620da2623dbe063ee5/raw/dd84e228368ed2c6d53ecdef9b167d739c3009d6/DOL_2019_Occupation.json"
Insert cell
data = d3.json(jsonURL)
.then(data => _.values(data));
Insert cell
filteredData = data.filter(function(d){
const percOfWomenString = d.runningTotal.replace(/%/,'')
const percOfWomen = parseInt(percOfWomenString)
return d.payGap > 0 //&& percOfWomen < 81
}).slice().sort((a, b) => d3.ascending(a.industryGrouping, b.industryGrouping) || d3.descending(a.payGap, b.payGap ))
Insert cell
{
//------------------------------------------------------------------------------------
//// STEP 1 - Initialize constants to be used later
const width=1500;
const height=4000;
const sunR = 50;// sun radius
const dotSpacing = sunR/15;
// colors
const charcoal = "#545253";
const scaleColor = "#ebe9e3";
const labelColor = "#545253";
//------------------------------------------------------------------------------------
//// STEP 2 - Calculations
const occCount = filteredData.length
const gapMinMax = d3.extent(filteredData, d => d.payGap); // ".extent" returns [min, max] of a dataset
// console.log(gapMinMax) // returns our min/max gap %
// Creating a function to calculate the ray size based on the pay gap %
const maxRayLength = sunR*5.5
const rayEndPoint = d3.scaleLinear()
.domain(gapMinMax) // "domain" is the input. This is our actual min and max of the dataset
.range([sunR*1.3,maxRayLength]); // "range" is the output of our choosing

// Find distinct industries
const distinctIndustries = [...new Set(filteredData.map(d=> d.industryGrouping))]
// console.log(distinctIndustries)

// Creating a function that takes the industry and returns an index
var findIndustryIndex = d3.scaleBand()
.domain(distinctIndustries)
.range([0,distinctIndustries.length])

var groupIndex = d3.scaleBand()
.domain(distinctIndustries)
.range([0,distinctIndustries.length])
const myNest = d3c.nest() //creating a new nested dataset object
.key(item => item.industryGrouping) //assigning the nested key to be the majorGroup value
.object(filteredData) //passing our data thru
console.log(myNest) //returns object with 34 items (major groups) and within them are the occupation objects
//------------------------------------------------------------------------------------
//// STEP 3 - Tranform data to prep for visual elements
const sunBuildData = _.map(myNest, (occs,i)=> {
const occCount = occs.length
const womenShare = occs.womenCount / occs.totalCount
var sunAngle = 180
if(occCount < 13) {sunAngle = 90}
if(occCount > 30) {sunAngle = 360}
return {
xPos: (groupIndex(i) % 2 ) * 450,
yPos: Math.floor(groupIndex(i) / 2) * 400, //takes the major group name, outputs y position
groupName: i,
occCount,
industryIndex: findIndustryIndex(occs.industryGrouping),
sunAngle,
rays: _.map(occs, (d,i) => {
var OccName = d.Occupation
var numDots = Math.round(d.dollDiff/1000) // 1 dot per $1k difference, used below in dots variable
var dotLength = numDots*dotSpacing
var thisRayLength = Math.round(rayEndPoint(d.payGap)) - dotLength // using our "rayLength" function
const thisAngle = (-sunAngle / occCount) * i - 90
return {
OccName,
thisAngle,
textAngle: (-sunAngle / occCount) * i - 1,
thisRayLength,
dots: _.times(numDots, i => {
var dotShapeIndicator = .5
if ((i+1)%5 ==0) { dotShapeIndicator = 1 }
return { dotPos: thisRayLength + dotSpacing *(i+1), dotShapeIndicator, thisAngle}
})
}
})
}
})
console.log(sunBuildData)
//------------------------------------------------------------------------------------
//// STEP 4 - Drawing elements
const svg = DOM.svg(width,height);

const myCanvas = d3.select(svg)
.append("g")
.attr("transform", `translate(${300},${300})`)
var myDataDrawing = myCanvas.selectAll('g')
.data(sunBuildData) // take our data
.enter() // enter it into the svg
.append('g') // make a "group" for each occupation
.attr('class',`industryGroupData`) // labelling these groups
.attr("transform", occs =>`translate(${occs.xPos},${occs.yPos})`)
// Colored circle
myCanvas.selectAll('g.industryGroupData')
.append('circle')
.attr('cx', `0`)
.attr('cy', `0`)
.attr("r", `${rayEndPoint(0)}`)
.attr("fill",`yellow`)
myCanvas.selectAll('g.industryGroupData')
.append('text')
.style("font-size","8px")
.attr("fill", `${charcoal}`)
.attr("font-family", "Sans-Serif")
.text(occs =>`${occs.groupName.toUpperCase()}`)
.attr("transform", `translate(${0},${20})`)
.style("text-anchor", "middle")
// Scale circles
myCanvas.selectAll('g.industryGroupData')
.append('circle')
.attr('cx', `0`)
.attr('cy', `0`)
.attr("r", `${rayEndPoint(10)}`)
.attr("stroke", `${scaleColor}`)
.attr("fill", 'none')
myCanvas.selectAll('g.industryGroupData')
.append('circle')
.attr('cx', `0`)
.attr('cy', `0`)
.attr("r", d => `${rayEndPoint(20)}`)
.attr("stroke", `${scaleColor}`)
.attr("fill", 'none')

myCanvas.selectAll('g.industryGroupData')
.append('circle')
.attr('cx', `0`)
.attr('cy', `0`)
.attr("r", d => `${rayEndPoint(30)}`)
.attr("stroke", `${scaleColor}`)
.attr("fill", 'none')

// Going down a layer in the nest to get occupation data
var mySuns = myDataDrawing.selectAll('g.industryGroupData')
.data(occs => occs.rays)
.enter()
.append('g') // make a "group" for each occupation
.attr('class',`occData`) // labelling these groups

myCanvas.selectAll('g.occData')
.append('path') // make a "path" for each occupation
.attr('d',d=> `M0,${sunR} L0,${d.thisRayLength}`)
.attr("transform", d => `rotate(${d.thisAngle})`)
.attr('stroke', d => `${charcoal}`)
.attr('fill', `none`)
myCanvas.selectAll('g.occData')
.append('text')
.text(d =>`${d.OccName.toUpperCase()}`)
.attr('x', d=> `${sunR+1}`)
.attr('y', `0`)
.style("font-size","4.5px")
.attr("fill", `${charcoal}`)
.attr("font-family", "Sans-Serif")
.attr("transform", d => `rotate(${d.textAngle})`)

// Going down 1 more layer in the nest to get dots data
mySuns.selectAll('g.occData')
.data(d => d.dots)
.enter()
.append('g') // make a "group" for each occupation
.attr('class',`dotsData`) // labelling these groups
// dots for dollar difference
myCanvas.selectAll('g.dotsData')
.append('circle')
.attr('cx', `0`)
.attr('cy', d => `${d.dotPos}`)
.attr("r", d => `${d.dotShapeIndicator}`)
.attr("fill", `${charcoal}`)
.attr("transform", d => `rotate( ${d.thisAngle})`);

return svg
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more