D3.js 散点图 x 轴上的自定义刻度
D3.js custom ticks on x axis of scatterplot
我有三个问题:
- 如何在 x 轴上添加刻度,以便它们分隔散点图的分组区域(每年分组的列)?
- 如何将年份作为文本放置在散点图的相应区域下方?
- 有没有比手动将每个点的 x 位置添加到输入数据中更优雅的生成散点图的方法?
更多背景信息:我想用 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>
我有三个问题:
- 如何在 x 轴上添加刻度,以便它们分隔散点图的分组区域(每年分组的列)?
- 如何将年份作为文本放置在散点图的相应区域下方?
- 有没有比手动将每个点的 x 位置添加到输入数据中更优雅的生成散点图的方法?
更多背景信息:我想用 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>