D3.js 散点图 x 轴上的自定义刻度

D3.js custom ticks on x axis of scatterplot

我有三个问题:

更多背景信息:我想用 D3.js 将 2014 年至 2020 年按年份分组的六个组织的会议可视化。散点图的每个点代表一个会议,六种颜色与组织相匹配。目前,我已经手动将散点图中每个点的 x 位置添加到输入数据文件中,因此年份之间存在差距。

当前结果:

想要的结果:

当前代码:

import axios from "axios";

let meetings
let colors = { a: "brown", b: "orange", c: "red", d: "purple", e: "blue", f: "green" };

window.addEventListener("load", () => {
    initUI();
});

function initUI() {
    // parse input data
    axios
        .get("assets/example.txt")
        .then(async (res) => {
            var rawData = res.data
                .split("\n")
                // filter header row
                .filter((row) => (row.indexOf("label") >= 0 ? false : true))
                .map((row) => {
                    var items = row.split(";");
                    return {
                        label: items[0],
                        year: items[1],
                        xPosition: items[2],
                    };
                });

            meetings = addYPositionForAllOrganizations(rawData);
            scatterplot = await showScatterPlot(meetings);
        })
        .then(() => {
            // always executed
        });
}

// Add counter for amount of meetings for one organziation per year for y axis position
function addYPosition(organizationList) {    
    organizationList.sort((a, b) => (a.year > b.year) ? 1 : -1)

    var yPosition = 1;
    var year = 2014;

    organizationList.forEach(element => {
        if (year < element.year) {
            // reset counter for next year
            yPosition = 1;
        }
        element.yPosition = 0;
        element.yPosition += yPosition;
        yPosition++;

        year = element.year;
    });
}

function addYPositionForAllOrganizations(data) {
    let a = data.filter(row => row.label == "a");
    addYPosition(a);
    let b = data.filter(row => row.label == "b");
    addYPosition(b);
    let c = data.filter(row => row.label == "c");
    addYPosition(c);
    let d = data.filter(row => row.label == "d");
    addYPosition(d);
    let e = data.filter(row => row.label == "e");
    addYPosition(e);
    let f = data.filter(row => row.label == "f");
    addYPosition(f);

    return a.concat(b).concat(c).concat(d).concat(e).concat(f);
}

async function showScatterPlot(data) {
    let margin = { top: 10, right: 30, bottom: 100, left: 60 },
        width = 1000 - margin.left - margin.right,
        height = 400 - margin.top - margin.bottom;

    // append the svg object to the body of the page
    let svg = d3
        .select("#scatter-plot")
        .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // Add x axis
    var x = d3.scaleLinear().domain([-2, 75]).range([0, width]);
    //FIXME this destroys ticks so that they are "invisible"
    var xAxis = d3.axisBottom(x).tickValues(2014, 2015, 2016, 2017, 2018, 2019, 2020);
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);
    // Add text label for x axis
    svg.append("text")
        .attr("transform", "translate(" + (width / 2) + "," + (height - (-100 / 3)) + ")")
        .attr("text-anchor", "middle")
        .style("font-family", "sans-serif")
        .text("Years 2014 - 2020");

    // Add y axis
    var y = d3.scaleLinear().domain([0.5, 10]).range([height, 0]);
    svg.append("g").attr("class", "y axis").call(d3.axisLeft(y));
    // Add text label for the y axis
    svg.append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 - margin.left)
        .attr("x", 0 - (height / 2))
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .style("font-family", "sans-serif")
        .text("Amount");

    // Add meetings as dots
    let meetings = svg.append('g')
        .selectAll("dot")
        .data(data)
        .enter()
        .append("circle")
        .attr("cx", function (d) { return x(d.xPosition); })
        .attr("cy", function (d) { return y(d.yPosition); })
        .attr("r", 5.5)
        .style("fill", getColorForMeeting);
    return { svg, x, y };    
}

function getColorForMeeting(data) {
    return colors[data.label];
}
<script src="https://d3js.org/d3.v4.js"></script>

<div id="scatter-plot"></div>

输入数据文件的摘录:

运行项目可以调查here

这里有一个例子可以解决你的三个问题。

<!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>