I'm learning d3 charts and I want to get the result like the image. The data is json and it looks like this:
[{
"date": "2020.12.1",
"pay": 1
},
{
"date": "2021.1.2",
"pay": 1
},
{
"date": "2021.2.1",
"pay": 1
},
...
pay = 1 //on time,
pay = 2 // missed,
pay = 3 // no data
Thanks regard.
Here's an example. For simplicity, I've hard coded the positions of entries in the color legend. In practice, it may be better to do the color legend in HTML so that you can take advantage of automatic horizontal layout.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
/* --- set up --- */
const margin = { top: 10, bottom: 50, left: 10, right: 10 };
const width = 500 - margin.left - margin.right;
const height = 140 - 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 parseTime = d3.timeParse('%Y.%-m.%-d');
const data = [
{ date: "2015.1.1", pay: 2 },
{ date: "2015.2.1", pay: 2 },
{ date: "2015.3.1", pay: 2 },
{ date: "2015.4.1", pay: 2 },
{ date: "2015.5.1", pay: 2 },
{ date: "2015.6.1", pay: 2 },
{ date: "2015.7.1", pay: 2 },
{ date: "2015.8.1", pay: 2 },
{ date: "2015.9.1", pay: 2 },
{ date: "2015.10.1", pay: 2 },
{ date: "2015.11.1", pay: 2 },
{ date: "2015.12.1", pay: 2 },
{ date: "2016.1.1", pay: 2 },
{ date: "2016.2.1", pay: 2 },
{ date: "2016.3.1", pay: 2 },
{ date: "2016.4.1", pay: 2 },
{ date: "2016.5.1", pay: 2 },
{ date: "2016.6.1", pay: 2 },
{ date: "2016.7.1", pay: 1 },
{ date: "2016.8.1", pay: 1 },
{ date: "2016.9.1", pay: 1 },
{ date: "2016.10.1", pay: 1 },
{ date: "2016.11.1", pay: 1 },
{ date: "2016.12.1", pay: 1 },
{ date: "2017.1.1", pay: 1 },
{ date: "2017.2.1", pay: 1 },
{ date: "2017.3.1", pay: 1 },
{ date: "2017.4.1", pay: 1 },
{ date: "2017.5.1", pay: 1 },
{ date: "2017.6.1", pay: 1 },
{ date: "2017.7.1", pay: 1 },
{ date: "2017.8.1", pay: 1 },
{ date: "2017.9.1", pay: 1 },
{ date: "2017.10.1", pay: 1 },
{ date: "2017.11.1", pay: 1 },
{ date: "2017.12.1", pay: 1 },
{ date: "2018.1.1", pay: 1 },
{ date: "2018.2.1", pay: 1 },
{ date: "2018.3.1", pay: 3 },
{ date: "2018.4.1", pay: 2 },
{ date: "2018.5.1", pay: 1 },
{ date: "2018.6.1", pay: 3 },
{ date: "2018.7.1", pay: 1 },
{ date: "2018.8.1", pay: 3 },
{ date: "2018.9.1", pay: 3 },
{ date: "2018.10.1", pay: 3 },
{ date: "2018.11.1", pay: 1 },
{ date: "2018.12.1", pay: 1 },
{ date: "2019.1.1", pay: 3 },
{ date: "2019.2.1", pay: 1 },
{ date: "2019.3.1", pay: 2 },
{ date: "2019.4.1", pay: 3 },
{ date: "2019.5.1", pay: 3 },
{ date: "2019.6.1", pay: 1 },
{ date: "2019.7.1", pay: 1 },
{ date: "2019.8.1", pay: 1 },
{ date: "2019.9.1", pay: 3 },
{ date: "2019.10.1", pay: 2 },
{ date: "2019.11.1", pay: 2 },
{ date: "2019.12.1", pay: 2 },
{ date: "2020.1.1", pay: 1 },
{ date: "2020.2.1", pay: 2 },
{ date: "2020.3.1", pay: 2 },
{ date: "2020.4.1", pay: 1 },
{ date: "2020.5.1", pay: 3 },
{ date: "2020.6.1", pay: 1 },
{ date: "2020.7.1", pay: 1 },
{ date: "2020.8.1", pay: 3 },
{ date: "2020.9.1", pay: 1 },
{ date: "2020.10.1", pay: 2 },
{ date: "2020.11.1", pay: 1 },
{ date: "2020.12.1", pay: 1 },
{ date: "2021.1.1", pay: 3 },
{ date: "2021.2.1", pay: 2 },
{ date: "2021.3.1", pay: 1 },
{ date: "2021.4.1", pay: 1 },
{ date: "2021.5.1", pay: 1 },
{ date: "2021.6.1", pay: 2 },
{ date: "2021.7.1", pay: 3 },
{ date: "2021.8.1", pay: 3 },
{ date: "2021.9.1", pay: 2 },
{ date: "2021.10.1", pay: 2 },
{ date: "2021.11.1", pay: 3 },
{ date: "2021.12.1", pay: 3 },
]
// convert the date strings to Date objects
.map(({ date, pay }) => ({ date: parseTime(date), pay }));
// group the payments by year
const groupedByYear = d3.group(data, d => d.date.getFullYear());
/* --- scales --- */
// scale to place the groups according to the year
const x = d3.scaleBand()
.domain(groupedByYear.keys())
.range([0, width]);
// scales to place the dots in a group
const numRows = 3;
const numCols = 4;
const row = d3.scalePoint()
.domain(d3.range(numRows))
.range([0, height])
.padding(1);
const col = d3.scalePoint()
.domain(d3.range(numCols))
.range([0, x.bandwidth()])
.padding(1);
// color scale for circles
const ontime = 1;
const missing = 2;
const nodata = 3;
const color = d3.scaleOrdinal()
.domain([ontime, missing, nodata])
.range(['DarkSlateGray', 'MediumVioletRed', 'Gainsboro']);
// color scale for year labels
const colorYear = d3.scaleSequential()
// input is number of missed payments for that year
.domain([0, 12])
// output interpolates between the color for on time
// and the color for missing
.interpolator(d3.interpolateHcl(color(ontime), color(missing)));
/* --- draw circles --- */
// add one group for each year
const groups = g.selectAll('g')
.data(groupedByYear)
.join('g')
.attr('transform', ([year, payments]) => `translate(${x(year)})`);
// calculate max radius size
const radius = (Math.min(row.step(), col.step()) / 2) - 2;
// add circles
groups.selectAll('circle')
.data(([year, payments]) => payments)
.join('circle')
.attr('transform', (d, i) => {
const rowIndex = Math.floor(i / numCols);
const colIndex = i % numCols;
return `translate(${col(colIndex)},${row(rowIndex)})`;
})
.attr('fill', d => color(d.pay))
.attr('r', radius);
/* --- add axis for year labels --- */
const xAxis = d3.axisBottom(x).tickSize(0);
g.append('g')
// move to the bottom of the chart
.attr('transform', `translate(0,${height})`)
// add axis
.call(xAxis)
// remove baseline
.call(g => g.select('.domain').remove())
// increase font size of the labels and set their color
.call(
g => g.selectAll('text')
.attr('font-size', 14)
.attr('fill', year => colorYear(
// get number of months with missing payments
groupedByYear.get(year)
.filter(d => d.pay === missing).length
))
);
/* --- add color legend --- */
const fontSize = 14;
const legendData = [
{label: 'ON TIME', color: color(ontime), x: 0},
{label: 'MISSED PAYMENT', color: color(missing), x: 100},
{label: 'NO DATA', color: color(nodata), x: 270},
];
const legendCells = g.append('g')
.attr('transform', `translate(${margin.left},${height + 40})`)
.selectAll('g')
.data(legendData)
.join('g')
.attr('transform', d => `translate(${d.x})`);
legendCells.append('circle')
.attr('r', fontSize / 2)
.attr('fill', d => d.color);
legendCells.append('text')
.attr('dominant-baseline', 'middle')
.attr('font-family', 'sans-serif')
.attr('fill', 'black')
.attr('font-size', fontSize)
.attr('x', fontSize)
.text(d => d.label);
</script>
</body>
</html>