Sometimes it can be useful to change the way that we’re looking at the same data to be able to present other insights as to what the data could mean. Previously, I had use hierarchy as the inner ring and then drilled down using LAID OFF status. Here, I’ve done the opposite:

See the code:
<script type="module">
    import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
  
    const data = await d3.tsv("https://jernwerber.dev/static/clc-laid-off-2024-01-20.tsv");
  
    const groupedData = d3.group(data, d => d.LaidOff);
    
    const svgWidth = 700;
    const svgHeight = 500;
    
    const outerRadius = 100;
    const innerRadius = 50;
    
    const shiftX = 0;
    const shiftY = 40;
    
    const baseColor = "crimson";
    const backgroundColor = "none";
    
    const pieGap = 0.04;
    const pieRotate = 0 * Math.PI;
    
    const CS = {
      HIERARCHY : {
        0 : "non-managers",
        1 : "managers",
        2 : "directors",
        3 : "C-level",
        4 : "CEO"
      }
    }
    
    const d3svg = d3.create("svg")
        .attr("aria-labelledby","chart-title")
        // .attr("width", svgWidth)
        // .attr("height", svgHeight)
        .attr("viewBox", [-svgWidth / 2, -svgHeight / 2, svgWidth, svgHeight])
        .style("background-color", backgroundColor)
        .style("margin-inline","auto")
    ;
    
    d3svg.append("text")
        .attr("id", "chart-title")
        .attr("text-anchor", "middle")
        .attr("transform",`translate(0 ${ -svgHeight/2.2 + shiftY })`)
        .style("font-size","24")
        .style("font-weight", "light")
        .style("font-style","italic")
    .selectAll("tspan")
    .data(["Canada Learning Code 2024 Layoffs:", "Employees LAID OFF × HIERARCHY"])
        .join("tspan")
            .text(d => d)
            .attr("x", 0)
            .attr("dy", (d,i) => 28 * i)
    ;
    
    d3svg.append("g") 
      .attr("transform",`translate(${shiftX},${shiftY})`)
      .selectAll("path")
      .data(
        d3.pie()
          .value(d => d[1].length)
          .startAngle(pieRotate)
          .endAngle(2*Math.PI + pieRotate)
          .sort(null)(groupedData)
      )
      .join("path")
          .each(function(i,j) {
              d3svg.append("g")
                .attr("transform",`translate(${shiftX},${shiftY})`)
                .selectAll("path")
                .data(
                  d3.pie()
                    .value(i => i[1].length)
                    .startAngle(i.startAngle + pieGap/2)
                    .endAngle(i.endAngle - pieGap/2)
                    .sort((a,b) => i.data[0] === "FALSE" ? d3.ascending(a[0],b[0]) : d3.descending(a[0],b[0]))(d3.group(i.data[1], d => d.Hierarchy))
                )
                .join("path")
                  .each (function (o,p){
                    const [x,y] = d3.arc().innerRadius(180).outerRadius(180).centroid(o);
                    d3svg.append("text")
                      .attr("transform",`translate(${shiftX},${shiftY})`)
                      .attr("dy",6)
                      .attr("text-anchor",(o.endAngle + o.startAngle)/2 < Math.PI ? "start" : "end" )
                      .text(`${i.data[0] === "TRUE" ? "" : ""} ${CS.HIERARCHY[o.data[0]]}`)
                      .attr("x", x)
                      .attr("y", y)
                      .style("font-style","italic")
                      .style("font-family", "Georgia")
                        .append("tspan")
                        .text(` ${o.data[1].length}`)
                        .style("font-weight","bold")
              })
                .attr("fill", (k,m) => d3.color(
                  d3.quantize(
                    d3.interpolateRgb(d3.color("aqua"),d3.color("aqua").darker(2)),5).reverse()[k.data[0]]
              )
                     )
                .attr("stroke", (k,m) => k.data[0] === "TRUE" ? "none" : "none")  
                .attr("d", d3.arc().innerRadius(outerRadius + 10).outerRadius(outerRadius + 50))
            .append("title")
               .text(d => d.data[1].length)
         })
        .attr("fill", (d, i) => d3.quantize(
                d3.interpolateReds,6
                ).reverse()[1-i]
             )
        .attr("stroke", "none")
        .attr("d", d3.arc().innerRadius(50).outerRadius(100))
        .append("title")
          .text(d => d.value)
    ;
    
    d3svg.append("g")
      .attr("transform",`translate(${shiftX},${shiftY})`)
      .selectAll("g")
      .data(
        d3.pie()
          .value(d => d[1].length)
          .startAngle(pieRotate)
          .endAngle(2 * Math.PI + pieRotate)
          .sort(null)(groupedData)
      )
      .join("g")  
        .each(function (d, i) {
        const [x,y] = d3.arc().innerRadius(150).outerRadius(180).centroid(d);
  
        const g = d3.select(this);
        const labelWidth = 36;
        const labelHeight = 24;
  
        g.append("rect")
            .attr("x", x - labelWidth/2)
            .attr("y", y - labelHeight/2)
            .attr("width",labelWidth)
            .attr("height",labelHeight)
            .attr("fill", "none")
        ;
  
          g.append("text")
            .attr("dy",6)
            .attr("text-anchor",(d.endAngle + d.startAngle)/2 < Math.PI ? "start" : "end" )
            .attr("x", x)
            .attr("y", y)
            .attr("fill","darkslategrey")
            .style("font-weight","light")
            .style("font-style", "italic")
            .style("font-family","Georgia")
        ;  
      })
    ;
      
    d3.select("#d3-container-998").node()
      .append(d3svg.node())
    ;
    
  </script>
  <div id="d3-container-998">
    <!-- this will hold whatever D3 generates -->
  </div>

