Data Team

Building an interactive graphic for teacher salaries
And why Chalkbeat maintains a code-first visualization toolkit

Coming into this summer, I knew I wanted to work with Chalkbeat’s dailygraphics rig. I thought it would be a great opportunity to learn about building visualizations with code, since I’d primarily used tools like Tableau, Flourish, and Datawrapper in the past.

Building custom visualizations isn’t always a good investment compared to those kinds of out-of-the-box tools. For a lot of local stories, it makes sense to stick with Datawrapper and not reinvent the wheel. However, for stories where we had a lot of data series, like changes in average NAEP scores for 13-year-olds by subject and racial or ethnic group, then I worked in the rig to have more control over axes, colors, and small multiple grouping.

One story in particular called for something more individualized. At the end of June, Memphis reporter Laura Testino asked the data team to help her visualize changes to how Memphis Shelby County Schools pays its teachers. The district moved from a 31-step payscale to an 18-step scale, with new brackets for teachers with an Ed.S. degree.

The goal was to have a graphic that was teacher-friendly and contained all the pertinent information. We decided to make an interactive where teachers could select their current salary range, and the graphic would return the new salary range. Rather than use one of our pre-existing dailygraphics templates, we started from scratch using JavaScript and lit-html. The latter lets us create a strong link between JavaScript values and the structure or content of the page, where updating one changes the other.

The basics

The first thing we did was set up a box for teachers to input their salary and indicate what kind of degree they had. Both inputs updated a state object with values for the numerical salary and pay ladder. A separate variable for Ed.S. degrees was controlled by a checkbox, and could be true or false. By using a setState function in the template code, changes would update the values and then re-render the template based on the new state.

<label for="salary-input">Salary:</label>
<input
    id="salary-input"
    .value=${state.salary}
    @input=${e => setState("salary", Number(e.target.value))}
>
<label for="degree-select">Degree:</label>
<select
  id="degree-select"
  .value=${state.oldDegree}
  @input=${e => setState("oldDegree", e.target.value)}
>
    <option value="bach">B.A.</option>
    <option value="mast">M.A.</option>
    <option value="doc">Doctorate</option>
</select>
<input 
  id="eds-check"
  type="checkbox"
  .value=${state.eds}
  @input=${e => setState("eds", e.target.checked)}>
<label for="eds-check">You have an Ed.S.?</label>

The graphic takes the old salary, then displays the old salary band and new salary band. A salaryMatch function retrieves numbers from the data provided to us by Memphis Shelby County Schools, and then tries to assign the salary to a range by checking which step was either less than or equal to the input.

function salaryMatch() {
  var oldDegree = "old_" + state.oldDegree;
  var newDegree = "new_" + state.oldDegree;
  if (state.eds && state.oldDegree != "doc"){
    newDegree = "new_eds";
  }
  var first = window.DATA[0];
  var salary = Number(state.salary.replace(/[\$,]/g, ""));
  let found = window.DATA.findLast(d => d[oldDegree] <= salary) || first;
  let previous = {
    degree: oldDegree,
    step: found.old_step,
    salary: found[oldDegree]
  };
  let current = {
    degree: newDegree,
    step: found.new_step,
    salary: found[newDegree]
  };
  let raise = current.salary - previous.salary;
  return {
    previous,
    current,
    raise,
    row: found
  };
}

The oldDegree and newDegree variables match the headers in the dataset, which have names like old_bach (aka old pay scale for teachers with bachelor’s degrees) and new_bach (new pay scale!). If someone with a bachelor’s or master’s degree indicates that they have an Ed.S., the Ed.S. salary overrides the salary ranges for those degrees, but it doesn’t override a doctorate.

The output from this function is returned to the user like this:

<h3>Your result:</h3>
<div class="result-grid">
  <div class="old">
    <h4>${str("old_step")}: ${previous.step}</h4>
    $${cash(previous.salary)}
  </div>
  <span>&raquo;</span>
  <div class="raise">
    + $${cash(result.raise)}<br>
    (${(result.raise / previous.salary * 100).toFixed(1)}%)
  </div>
  <span>&raquo;</span>
  <div class="new">
    <h4>${str("new_step")}: ${current.step}</h4>
    $${cash(current.salary)}
  </div>
</div>

So now, our hypothetical teacher knows what their new salary band is. But how much of a raise do they get? And how does that compare to other salary bands?

Iteration!

If you want to show each row of a dataset on a page, you have to iterate, or loop through the data. In lit-html, this is done with the map() function.

In the early stages of this graphic, we iterated through the old salary ranges and displayed them as an ordered list in addition to the user’s specific salary band.

<ol>
  ${window.DATA.map(i => html`
    <li>
      B.A. $${dollars.format(i.old_bach)} |
      M.A. $${dollars.format(i.old_mast)} | 
      Doctorate $${dollars.format(i.old_doc)}
    </li>`)}
</ol>

This laid the groundwork for us to make a bar chart that showed the amount of raise for each salary band, calculated by subtracting the old salary band minimum from the new band minimum. Each row of the dataset gets a bar scaled so that the maximum raise takes up 100% of its grid container.

We wanted the graphic to emphasize the old salary band that matched the user’s salary, highlighting the resulting raise. In code, this translates to if-then logic; if the current salary variable falls within a certain range, then the graphic will usually emphasize that range. Conditional ternary operators in lit-html change the CSS style when the salary range matches.

Because we’re using a CSS grid to lay out our bar chart and its labels, we assign the “current” class in multiple places to bold text and highlight the bar. We also add a visually-hidden label to the user’s selected salary band for accessibility purposes, since the bold won’t be visible in most screen readers.

${bands.map(b => {
  var highlight = b.current ? "current" : "not";
  return html`
  <div class="step ${highlight}">
    ${b.step}
    ${b.current ? html`<span class="sr-only">(your step)</span>` : ""}
  </div>
  <div class="bar-container ${highlight}">
    <div class="bar"
      style="width: ${b.raise / max * 100}%">
    </div>
  </div>
  <div class="amount ${highlight}">$${cash(b.raise)}</div>
  <div class="percent ${highlight}">${b.per_change}%</div>
`})}

And that’s it!

Takeaways

Although I had some familiarity with JavaScript before this, and I knew how important it was in web development, working on this graphic showed me how to actually develop an interactive graphic and understand the different moving parts.

Tools like Datawrapper let users look up information in a table or highlight specific areas of a graph, but they don’t make it easy to change what you’re looking at based on what a user wants, or to find something that isn’t explicitly included in the dataset–both of which are key to show a specific salary within a range. So even though it’s more difficult to use, this is why we have the rig: to have a bit more control over how we present data to readers, and to keep me from blowing a gasket when I can’t get Datawrapper to cooperate.

Working with a reporter to understand the context for these changes and with senior data editor Thomas Wilburn made it possible to develop a graphic that was both visually appealing and useful to readers.