<div id="chart-container"></div>
html {
  overflow: hidden;
}

#chart-container {
  height: 200px;
  width: 100%;
}

line {
  stroke: #999;
  stroke-opacity: 0.6;
}

circle {
  stroke: #fff;
  stroke-width: 1.5px;
}

text {
  font-family: sans-serif;
  font-size: 8px;
  text-shadow: 1px 0px 0px #fff, 0px 1px 0px #fff, 1px 1px 0px #fff, -1px 0px 0px #fff, 0px -1px 0px #fff, -1px -1px 0px #fff;
}

@media (min-width: 700px) {
  text {
    font-size: 10px;
  }
}

@media (min-width: 600px) {
  #chart-container {
    height: 100vh;
  }
}
const scale = d3.scaleOrdinal(d3.schemeCategory10);

const color = d => scale(d.group);

const drag = simulation => {
  function dragstarted(d) {
    if (!d3.event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }

  function dragged(d) {
    d.fx = d3.event.x;
    d.fy = d3.event.y;
  }

  function dragended(d) {
    if (!d3.event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }

  return d3
    .drag()
    .on('start', dragstarted)
    .on('drag', dragged)
    .on('end', dragended);
};

const dataPromise = d3.json(
  'https://gist.githubusercontent.com/hayesjosh/7eb3ad4c1015ebf66fa0a488c0dfcd49/raw/9277b542b6a24d98eeb934b251d7946230db699c/genre3.json'
);

dataPromise.then(data => {
  console.clear();
  const links = data.links.map(d => ({ ...d }));
  const nodes = data.nodes.map(d => ({ ...d }));

  const body = document.getElementById('chart-container');

  function redraw() {
    const height = body.offsetHeight;
    const width = window.innerWidth;

    const RADIUS = width > 700 ? 8 : 5;
    const STRENGTH = width > 700 ? -200 : -100
    const LABEL_X_OFFSET = RADIUS + 4;
    const LABEL_Y_OFFSET = RADIUS + 4;

    const simulation = d3
      .forceSimulation(nodes)
      .force('link', d3.forceLink(links).id(d => d.id))
      .force('charge', d3.forceManyBody().strength(STRENGTH))
      .force('center', d3.forceCenter(width / 2, height / 2));

    d3.select('svg').remove();
    const svg = d3
      .select(body)
      .append('svg')
      .attr('width', width)
      .attr('height', height);

    const link = svg
      .selectAll('line')
      .data(links)
      .enter()
      .append('line')
      .attr('stroke-width', d => Math.log(Math.sqrt(d.value)));

    const node = svg
      .selectAll('circle')
      .data(nodes)
      .enter()
      .append('circle')
      .attr('r', RADIUS)
      .attr('fill', color)
      .call(drag(simulation));

    const label = svg
      .selectAll('text')
      .data(nodes)
      .enter()
      .append('text')
      .text(d => d.id);

    simulation.on('tick', () => {
      label
        .attr('dx', d => minmax(0, width - LABEL_X_OFFSET - 20, d.x - LABEL_X_OFFSET))
        .attr('dy', d => minmax(RADIUS + LABEL_Y_OFFSET, height - LABEL_Y_OFFSET - RADIUS * 2, d.y - LABEL_Y_OFFSET));

      node.attr('cx', d => minmax(0, width - RADIUS * 2, d.x)).attr('cy', d => minmax(0, height - RADIUS * 2, d.y));

      link
        .attr('x1', d => minmax(0, width - RADIUS, d.source.x))
        .attr('y1', d => minmax(0, height - RADIUS, d.source.y))
        .attr('x2', d => minmax(0, width - RADIUS, d.target.x))
        .attr('y2', d => minmax(0, height - RADIUS, d.target.y));
    });
  }

  redraw();
  // window.addEventListener('resize', redraw);
});

function minmax(min, max, val) {
  return Math.max(min, Math.min(max, val));
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.2/d3.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/d3-force/1.1.0/d3-force.min.js