Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation


Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

              html { height: 100%; }
body {
  align-items: center;
  background-color: #1D1E22;
  display: flex;
  justify-content: center;
  height: 100%;

svg {
  background-color: #fff;
  margin: 20px;
  padding: 100px 20px 50px 50px;

#title {
  font-size: 24px;

div.tooltip {
  background: rgba(112, 127, 140, .85);
  border: none;
  border-radius: 2px;
  color: #fff;
  font-size: 18px;
  padding: 10px 20px;
  pointer-events: none;
  position: absolute;
  text-align: center;

.states {
  fill: none;
              // store data uri's
const usEducationData = ''
const usData = ''

// set up svg
const w = 960
const h = 600
const svg ='body')
              .attr('width', w)
              .attr('height', h)

// set up chart title
   .attr('transform', `translate(${w/2}, -60)`)
   .attr('class', 'title')
   .attr('id', 'title')
   .style('text-anchor', 'middle')
   .text('United States Educational Attainment')

// set up chart subtitle
   .attr('transform', `translate(${w/2}, -30)`)
   .attr('class', 'description')
   .attr('id', 'description')
   .style('text-anchor', 'middle')
   .text('Percentage of adults age 25 and older with a bachelor\'s degree or higher (2010-2014)')

// append div for use as tooltip
const tooltip ='body')
                  .attr('class', 'tooltip')
                  .attr('id', 'tooltip')
                  .style('opacity', 0)

// set up geographic path generator
const path = d3.geoPath()

// queue asynchronous tasks (load json files and pass to await callback)
  .defer(d3.json, usData)
  .defer(d3.json, usEducationData)

// set up await callback
function ready(error, us, education) {
  // throw if error
  if (error) throw error
  // get min and max education figures
  const eduThreshold = d3.extent( => item.bachelorsOrHigher))
  // set up x scale for legend
  const xScale = d3.scaleLinear()
                   .rangeRound([600, 860])
  // set up color scale for legend and map
  const color = d3.scaleThreshold()
                  .domain(d3.range(eduThreshold[0], eduThreshold[1], (eduThreshold[1] - eduThreshold[0]) / 8))

  // set up legend
  const g = svg.append('g')
               .attr('id', 'legend')
               .attr('class', 'key')
               .attr('transform', 'translate(0, 40)')
  // add legend squares
   // map over color range (e.g. ["#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#a50f15", "#67000d"])
   .data(color.range().map(d => { // addapted from
     d = color.invertExtent(d) // compute the domain values corresponding to the color range
     if (d[0] == undefined) d[0] = eduThreshold[0] // account for first invert (e.g. [undefined, 2.6])
     if (d[1] == undefined) d[1] = eduThreshold[1] // account for last invert (e.g. [66.0375, undefined])
     return d
   .attr('height', 8)
   .attr('x', d => xScale(d[0]))
   .attr('width', d => xScale(d[1]) - xScale(d[0]))
   .attr('fill', d => color(d[0]))

  // add legend title
   .attr('x', xScale.range()[0])
   .attr('y', -6)
   .attr('fill', '#000')
   .attr('text-anchor', 'start')
   .attr('font-weight', 'bold')
   .text('Education Rate')

  // add legend axis
           .tickFormat(d => Math.round(d) + '%')
   .select('.domain') // select and remove domain path (horizontal axis line)
  // set up counties
     .data(topojson.feature(us, us.objects.counties).features) // use topojson.feature to convert TopoJSON to GeoJSON and select features array
     .attr('class', 'county')
     .attr('fill', ({id}) => {
       const county = lookupCounty(id, education) // filter for county data (refer to function)
       if (county[0]) {
         return color(county[0].bachelorsOrHigher) // if a county match was made, return the color associated with the level of education using the color scale
       // no match
       return color(0)
     .attr('d', path) // use path (geoPath) to define 'd' attribute of 'path' element
     // set up mouseover to modify tooltip div
     .attr('data-fips', ({id}) => {
       const county = lookupCounty(id, education)
       return county[0].fips
     .attr('data-education', ({id}) => {
       const county = lookupCounty(id, education)
       return county[0].bachelorsOrHigher
     // set up mouseover to modify tooltip div
     .on('mouseover', ({id}) => {
              .style('opacity', 0.9)
       tooltip.html(() => {
              const county = lookupCounty(id, education) // filter for county data (refer to function)
              if (county[0]) {
                return `${county[0].area_name}, ${county[0].state}: ${county[0].bachelorsOrHigher}%` // if a county match was made, return the county data formatted for the tooltip
              // return empty string if no match
              return ''})
              .attr('data-education', () => {
                const county = lookupCounty(id, education)
                return county[0].bachelorsOrHigher
              .style('left', (d3.event.pageX) + 'px')
              .style('top', (d3.event.pageY) + 'px')
     // set up mouseout to modify tooltip div
     .on('mouseout', () => {
              .style('opacity', 0)
  // set up states
     .datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b)) // use topojson.mesh to mesh TopoJSON geometry and convert to GeoJSON lines
     .attr('class', 'states')
     .attr('d', path) // set 'd' attribute to the geoPath()

// set up function to filter education array for matching id/fips (i.e. match the correct county geometry with it's education data)
function lookupCounty(countyID, eduData) {
  return eduData.filter(obj => obj.fips == countyID)
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.