D3 带多个标签的 Y 轴

D3 Y-axis with multiple labels

我想创建一个 d3 图表,其中 y 轴标签将如给定图像所示

它们是几个月 -> 低于相应的季度 -> 低于相应的年份

请帮助我如何创建这个。

这是实现此目的的一种方法。

<html>
    <head>
        <script src="https://d3js.org/d3.v7.min.js"></script>
    </head>
    
    <body>
        <div id="chart"></div>
        <script>
            // dimensions

            const width = 800;
            const height = 100;

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

            // add main svg element

            const svg = d3.select('#chart')
              .append('svg')
                .attr('width', width)
                .attr('height', height);

            // create x scale

            const startDate = new Date(2019, 1, 01);
            const endDate = new Date(2022, 0, 01);

            const months = d3.timeMonth.range(startDate, endDate);

            const x = d3.scaleTime()
                .domain([startDate, endDate])
                .range([margin.left, width - margin.right]);

            // Month label

            const monthAxis = g => g.append('g')
                .call(d3.axisBottom(x)
                               // every month, show abbreviation
                        .ticks(d3.timeMonth.every(1), d3.timeFormat('%b')));

            // Quarter label

            // map from month index (Jan is 0) to quarter label
            const monthToQuarter = new Map([
              [2, 'Q1'], // March
              [5, 'Q2'], // June
              [8, 'Q3'], // September
              [11, 'Q4'] // December
            ]);

            const quarterAxis = g => g.append('g')
                // move these labels 30 pixels down
                .attr('transform', `translate(0,30)`)
                .call(d3.axisBottom(x)
                        // don't add room for ticks
                        .tickSize(0)
                        // only have labels for Mar, Jun, Sep, and Dec
                        .tickValues(months.filter(d => monthToQuarter.has(d.getMonth())))
                        // show the quarter instead of the date
                        .tickFormat(d => monthToQuarter.get(d.getMonth())))
                // remove the baseline
                .call(g => g.select('.domain').remove())
                // remove tick lines
                .call(g => g.selectAll('.tick>line').remove())
            
            // Year label
            
            const yearAxis = g => {
              const group = g.append('g')
                  // move these labels 60 pixels down
                  .attr('transform', `translate(0,60)`);

              // data for line segments for year ranges
              // lines go from february to january
              const segments = months.filter(d => d.getMonth() === 1)
                .map(feb => {
                  const jan = d3.timeMonth.offset(feb, 11);
                  return [feb, jan];
                });
              
              const y = 6;
              
              // add lines
              group.append('g')
                .selectAll('line')
                .data(segments)
                .join('line')
                  .attr('x1', d => x(d[0]))
                  .attr('x2', d => x(d[1]))
                  .attr('y1', y)
                  .attr('y2', y)
                  .attr('stroke', 'lightgray');

              // add circles
              group.append('g')
                .selectAll('circle')
                .data(segments.flat())
                .join('circle')
                  .attr('cx', d => x(d))
                  .attr('cy', y)
                  .attr('r', 3)
                  .attr('fill', 'lightgray');

              // add labels for years
              group.append('g')
                  .call(d3.axisBottom(x)
                          // don't add room for ticks
                          .tickSize(0)
                          // center year labels between July and August
                          .tickValues(
                            months
                              .filter(d => d.getMonth() === 6)
                              .map(d => d3.timeDay.offset(d, 15))
                          )
                          .tickFormat(d => d.getFullYear()))
                  // remove the baseline
                  .call(g => g.select('.domain').remove())
                  // remove tick lines
                  .call(g => g.selectAll('.tick>line').remove())
                  /*
                    Duplicate the label and make it have a thick
                    white stroke. This provides a white background
                    to the labels so that the line does not show
                    behind them.

                    The same technique is used here:
                    https://observablehq.com/@d3/collapsible-tree
                  */
                  .call(g => g.selectAll('.tick>text').clone()
                                 .lower()
                                 .attr('stroke', 'white')
                                 .attr('stroke-width', '4')
                                 .attr('fill', null)
                                 .text(d => d.getFullYear()))
            }

            // Draw axes

            svg.append('g')
              .call(monthAxis)
              .call(quarterAxis)
              .call(yearAxis);
        </script>
    </body>
</html>

这增加了三个轴,一个在另一个下面。多年来的轴需要更多的工作来处理直线和圆圈。

输出如下: