score:2

Accepted answer

Here's an example that resolves your three issues.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
    <div id="legend"></div>
    <div id="chart"></div>

    <script>
      // margin convention set up

      const margin = { top: 20, bottom: 20, left: 20, right: 20 };

      const width = 600 - margin.left - margin.right;
      const height = 125 - margin.top - margin.bottom;

      const svg = d3.select('#chart')
        .append('svg')
          .attr('width', width + margin.left + margin.right)
          .attr('height', height + margin.top + margin.bottom);

      const g = svg.append('g')
          .attr('transform', `translate(${margin.left},${margin.top})`);


      // data

      const data = [
        { label: "a", year: 2014 },
        { label: "a", year: 2014 },
        { label: "a", year: 2014 },
        { label: "a", year: 2014 },
        { label: "a", year: 2014 },
        { label: "a", year: 2014 },
        { label: "a", year: 2014 },
        { label: "a", year: 2014 },
        { label: "b", year: 2014 },
        { label: "b", year: 2014 },
        { label: "b", year: 2014 },
        { label: "c", year: 2014 },
        { label: "c", year: 2014 },
        { label: "c", year: 2014 },
        { label: "d", year: 2014 },
        { label: "d", year: 2014 },
        { label: "d", year: 2014 },
        { label: "d", year: 2014 },
        { label: "e", year: 2014 },
        { label: "e", year: 2014 },
        { label: "e", year: 2014 },
        { label: "e", year: 2014 },
        { label: "a", year: 2015 },
        { label: "a", year: 2015 },
        { label: "a", year: 2015 },
        { label: "a", year: 2015 },
        { label: "a", year: 2015 },
        { label: "a", year: 2015 },
        { label: "b", year: 2015 },
        { label: "b", year: 2015 },
        { label: "b", year: 2015 },
        { label: "b", year: 2015 },
        { label: "b", year: 2015 },
        { label: "b", year: 2015 },
        { label: "b", year: 2015 },
        { label: "b", year: 2015 },
        { label: "c", year: 2015 },
        { label: "c", year: 2015 },
        { label: "c", year: 2015 },
        { label: "c", year: 2015 },
        { label: "c", year: 2015 },
        { label: "c", year: 2015 },
        { label: "c", year: 2015 },
        { label: "d", year: 2015 },
        { label: "d", year: 2015 },
        { label: "d", year: 2015 },
        { label: "d", year: 2015 },
        { label: "d", year: 2015 },
        { label: "e", year: 2015 },
        { label: "a", year: 2016 },
        { label: "a", year: 2016 },
        { label: "a", year: 2016 },
        { label: "a", year: 2016 },
        { label: "a", year: 2016 },
        { label: "a", year: 2016 },
        { label: "b", year: 2016 },
        { label: "b", year: 2016 },
        { label: "b", year: 2016 },
        { label: "b", year: 2016 },
        { label: "b", year: 2016 },
        { label: "b", year: 2016 },
        { label: "c", year: 2016 },
        { label: "c", year: 2016 },
        { label: "c", year: 2016 },
        { label: "c", year: 2016 },
        { label: "c", year: 2016 },
        { label: "d", year: 2016 },
        { label: "d", year: 2016 },
        { label: "d", year: 2016 },
        { label: "d", year: 2016 },
        { label: "a", year: 2017 },
        { label: "a", year: 2017 },
        { label: "a", year: 2017 },
        { label: "a", year: 2017 },
        { label: "a", year: 2017 },
        { label: "a", year: 2017 },
        { label: "a", year: 2017 },
        { label: "a", year: 2017 },
        { label: "a", year: 2017 },
        { label: "b", year: 2017 },
        { label: "b", year: 2017 },
        { label: "b", year: 2017 },
        { label: "b", year: 2017 },
        { label: "b", year: 2017 },
        { label: "b", year: 2017 },
        { label: "b", year: 2017 },
        { label: "b", year: 2017 },
        { label: "b", year: 2017 },
        { label: "c", year: 2017 },
        { label: "c", year: 2017 },
        { label: "c", year: 2017 },
        { label: "c", year: 2017 },
        { label: "c", year: 2017 },
        { label: "d", year: 2017 },
        { label: "d", year: 2017 },
        { label: "d", year: 2017 },
        { label: "d", year: 2017 },
        { label: "d", year: 2017 },
        { label: "d", year: 2017 },
        { label: "d", year: 2017 },
        { label: "a", year: 2018 },
        { label: "a", year: 2018 },
        { label: "a", year: 2018 },
        { label: "a", year: 2018 },
        { label: "b", year: 2018 },
        { label: "b", year: 2018 },
        { label: "b", year: 2018 },
        { label: "c", year: 2018 },
        { label: "c", year: 2018 },
        { label: "c", year: 2018 },
        { label: "c", year: 2018 },
        { label: "e", year: 2018 },
        { label: "a", year: 2019 },
        { label: "a", year: 2019 },
        { label: "a", year: 2019 },
        { label: "a", year: 2019 },
        { label: "b", year: 2019 },
        { label: "b", year: 2019 },
        { label: "b", year: 2019 },
        { label: "b", year: 2019 },
        { label: "b", year: 2019 },
        { label: "b", year: 2019 },
        { label: "b", year: 2019 },
        { label: "c", year: 2019 },
        { label: "c", year: 2019 },
        { label: "c", year: 2019 },
        { label: "e", year: 2019 },
        { label: "e", year: 2019 },
        { label: "f", year: 2019 },
        { label: "a", year: 2020 },
        { label: "a", year: 2020 },
        { label: "a", year: 2020 },
        { label: "a", year: 2020 },
        { label: "a", year: 2020 },
        { label: "b", year: 2020 },
        { label: "b", year: 2020 },
        { label: "b", year: 2020 },
        { label: "b", year: 2020 },
        { label: "b", year: 2020 },
        { label: "b", year: 2020 },
        { label: "c", year: 2020 },
        { label: "c", year: 2020 },
        { label: "c", year: 2020 },
        { label: "d", year: 2020 },
        { label: "d", year: 2020 },
        { label: "e", year: 2020 },
      ];

      // map from the year to the label to the array
      // of meetings for that year and label
      const yearToLabelToMeetings = d3.rollup(
        data,
        // group is an array of all of the meetings
        // that have the same year and label.
        // add the y index for each meeting
        group => group.map((d, i) => ({...d, y: i + 1})),
        // first group by year
        d => d.year,
        // then group by label
        d => d.label
      );

      // get the max number of meetings for any year and label
      const maxCount = d3.max(
        yearToLabelToMeetings,
        ([year, labelToMeetings]) => d3.max(
          labelToMeetings,
          ([label, meetings]) => meetings.length
        )
      );

      // sorted lists of the labels and years
      const labels = [...new Set(data.map(d => d.label))].sort();
      const years = [...new Set(data.map(d => d.year))].sort(d3.ascending);


      // scales

      // for setting the y position of the dots
      const y = d3.scaleLinear()
          .domain([0, maxCount])
          .range([height, 0]);

      // for setting the x position of the groups for the years
      const yearX = d3.scaleBand()
          .domain(years)
          .range([0, width])
          .padding(0.3);

      // for setting the x position of the columns of dots
      // within a year group
      const labelX = d3.scalePoint()
          .domain(labels)
          .range([0, yearX.bandwidth()]);

      // for setting the color of the dots
      const color = d3.scaleOrdinal()
          .domain(labels)
          .range(d3.schemeCategory10);


      // drawing the data

      // create one group for each year and set the group's horizontal position
      const yearGroups = g.selectAll('g')
        .data(yearToLabelToMeetings)
        .join('g')
          .attr('transform', ([year, labelToMeetings]) => `translate(${yearX(year)})`);

      // inside each year group, create one group for each label and set its
      // horizontal position in the group
      const labelGroups = yearGroups.selectAll('g')
        .data(([year, labelToMeetings]) => labelToMeetings)
        .join('g')
          .attr('transform', ([label, meetings]) => `translate(${labelX(label)})`);

      // add the dots
      labelGroups.selectAll('circle')
        .data(([label, meetings]) => meetings)
        .join('circle')
          .attr('cy', d => y(d.y))
          .attr('r', 4)
          .attr('fill', d => color(d.label));


      // axes

      // x axis
      g.append('g')
          // move the axis to the bottom of the chart
          .attr('transform', `translate(0,${height})`)
          // add the axis
          .call(d3.axisBottom(yearX).tickSizeOuter(0))
          // move the tick marks to be in between the groups
          .call(g =>
            g.selectAll('line')
                .attr('x1', yearX.step() / 2)
                .attr('x2', yearX.step() / 2)
              // remove the last tick mark
              .filter(d => d === years[years.length - 1])
              .remove()
          );

      // y axis
      g.append('g')
          .call(d3.axisLeft(y));


      // color legend

      const size = '10px';

      // create div for the legend to go in
      const legend = d3.select('#legend')
        .append('div')
          .style('display', 'flex')
          .style('font-family', 'sans-serif')
          .style('font-size', size);

      // create one div for each entry in the color scale
      const cell = legend.selectAll('div')
        .data(color.domain())
        .join('div')
          .style('margin-right', '1em')
          .style('display', 'flex')
          .style('align-items', 'center');

      // add the colored square for each entry
      cell.append('div')
          .style('background', d => color(d))
          .style('min-width', size)
          .style('min-height', size)
          .style('margin-right', '0.5em');

      // add the text label for each entry
      cell.append('div')
          .text(d => d);
    </script>
</body>
</html>


More Query from same tag