drawPackedCircles()

in code/decision-tree/js/EntropyTree.js [84:370]


  drawPackedCircles() {
    const self = this;
    const packedDataArray = [];

    this.treeG.selectAll("g.tree-node").each(function (dG, i) {
      const cell = select(this)
        .append("g")
        .attr(
          "transform",
          `translate(${dG.x - self.radius}, ${dG.y - self.radius})`
        );
      //   make new array of data
      let currData = entropyTreeData[i];

      const nAppleArr = [...Array(+currData["nApple"]).keys()].map(() => {
        return { category: "apple" };
      });
      const nOakArr = [...Array(+currData["nOak"]).keys()].map(() => {
        return { category: "oak" };
      });
      const nCherryArr = [...Array(+currData["nCherry"]).keys()].map(() => {
        return { category: "cherry" };
      });
      const newData = shuffle([...nAppleArr, ...nOakArr, ...nCherryArr]);

      let root = { children: newData };
      let hierarchyData = hierarchy(root).sum((d) => 1);
      // second, create pack representation of data
      let packed = (data) =>
        pack()
          .size([self.radius * 2, self.radius * 2])
          .radius(() => (etMobile ? 5 : 7))
          .padding(etMobile ? 2 : 3)(data);

      const packedData = packed(hierarchyData);
      packedDataArray.push({
        packedData: packedData,
        x: dG.x,
        y: dG.y,
      });

      const treePacks = cell
        .append("g")
        .attr("class", "circle-packer")
        .selectAll("circle.point")
        .data(packedData.descendants())
        .join("circle")
        .attr("class", "point")
        .attr("r", (d) => d.r)
        .attr("cx", (d) => d.x)
        .attr("cy", (d) => d.y)
        .attr("fill", (d) => {
          if (d.depth === 0) {
            return "rgb(251, 151, 148)";
          } else {
            return self.colorScale(d.data.category);
          }
        })
        .attr("stroke", (d) => {
          if (d.depth === 0) {
            return "white";
          } else {
            return "white";
          }
        })
        .attr("stroke-width", (d) => {
          if (d.depth === 0) {
            return 3;
          } else {
            return 1;
          }
        });

      // add annotation text
      cell
        .selectAll("text.ttt")
        .data(packedData.descendants())
        .join("text")
        .attr("class", "ttt")
        .attr("text-anchor", "middle")
        .attr("alignment-baseline", "middle")
        .attr("stroke-width", (d) => {
          return "name" in self.nodes[i].data ? "3px" : "8px";
        })
        .attr("stroke", (d) => {
          return "name" in self.nodes[i].data
            ? "#232f3e"
            : self.strokeColors[+self.nodes[i].data.class];
        })
        .attr("dy", (d) => {
          if (d.depth === 0) {
            return d.y - d.r - 10;
          }
        })
        .attr("dx", (d) => d.x)
        .text((d) => {
          if (d.depth === 0) {
            return "name" in self.nodes[i].data
              ? self.nodes[i].data.name
              : self.classNames[+self.nodes[i].data.class];
          } else {
            return "";
          }
        });
      // add entropy text
      cell
        .selectAll("text.entropy-values")
        .data(packedData.descendants())
        .join("text")
        .attr("class", "entropy-values")
        .attr("text-anchor", "middle")
        .attr("alignment-baseline", "middle")
        .attr("dy", (d) => {
          if (d.depth === 0) {
            return d.y + d.r + 12;
          }
        })
        .attr("dx", (d) => d.x)
        .text((d) => {
          if (d.depth === 0) {
            return `Entropy: ${self.nodes[i].data.entropy}`;
          }
        });
    });

    const annotations = [
      {
        type: annotationLabel,
        note: {
          label:
            "For a sample of three equally-sized classes, the original data set has the maximum possible entropy value: log2(3) = 1.585.",
          wrap: 230,
        },
        connector: {
          end: "none", // 'dot' also available
          endScale: 0.15,
        },
        x: packedDataArray[0]["x"],
        y:
          packedDataArray[0]["y"] +
          self.radius +
          packedDataArray[0]["packedData"]["y"], // margin top
        dy: etMobile ? 150 : -1,
        dx: etMobile ? -60 : -180,
      },
      {
        type: annotationLabel,
        note: {
          label:
            "Our first leaf node successfully separates out all Oak samples, at the cost of bringing along two Apple data points.",
          //   bgPadding: 20,
          wrap: 200,
        },
        connector: {
          end: "none", // 'dot' also available
          endScale: 0.15,
        },
        x: packedDataArray[2]["x"] + self.MARGIN.LEFT,
        y: packedDataArray[2]["y"] + self.MARGIN.TOP, // margin top
        dy: etMobile ? 120 : -120,
        dx: etMobile ? -70 : 100,
      },
      {
        type: annotationLabel,
        note: {
          label: etMobile
            ? ""
            : "Each decision node, including those above and below this one, is selected using information gain, a function of the tree's entropy at the current and prior depth.",
          title: "Leaf Nodes",
          wrap: 220,
        },
        connector: {
          end: "dot", // 'dot' also available
          endScale: 0.5,
        },
        x: packedDataArray[1]["x"] + self.MARGIN.LEFT,
        y: packedDataArray[1]["y"] + self.MARGIN.TOP, // margin top
        dy: etMobile ? 0 : -5,
        dx: etMobile ? 0 : -220,
      },
      {
        type: annotationLabel,
        note: {
          label:
            "The second leaf node partitions off a large number of Cherry trees, at the cost of misclassifying one Apple tree.", //   bgPadding: 20,
          title: "Leaf Nodes",
          wrap: 220,
        },
        connector: {
          end: "dot", // 'dot' also available
          endScale: 0.5,
        },
        x: packedDataArray[3]["x"] + self.MARGIN.LEFT,
        y: packedDataArray[3]["y"] + self.MARGIN.TOP, // margin top
        dy: etMobile ? 110 : -120,
        dx: etMobile ? 90 : -80,
      },
      {
        type: annotationLabel,
        note: {
          label: etMobile
            ? ""
            : "Our third, and final, decision node attempts to partition the remaining Apple and Cherry data points.",
          title: "Leaf Nodes",
          wrap: 220,
        },
        connector: {
          end: "dot", // 'dot' also available
          endScale: 0.5,
        },
        x: packedDataArray[5]["x"] + self.MARGIN.LEFT,
        y: packedDataArray[5]["y"] + self.MARGIN.TOP,
        dy: etMobile ? 0 : -1,
        dx: etMobile ? 0 : -220,
      },
      {
        type: annotationLabel,
        note: {
          label: etMobile
            ? ""
            : "We could further attempt to partition the remaining points, but going too deep will overfit our model, so we decide to stop here.",
          title: "Leaf Nodes",
          wrap: 220,
        },
        connector: {
          end: "dot", // 'dot' also available
          endScale: 0.5,
        },
        x: packedDataArray[7]["x"] + self.MARGIN.LEFT,
        y: packedDataArray[7]["y"] + self.MARGIN.TOP,
        dy: etMobile ? 0 : -50,
        dx: etMobile ? 0 : -120,
      },

      {
        type: annotationLabel,
        note: {
          label:
            "While not always successful, a Decision Tree does its best to partition data at the leaf nodes into groups as 'pure' as possible, as seen here and below.",
          title: "Leaf Nodes",
          wrap: 220,
        },
        connector: {
          end: "dot", // 'dot' also available
          endScale: 0.5,
        },
        x: packedDataArray[6]["x"] + self.MARGIN.LEFT,
        y: packedDataArray[6]["y"] + self.MARGIN.TOP,
        dy: etMobile ? 120 : 120,
        dx: etMobile ? -80 : 0,
      },
      {
        type: annotationLabel,
        note: {
          label: etMobile
            ? ""
            : "At this stage, our data is well-partitioned, but we can try going deeper if we want to separate the classes even further.",
          title: "Leaf Nodes",
          wrap: 220,
        },
        connector: {
          end: "dot", // 'dot' also available
          endScale: 0.5,
        },
        x: packedDataArray[4]["x"] + self.MARGIN.LEFT,
        y: packedDataArray[4]["y"] + self.MARGIN.TOP, // margin top
        dy: etMobile ? 0 : -5,
        dx: etMobile ? 0 : self.WIDTH / 4,
      },
    ].map(function (d) {
      d.color = "grey";
      return d;
    });

    const makeAnnotations = annotation()
      //   .editMode(true)
      .type(annotationLabel)
      .annotations(annotations);

    select(`svg#${this.container}-svg`)
      .append("g")
      .attr("class", "annotation-group-entropy")
      .call(makeAnnotations);

    selectAll(".annotation-group-entropy").lower();
    selectAll("path.link").lower();
  }