LAID OFF status by Hierarchy

Chart description

  • The inner ring in the nested chart above is all roles separated by laid off status with those laid off coloured in dark red and those not laid off coloured in a brighter shade of red.
  • The outer ring further separates the roles already grouped by LAID OFF status by a role’s Hierarchy, a number from 0 to 4 indicating an inferred (based on role title) relative power level of an employee within the organization (0 corresponds to non-managers, while 1 to 4 corresponds to increasing degrees of decision-making power and responsbility). These are coloured in different shades of aqua, with the darkest shades being for those with the least relative power.

Summary of data

  1. 59.5% of employees were laid off, which we have already seen but bears repeating.
  2. 84% of laid off employees were non-manager level (21 out of 25), which might seem like a familiar figure but is, in fact, a coincidence with the proportion of non-manager employees who were laid off.
  3. 12% of laid off employees were managers (3 out of 25), or 1/7th the number of non-manager employees. Of course, every other group will seem comparatively small to the first group, since the first group has already accounted for so many of the laid off employees. Only 4% of laid off employees were directors (1 out of 25), while 0% of laid off employees were executives (0 out of 25).
  4. 40.5% of employees were retained (17 out of 42).
  5. 23.5% of the employees who were retained were non-manager level (4 out of 17) and likewise 23.5% of the employees who were retained were director level (4 out of 17 again).
  6. 41% of the employees who were retained were manager level (7 out of 17).
  7. 12% of the employees who were retained were executive level (2 out of 17).

Key takeaways

  1. Again, we see that non-managers–the lowest level of employee–were significantly disproportionately affected by the Canada Learning Code lay offs compared to other hierarchy groups, with around a 5:1 likelihood of non-managers being laid off versus not.
  2. The ratio of retained non-managers to managers is also quite skewed, with there being 1.75 managers retained for every non-manager employee retained.
  3. The organization formed by the retained employees is very top-heavy, with 76.5% of retained employees being of manager level or higher or a ratio of 3.25 retained manager-level or higher employees for every non-manager employee. If this doesn’t raise 🚩 red flags 🚩 for potential sponsors, donors, or partners, it should at the very least raise eyebrows. Why is so much management necessary for so few people?

Conclusions from each analysis

What conclusions can we draw about Canada Learning Code’s current state from examining the data and key takeaways as we have?

  1. Looking at this data, the first obvious conclusion one can draw is that Canada Learning Code, in its current form, is excessively top heavy. I think this is something that would concern sponsors, donors, or partners especially, since not only are there way more manager+ employees than non-managers now (3.25:1), but that manager+ employees tend to command higher salaries than non-managers do. Thus the organization, from a salary mass perspective is even more so top heavy than the already skewed ratios found in the hierarchy levels of retained employees.
  2. For a second conclusion, the damage done from gutting the non-manager level likely makes it very difficult for Canada Learning Code to achieve impact numbers anywhere near its previous years, especially at comparable dollar-to-impact value. In terms of value, non-manager level employees represent the best bang for the donor buck: not only do they tend to be the cheapest employees, but also most directly responsible for generating impact. Getting rid of most of them not only reduces capacity overall, but it means that that same impact is now likely being done by higher level (i.e. more expensive) employees.
  3. A third conclusion, then, is actually more of a pondering: let’s say a donor, sponsor, or partner is fine with a bad value spend for one reason or another. Why should that spend go towards Canada Learning Code? What value is being added that might convince an external party to give Canada Learning Code money? What does Canada Learning Code still do?

Note: I’ve mostly avoided talking about Canada Learning Code’s volunteer base or chapter (club) model in these analyses for the simple reason that they’re volunteers. They’re free and any costs associated with them, or hosting events, would be utterly eclipsed by the money that would be spent keeping the lights on and keeping folks’ paycheques on time. And besides, how much money would be worth it to run a bunch of volunteer-based clubs? How much infrastructure (or donor dollars) would you really need to support that?

That’s it for now! Back to your regularly scheduled programming. This may get updated with further insights, but I believe it paints a pretty clear picture, based on freely available data and observations, on the strange state Canada Learning Code finds itself in.