-3

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.

result Image

albert
  • 8,285
  • 3
  • 19
  • 32
programer
  • 1
  • 3

1 Answers1

0

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>
Dan
  • 1,501
  • 9
  • 8
  • But can you give the year color as a color range? For example, in 2020, if ON TIME is 12, the year color is changed to ON TIME color, and if ON TIME is 6 and MISSED is 6, it is an intermediate color between ON TIME and MISSED color. – programer Nov 03 '21 at 06:22
  • Sure, I've updated the example to show how you can do it. You can create a color scale that interpolates between the colors for on time and missed payment. The input to this color scale will be the number of missed payments in the year. – Dan Nov 03 '21 at 15:18