Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

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

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

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.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
      rel="stylesheet"
    />
    <!-- CSS -->

    <div id="vis" class="vis vis-container">
      <h1>A Taxonomy for Personal Data Classification</h1>
      <div class="controls-container">
        <div id="control-spacer" class="control-group"></div>
        <div id="data-control" class="control-group">
          <div class="btn-group">
            <button class="btn is-selected" data-chart-data="categories"
              >Data Categories</button
            >
            <button class="btn" data-chart-data="uses"
              >Data Uses</button
            >
            <button class="btn" data-chart-data="subjects"
              >Data Subjects</button
            >
            <button class="btn" data-chart-data="qualifiers"
              >Data Qualifiers</button
            >
          </div>
        </div>
        <div id="chart-type-control" class="control-group">
          <div class="btn-group">
            <button class="btn btn--icon is-selected" data-chart-type="tree">
              <img src="https://harpocrates.ethyca.com/assets/Tree@1x.svg" alt="tree" />
            </button>
            <button class="btn btn--icon" data-chart-type="radialTree">
              <img src="https://harpocrates.ethyca.com/assets/Radial%20Tree@1x.svg" alt="radial tree" />
            </button>
            
            <button
              class="btn btn--icon"
              data-chart-type="sunburst"
            >
              <img src="https://harpocrates.ethyca.com/assets/Sunburst@1x.svg" alt="sunburst" />
            </button>
          </div>
        </div>
      </div>
      <div id="vis-chart" class="chart-container">
        <svg id="vis-sunburst"></svg>
        <svg id="vis-radial-tree"></svg>
        <svg id="vis-tree"></svg>
      </div>
      <div id="vis-color-legend"></div>
    </div>
    <!-- D3 JS -->
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <!-- JS -->

              
            
!

CSS

              
                /* Typography */
.vis {
  font-family: "Source Sans Pro", sans-serif;
  font-weight: 400;
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: 0.15px;
  color: #313f4e;
  margin: 0 auto;
}

.vis h1 {
  font-weight: 700;
  font-size: 26px;
  letter-spacing: -0.48px;
  color: #111439;
  margin: 0;
}

.vis .legend {
  font-weight: 600;
  font-size: 10px;
  letter-spacing: -0.19px;
  color: #0a2540;
}

.vis .card-title {
  font-weight: 700;
}

.vis .card-subtitle {
  font-weight: 600;
}

/* General */
.vis-container {
  max-width: 1400px;
  text-align: center;
}

.vis-container > * + * {
  margin-top: 1.5rem;
}

.chart-container > svg {
  display: block;
}

@media screen and (min-width: 1200px) {
  .controls-container {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
  }
}

.btn-group {
  display: inline-flex;
  vertical-align: middle;
  margin: 0.25em;
}

.btn {
  line-height: 1;
  font-size: 14px;
  background-color: #eef2f7;
  border: none;
  padding: 0.65em 0.75em;
  margin: 0;
}

.btn--icon {
  padding: 0;
  width: 48px;
  height: 2.3em;
  display: inline-flexbox;
  justify-content: center;
  align-items: center;
}

.btn:first-child {
  border-top-left-radius: 2px;
  border-bottom-left-radius: 2px;
}

.btn:last-child {
  border-top-right-radius: 2px;
  border-bottom-right-radius: 2px;
}

.btn:hover {
  background-color: #e9ebee;
}

.btn:active,
.btn.is-selected {
  background-color: #cbd7e7;
}

/* Legend */
.vis-color-legend {
  display: inline-flex;
  justify-content: center;
  flex-wrap: wrap;
}

.vis-color-legend .legend-item {
  display: inline-flex;
  align-items: center;
  margin-left: 0.5em;
  margin-right: 0.5em;
}

.vis-color-legend .legend-swatch {
  width: 1em;
  height: 1em;
  margin-right: 0.5em;
}

/* Tooltip */
.vis-tooltip {
  position: absolute;
  top: 0;
  left: 0;
  background-color: #ffffff;
  padding: 1em;
  border-radius: 4px;
  box-shadow: 0 2px 24px 0 rgba(0, 0, 0, 0.2);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
}

.vis-tooltip.is-visible {
  opacity: 1;
}

.vis-tooltip .card {
  max-width: 240px;
}

.vis-tooltip .card > * + * {
  margin-top: 1em;
}

/* Sunburst */
.vis .sunburst-svg .partition-path,
.vis .sunburst-svg .label-text {
  transition: fill 0.15s;
}

.vis .sunburst-svg .partition-path.is-highlighted {
  fill: #57f2ea;
}

.vis .sunburst-svg .label-text.is-highlighted {
  fill: currentColor;
}

/* Radial Tree */
/* Tree */
.vis .radial-tree-svg .link,
.vis .radial-tree-svg .node,
.vis .tree-svg .link,
.vis .tree-svg .node {
  transition: fill 0.15s;
}

.vis .radial-tree-svg .link,
.vis .tree-svg .link {
  stroke-width: 1.5px;
}

.vis .radial-tree-svg .link.is-highlighted,
.vis .tree-svg .link.is-highlighted {
  stroke: #57f2ea;
  stroke-width: 2.5px;
}

.vis .radial-tree-svg .node.is-highlighted,
.vis .tree-svg .node.is-highlighted {
  fill: #57f2ea;
}

.vis .radial-tree-svg .label-text .label-text__bg,
.vis .tree-svg .label-text .label-text__bg {
  stroke: #ffffff;
  stroke-width: 3px;
}

.vis .radial-tree-svg .label-text.is-highlighted,
.vis .tree-svg .label-text.is-highlighted {
  font-weight: 700;
}

              
            
!

JS

              
                class VisColorLegend {
    constructor({ el }) {
      this.el = el;
    }
  
    renderLegend() {
      d3.select(this.el)
        .classed("vis-color-legend", true)
        .selectAll(".legend-item")
        .data(this.scale.domain())
        .join((enter) =>
          enter
            .append("div")
            .attr("class", "legend-item")
            .call((item) => item.append("div").attr("class", "legend-swatch"))
            .call((item) => item.append("div").attr("class", "legend-label"))
        )
        .call((item) =>
          item
            .select(".legend-swatch")
            .style("background-color", (d) => this.scale(d))
        )
        .call((item) => item.select(".legend-label").text((d) => d));
    }
  
    updateScale(scale) {
      this.scale = scale;
      this.renderLegend();
    }
  }

  class VisTooltip {
    constructor() {
      this.tooltip = d3
        .select("body")
        .append("div")
        .attr("class", "vis vis-tooltip");
      this.show = this.show.bind(this);
      this.hide = this.hide.bind(this);
      this.move = this.move.bind(this);
    }
  
    show({ accessor, d }) {
      const content = `
      <div class="card">
        <div class="card-title">${accessor.name(d.data)}</div>
        <div>
          <div class="card-subtitle">Hierarchy:</div>
          <div style="text-transform: capitalize">${accessor
            .id(d.data)
            .split(".")
            .map((d) => d.replace(/_/g, " "))
            .join(" > ")}</div>
        </div>
        ${
          accessor.description(d.data)
            ? `<div>
                <div class="card-subtitle">Description:</div>
                <div>${accessor.description(d.data)}</div>
              </div>`
            : ""
        }
      </div>
      `;
      this.tooltip.html(content).classed("is-visible", true);
      const box = this.tooltip.node().getBoundingClientRect();
      this.width = box.width;
      this.height = box.height;
    }
  
    hide() {
      this.tooltip.classed("is-visible", false);
    }
  
    move(event) {
      const padding = 16;
      let x = event.pageX;
      let y = event.pageY;
      const bodyWidth = document.body.clientWidth;
      const bodyHeight = document.body.clientHeight;
      if (x < bodyWidth / 2) {
        x = x - this.width - padding;
        if (x < 0) {
          x = 0;
        }
      } else {
        x = x + padding;
        if (x + this.width > bodyWidth) {
          x = bodyWidth - this.width;
        }
      }
      if (y < bodyHeight / 2) {
        y = y - this.height - padding;
        if (y < 0) {
          y = 0;
        }
      } else {
        y = y + padding;
        if (y + this.height > bodyHeight) {
          y = bodyHeight - this.height;
        }
      }
  
      this.tooltip.style("transform", `translate(${x}px,${y}px)`);
    }
  }

  class VisSunburst {
    constructor({ el, accessor, tooltip }) {
      this.el = el;
      this.accessor = accessor;
      this.tooltip = tooltip;
      this.resizeVis = this.resizeVis.bind(this);
      this.enteredPartition = this.enteredPartition.bind(this);
      this.movedPartition = this.movedPartition.bind(this);
      this.leftPartition = this.leftPartition.bind(this);
      this.init();
    }
  
    init() {
      this.dims = {
        margin: 40,
        maxHeight: 960,
      };
  
      this.arc = d3
        .arc()
        .startAngle((d) => d.x0)
        .endAngle((d) => d.x1)
        .padAngle((d) => Math.min((d.x1 - d.x0) / 2, 0.005))
        .innerRadius((d) => d.y0)
        .outerRadius((d) => d.y1 - 1);
  
      this.partition = d3.partition();
  
      this.svg = d3.select(this.el).attr("class", "sunburst-svg");
      this.g = this.svg.append("g");
      this.gPartitions = this.g.append("g").attr("class", "partitions-g");
      this.gLabels = this.g
        .append("g")
        .attr("class", "labels-g")
        .attr("fill", "currentColor")
        .attr("pointer-events", "none")
        .attr("text-anchor", "middle");
      this.gCenter = this.g
        .append("g")
        .attr("class", "center-g")
        .call((g) => g.append("path").attr("class", "center-path"))
        .call((g) =>
          g
            .append("path")
            .attr("class", "center-text-path")
            .attr("fill", "none")
            .attr("id", "sunburst-center-text-path")
        )
        .call((g) =>
          g
            .append("text")
            .attr("class", "center-text")
            .attr("text-anchor", "middle")
            .attr("dy", "0.32em")
            .append("textPath")
            .attr("startOffset", "50%")
            .attr("xlink:href", "#sunburst-center-text-path")
        );
  
      window.addEventListener("resize", this.resizeVis);
      this.resizeVis();
    }
  
    resizeVis() {
      if (this.svg.style("display") === "none") return;
      this.dims.width = this.el.parentNode.clientWidth;
      this.dims.height = Math.min(this.dims.width, this.dims.maxHeight);
      this.dims.radius =
        Math.min(this.dims.width, this.dims.height) / 2 - this.dims.margin;
  
      this.arc.padRadius(this.dims.radius / 2);
  
      this.partition.size([2 * Math.PI, this.dims.radius]);
  
      if (this.displayData) {
        this.wrangleData();
      }
    }
  
    renderVis() {
      this.renderPartitionPaths();
      this.renderPartitionLabels();
      this.renderCenter();
      this.autoViewBox();
    }
  
    renderPartitionPaths() {
      this.partitionPath = this.gPartitions
        .selectAll(".partition-path")
        .data(this.displayData.descendants().filter((d) => d.depth))
        .join((enter) =>
          enter
            .append("path")
            .attr("class", "partition-path")
            .on("mouseenter", this.enteredPartition)
            .on("mousemove", this.movedPartition)
            .on("mouseleave", this.leftPartition)
        )
        .attr("fill", (d) => {
          while (!this.color.domain().includes(this.accessor.colorKey(d.data)))
            d = d.parent;
          return this.color(this.accessor.colorKey(d.data));
        })
        .attr("d", this.arc);
    }
  
    renderPartitionLabels() {
      this.labelText = this.gLabels
        .selectAll(".label-text")
        .data(
          this.displayData
            .descendants()
            .filter((d) => d.depth && ((d.y0 + d.y1) / 2) * (d.x1 - d.x0) > 10)
        )
        .join((enter) =>
          enter.append("text").attr("class", "label-text").attr("dy", "0.35em")
        )
        .attr("fill", "#ffffff")
        .attr("transform", function (d) {
          const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI;
          const y = (d.y0 + d.y1) / 2;
          return `rotate(${
            x - 90
          }) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
        })
        .text((d) => this.getLabelText(d));
    }
  
    renderCenter() {
      this.gCenter
        .select(".center-path")
        .attr("fill", this.color(this.accessor.colorKey(this.displayData)))
        .attr(
          "d",
          this.arc({
            x0: -Math.PI,
            x1: Math.PI,
            y0: this.displayData.y1 - this.dims.margin,
            y1: this.displayData.y1,
          })
        );
  
      this.gCenter.select(".center-text-path").attr("d", () => {
        const r = this.displayData.y1 - this.dims.margin / 2;
        const startAngle = (-Math.PI * 3) / 2 + 0.001;
        const endAngle = Math.PI / 2 - 0.001;
        const x0 = r * Math.cos(startAngle);
        const y0 = r * Math.sin(startAngle);
        const x1 = r * Math.cos(endAngle);
        const y1 = r * Math.sin(endAngle);
        return `M ${x0} ${y0} A ${r} ${r} 0 1 1 ${x1} ${y1}`;
      });
  
      this.gCenter
        .select(".center-text textPath")
        .attr("fill", "#ffffff")
        .text(this.displayData.data.name);
    }
  
    enteredPartition(event, d) {
      const ancestors = new Set(d.ancestors());
      this.partitionPath.classed("is-highlighted", (e) => ancestors.has(e));
      this.labelText.classed("is-highlighted", (e) => ancestors.has(e));
      this.tooltip.show({
        accessor: this.accessor,
        d,
      });
    }
  
    movedPartition(event, d) {
      this.tooltip.move(event);
    }
  
    leftPartition(event, d) {
      this.partitionPath.classed("is-highlighted", false);
      this.labelText.classed("is-highlighted", false);
      this.tooltip.hide();
    }
  
    wrangleData() {
      this.root.count().sort((a, b) => b.value - a.value);
      this.displayData = this.partition(this.root);
      this.displayData.each((d) => {
        if (d.y0 < 0) {
          d.y0 -= this.dims.margin;
        } else {
          d.y0 += this.dims.margin;
        }
        if (d.y1 < 0) {
          d.y1 -= this.dims.margin;
        } else {
          d.y1 += this.dims.margin;
        }
      });
      this.maxLetterCount = (this.displayData.y1 - this.displayData.y0) / 6.5;
      this.renderVis();
    }
  
    autoViewBox() {
      const { y, height } = this.g.node().getBBox();
  
      this.svg.attr("viewBox", [
        -this.dims.width / 2,
        Math.floor(y),
        this.dims.width,
        Math.ceil(height),
      ]);
    }
  
    getLabelText(d) {
      let label = this.accessor.name(d.data);
      if (label.length > this.maxLetterCount) {
        if (!d.children) {
          label = "..." + label.slice(label.length - this.maxLetterCount + 2);
        } else {
          label = label.slice(0, this.maxLetterCount - 1) + "...";
        }
      }
      return label;
    }
  
    destroy() {
      window.removeEventListener("resize", this.resizeVis);
      this.svg.selectAll("*").remove();
    }
  
    updateData({ data, color }) {
      this.root = data;
      this.color = color;
      this.wrangleData();
    }
  }

  
  class VisRadialTree {
    constructor({ el, accessor, tooltip }) {
      this.el = el;
      this.accessor = accessor;
      this.tooltip = tooltip;
      this.resizeVis = this.resizeVis.bind(this);
      this.enteredNode = this.enteredNode.bind(this);
      this.movedNode = this.movedNode.bind(this);
      this.leftNode = this.leftNode.bind(this);
      this.init();
    }
  
    init() {
      this.dims = {
        margin: 80,
        maxHeight: 960,
        nodeRadius: 3.5,
      };
      this.maxLetterCount = this.dims.margin / 6.5;
  
      this.tree = d3
        .tree()
        .separation((a, b) => (a.parent == b.parent ? 1 : 2) / a.depth);
  
      this.svg = d3.select(this.el).attr("class", "radial-tree-svg");
      this.g = this.svg.append("g");
      this.gLinks = this.g.append("g").attr("class", "links-g");
      this.gNodes = this.g.append("g").attr("class", "nodes-g");
      this.gLabels = this.g
        .append("g")
        .attr("class", "labels-g")
        .attr("fill", "currentColor");
  
      window.addEventListener("resize", this.resizeVis);
      this.resizeVis();
    }
  
    resizeVis() {
      if (this.svg.style("display") === "none") return;
      this.dims.width = this.el.parentNode.clientWidth;
      this.dims.height = Math.min(this.dims.width, this.dims.maxHeight);
      this.dims.radius =
        Math.min(this.dims.width, this.dims.height) / 2 - this.dims.margin;
  
      this.tree.size([2 * Math.PI, this.dims.radius]);
  
      if (this.displayData) {
        this.displayData = this.tree(this.root);
        this.renderVis();
      }
    }
  
    renderVis() {
      this.renderLinks();
      this.renderNodes();
      this.renderNodeLabels();
      this.autoViewBox();
    }
  
    renderLinks() {
      this.link = this.gLinks
        .attr("fill", "none")
        .selectAll(".link")
        .data(this.displayData.links())
        .join((enter) => enter.append("path").attr("class", "link"))
        .attr("stroke", ({ target }) => {
          while (target.depth > 1) target = target.parent;
          return this.color(this.accessor.colorKey(target.data));
        })
        .attr(
          "d",
          d3
            .linkRadial()
            .angle((d) => d.x)
            .radius((d) => d.y)
        );
    }
  
    renderNodes() {
      this.node = this.gNodes
        .selectAll(".node")
        .data(
          this.displayData.descendants().filter((d) => d.depth !== 0),
          (d) => this.accessor.id(d.data)
        )
        .join((enter) =>
          enter
            .append("circle")
            .attr("class", "node")
            .attr("r", this.dims.nodeRadius)
            .attr("stroke", "transparent")
            .attr("stroke-width", 16)
            .on("mouseenter", this.enteredNode)
            .on("mousemove", this.movedNode)
            .on("mouseleave", this.leftNode)
        )
        .attr(
          "transform",
          (d) => `
          rotate(${(d.x * 180) / Math.PI - 90})
          translate(${d.y},0)
        `
        )
        .attr("fill", (d) => {
          while (d.depth > 1) d = d.parent;
          return this.color(this.accessor.colorKey(d.data));
        });
    }
  
    renderNodeLabels() {
      this.labelText = this.gLabels
        .selectAll(".label-text")
        .data(
          this.displayData.descendants().filter((d) => d.depth !== 0),
          (d) => this.accessor.id(d.data)
        )
        .join((enter) =>
          enter
            .append("g")
            .attr("class", "label-text")
            .on("mouseenter", this.enteredNode)
            .on("mousemove", this.movedNode)
            .on("mouseleave", this.leftNode)
            .call((g) => g.append("text").attr("class", "label-text__bg"))
            .call((g) => g.append("text").attr("class", "label-text__fg"))
        );
      this.labelText
        .selectAll("text")
        .attr(
          "transform",
          (d) => `
          rotate(${(d.x * 180) / Math.PI - 90}) 
          translate(${d.y},0) 
          rotate(${d.x >= Math.PI ? 180 : 0})
        `
        )
        .attr("dy", "0.32em")
        .attr("x", (d) => (d.x < Math.PI === !d.children ? 6 : -6))
        .attr("text-anchor", (d) =>
          d.x < Math.PI === !d.children ? "start" : "end"
        )
        .text((d) => this.accessor.name(d.data));
    }
  
    enteredNode(event, d) {
      const ancestors = new Set(d.ancestors());
      this.link.classed("is-highlighted", ({ target }, i, n) => {
        if (ancestors.has(target)) {
          d3.select(n[i]).raise();
          return true;
        }
        return false;
      });
      this.node.classed("is-highlighted", (e) => ancestors.has(e));
      this.labelText.classed("is-highlighted", (e) => ancestors.has(e));
      this.tooltip.show({
        accessor: this.accessor,
        d,
      });
    }
  
    movedNode(event, d) {
      this.tooltip.move(event);
    }
  
    leftNode(event, d) {
      this.link.classed("is-highlighted", false);
      this.node.classed("is-highlighted", false);
      this.labelText.classed("is-highlighted", false);
      this.tooltip.hide();
    }
  
    autoViewBox() {
      const { y, height } = this.g.node().getBBox();
  
      this.svg.attr("viewBox", [
        -this.dims.width / 2,
        Math.floor(y),
        this.dims.width,
        Math.ceil(height),
      ]);
    }
  
    wrangleData() {
      this.displayData = this.tree(this.root);
      this.renderVis();
    }
  
    destroy() {
      window.removeEventListener("resize", this.resizeVis);
      this.svg.selectAll("*").remove();
    }
  
    updateData({ data, color }) {
      this.root = data;
      this.color = color;
      this.wrangleData();
    }
  }
  class VisTree {
    constructor({ el, accessor, tooltip }) {
      this.el = el;
      this.accessor = accessor;
      this.tooltip = tooltip;
      this.resizeVis = this.resizeVis.bind(this);
      this.enteredNode = this.enteredNode.bind(this);
      this.movedNode = this.movedNode.bind(this);
      this.leftNode = this.leftNode.bind(this);
      this.init();
    }
  
    init() {
      this.dims = {
        margin: 0,
        nodeHeight: 48,
        nodeRadius: 3.5,
      };
  
      this.tree = d3
        .tree()
        .separation((a, b) => (a.parent == b.parent ? 1 : 2) / a.depth);
  
      this.svg = d3.select(this.el).attr("class", "tree-svg");
      this.g = this.svg.append("g");
      this.gLinks = this.g.append("g").attr("class", "links-g");
      this.gNodes = this.g.append("g").attr("class", "nodes-g");
      this.gLabels = this.g
        .append("g")
        .attr("class", "labels-g")
        .attr("fill", "currentColor");
  
      window.addEventListener("resize", this.resizeVis);
      this.resizeVis();
    }
  
    resizeVis() {
      if (this.svg.style("display") === "none") return;
      this.dims.width = this.el.parentNode.clientWidth;
  
      if (this.displayData) {
        this.wrangleData();
      }
    }
  
    renderVis() {
      this.renderLinks();
      this.renderNodes();
      this.renderNodeLabels();
      this.autoViewBox();
    }
  
    renderLinks() {
      this.link = this.gLinks
        .attr("fill", "none")
        .selectAll(".link")
        .data(this.displayData.links())
        .join((enter) => enter.append("path").attr("class", "link"))
        .attr("stroke", ({ target }) => {
          while (target.depth > 1) target = target.parent;
          return this.color(this.accessor.colorKey(target.data));
        })
        .attr(
          "d",
          d3
            .linkHorizontal()
            .x((d) => d.y)
            .y((d) => d.x)
        );
    }
  
    renderNodes() {
      this.node = this.gNodes
        .selectAll(".node")
        .data(
          this.displayData.descendants().filter((d) => d.depth !== 0),
          (d) => this.accessor.id(d.data)
        )
        .join((enter) =>
          enter
            .append("circle")
            .attr("class", "node")
            .attr("r", this.dims.nodeRadius)
            .attr("stroke", "transparent")
            .attr("stroke-width", 16)
            .on("mouseenter", this.enteredNode)
            .on("mousemove", this.movedNode)
            .on("mouseleave", this.leftNode)
        )
        .attr("transform", (d) => `translate(${d.y},${d.x})`)
        .attr("fill", (d) => {
          while (d.depth > 1) d = d.parent;
          return this.color(this.accessor.colorKey(d.data));
        });
    }
  
    renderNodeLabels() {
      this.labelText = this.gLabels
        .selectAll(".label-text")
        .data(
          this.displayData.descendants().filter((d) => d.depth !== 0),
          (d) => this.accessor.id(d.data)
        )
        .join((enter) =>
          enter
            .append("g")
            .attr("class", "label-text")
            .on("mouseenter", this.enteredNode)
            .on("mousemove", this.movedNode)
            .on("mouseleave", this.leftNode)
            .call((g) => g.append("text").attr("class", "label-text__bg"))
            .call((g) => g.append("text").attr("class", "label-text__fg"))
        );
      this.labelText
        .selectAll("text")
        .attr("transform", (d) => `translate(${d.y},${d.x})`)
        .attr("dy", "0.32em")
        .attr("x", (d) => (!d.children ? 6 : -6))
        .attr("text-anchor", (d) => (!d.children ? "start" : "end"))
        .text((d) => this.getLabelText(d));
    }
  
    enteredNode(event, d) {
      const ancestors = new Set(d.ancestors());
      this.link.classed("is-highlighted", ({ target }, i, n) => {
        if (ancestors.has(target)) {
          d3.select(n[i]).raise();
          return true;
        }
        return false;
      });
      this.node.classed("is-highlighted", (e) => ancestors.has(e));
      this.labelText.classed("is-highlighted", (e) => ancestors.has(e));
      this.tooltip.show({
        accessor: this.accessor,
        d,
      });
    }
  
    movedNode(event, d) {
      this.tooltip.move(event);
    }
  
    leftNode(event, d) {
      this.link.classed("is-highlighted", false);
      this.node.classed("is-highlighted", false);
      this.labelText.classed("is-highlighted", false);
      this.tooltip.hide();
    }
  
    autoViewBox() {
      const { x, y, width, height } = this.g.node().getBBox();
  
      this.svg.attr("viewBox", [
        width / 2 - this.dims.width / 2,
        Math.floor(y),
        this.dims.width,
        Math.ceil(height),
      ]);
    }
  
    wrangleData() {
      this.dims.nodeWidth =
        (this.dims.width - this.dims.margin * 2) / (this.root.height + 2);
      this.tree.nodeSize([this.dims.nodeHeight, this.dims.nodeWidth]);
      this.maxLetterCount = this.dims.nodeWidth / 6.5;
      this.displayData = this.tree(this.root);
      this.renderVis();
    }
  
    getLabelText(d) {
      let label = this.accessor.name(d.data);
      if (label.length > this.maxLetterCount) {
        if (!d.children) {
          label = "..." + label.slice(label.length - this.maxLetterCount + 2);
        } else {
          label = label.slice(0, this.maxLetterCount - 1) + "...";
        }
      }
      return label;
    }
  
    destroy() {
      window.removeEventListener("resize", this.resizeVis);
      this.svg.selectAll("*").remove();
    }
  
    updateData({ data, color }) {
      this.root = data;
      this.color = color;
      this.wrangleData();
    }
  }


  Promise.all([
    d3.csv("https://harpocrates.ethyca.com/assets/data_categories.csv"),
    d3.csv("https://harpocrates.ethyca.com/assets/data_uses.csv"),
    d3.csv("https://harpocrates.ethyca.com/assets/data_subjects.csv"),
    d3.csv("https://harpocrates.ethyca.com/assets/data_qualifiers.csv"),
  ]).then(([categoriesCSV, usesCSV, subjectsCSV, qualifiersCSV]) => {
    // Fix the controls alignment
    document.querySelector("#control-spacer").style.width =
      document.querySelector("#chart-type-control").clientWidth + "px";
  
    const tooltip = new VisTooltip();
  
    const colors = {
      categories: d3
        .scaleOrdinal()
        .domain([
          "Data Category",
          "System Data",
          "User Data",
          "User Provided Data",
          "Account Data",
          "Derived Data",
        ])
        .range([
          "#0861ce",
          "#8459cc",
          "#c14cbb",
          "#ed43a0",
          "#ff4a7f",
          "#ffa600",
        ]),
      uses: d3
        .scaleOrdinal()
        .domain([
          "Data Use",
          "Provide the capability",
          "Improve the capability",
          "Personalize the capability",
          "Advertising, Marketing or Promotion",
          "Third Party Sharing",
          "Collect",
          "Train AI System",
        ])
        .range([
          "#0861ce",
          "#8459cc",
          "#c14cbb",
          "#ed43a0",
          "#ff4a7f",
          "#ff635b",
          "#ff8436",
          "#ffa600",
        ]),
      subjects: d3
        .scaleOrdinal()
        .domain([
          "Data Subject",
          "Anonymous User",
          "Citizen Voter",
          "Commuter",
          "Consultant",
          "Custom",
          "Employee",
          "Job Applicant",
          "Next of Kin",
          "Passenger",
          "Patient",
          "Prospect",
          "Shareholder",
          "Supplier/Vendor",
          "Trainee",
          "Visitor",
        ])
        .range([
          "#0861ce",
          "#ff7040",
          "#ffa040",
          "#ffcf40",
          "#acff40",
          "#58ff40",
          "#52cf70",
          "#4ca0a0",
          "#4670cf",
          "#4040ff",
          "#6e40fe",
          "#9c40fe",
          "#c93ffd",
          "#f73ffc",
          "#fb409e",
          "#fd406f",
        ]),
      qualifiers: d3
        .scaleOrdinal()
        .domain([
          "Data Qualifier",
          "Identified Data",
          "Pseudonymized Data",
          "Unlinked Pseudonymized Data",
          "Anonymized Data",
          "Aggregated Data",
        ])
        .range([
          "#0861ce",
          "#8459cc",
          "#c14cbb",
          "#ed43a0",
          "#ff4a7f",
          "#ffa600",
        ]),
    };
  
    const elColorLegend = document.querySelector("#vis-color-legend");
  
    const colorLegend = new VisColorLegend({
      el: elColorLegend,
    });
  
    const accessor = {
      id: (d) => d.privacy_key,
      parentId: (d) => d.parent_key,
      name: (d) =>
        d.privacy_key
          .slice(d.privacy_key.lastIndexOf(".") + 1)
          .split("_")
          .map((d) => d[0].toUpperCase() + d.slice(1))
          .join(" "),
      colorKey: (d) => d.name,
      description: (d) => d.description,
    };
  
    const stratify = d3.stratify().id(accessor.id).parentId(accessor.parentId);
  
    // Chart data control
    const categoriesRoot = stratify(categoriesCSV);
    const usesRoot = stratify(usesCSV);
    const subjectsRoot = stratify(subjectsCSV);
    const qualifiersRoot = stratify(qualifiersCSV);
  
    const chartData = {
      categories: categoriesRoot,
      uses: usesRoot,
      subjects: subjectsRoot,
      qualifiers: qualifiersRoot,
    };
    const chartDataButtons = d3
      .select("#data-control")
      .selectAll("button")
      .on("click", (event) => {
        chartDataButtons.classed("is-selected", function () {
          return this === event.currentTarget;
        });
        selected.chartData = event.currentTarget.dataset.chartData;
        const data = chartData[selected.chartData].copy();
        const color = colors[selected.chartData].copy();
        colorLegend.updateScale(color);
        chart[selected.chartType].updateData({
          data,
          color,
        });
      });
  
    // Chart type control
    const chartType = {
      sunburst: {
        chart: VisSunburst,
        el: document.querySelector("#vis-sunburst"),
      },
      radialTree: {
        chart: VisRadialTree,
        el: document.querySelector("#vis-radial-tree"),
      },
      tree: {
        chart: VisTree,
        el: document.querySelector("#vis-tree"),
      },
    };
    const chart = {};
    const chartTypeButtons = d3
      .select("#chart-type-control")
      .selectAll("button")
      .on("click", (event) => {
        chartTypeButtons.classed("is-selected", function () {
          return this === event.currentTarget;
        });
        selected.chartType = event.currentTarget.dataset.chartType;
        for (const property in chartType) {
          chartType[property].el.style.display =
            property === selected.chartType ? "block" : "none";
        }
        if (!chart[selected.chartType]) {
          chart[selected.chartType] = new chartType[selected.chartType].chart({
            el: chartType[selected.chartType].el,
            accessor,
            tooltip,
          });
        }
        const data = chartData[selected.chartData].copy();
        const color = colors[selected.chartData].copy();
        colorLegend.updateScale(color);
        chart[selected.chartType].updateData({
          data,
          color,
        });
      });
  
    // Init
    const selected = {
      chartType: chartTypeButtons
        .filter(function () {
          return this.classList.contains("is-selected");
        })
        .node().dataset.chartType,
      chartData: chartDataButtons
        .filter(function () {
          return this.classList.contains("is-selected");
        })
        .node().dataset.chartData,
    };
    chartTypeButtons
      .filter(function () {
        return this.classList.contains("is-selected");
      })
      .node()
      .click();
  });
  
              
            
!
999px

Console