使用d3js绘制箱线图(box plot)

 转载请注明出处。

 上一篇文章记录了dot-pie的绘制,这篇文章将会介绍另一种图形,箱线图。箱线图使用数据的最小值、最大值、中位数以及上下两个四分位数展现了这组数据。下面是最终绘制的图形。

  1、data

const dataset = [
{
      "value": 1,
      "y": 9.6121,
      "x": "2016-02-24"
    },
    {
      "value": 2,
      "y": 8.3333,
      "x": "2016-02-24"
    },
    ......
]

  这个数据并不能直接用于绘制箱线图,第三步骤中将对此数据进行处理。

  2、create dimension、colors and tooltips

const dms = {
        width: 1000,
        height: 600,
        margin: {
            top: 50,
            right: 50,
            bottom: 50,
            left: 50
        },
        tooltipMargin: 10
    }

    dms.innerWidth = dms.width - dms.margin.left - dms.margin.right;
    dms.innerHeight = dms.height - dms.margin.top - dms.margin.bottom;

const colors = ["#0067AA", "#FF7F00", "#00A23F", "#FF1F1D",
                    "#A763AC", "#B45B5D", "#FF8AB6", "#B6B800"]
 const Tooltip = d3.select('#tooltip')
              .style('opacity', 0)
              .style("background", "white")
              .style("border", "1px solid #ddd")
              .style("box-shadow", "2px 2px 3px 0px rgb(92 92 92 / 0.5)")
              .style("font-size", ".8rem")
              .style("padding", "2px 8px")
              .style('font-weight', 600)
              .style('position', 'absolute')

  3、access data

// 排序
 dataset.sort((a,b ) => a.value - b.value);
// 获取值
const yearAccessor = d=>d.x;
const yAccessor = d => d.y;
const groupAccessor = d=>d.value;
 // 分组
  const dataByYearAndGroup = d3.nest()
                    .key(yearAccessor)
                    .key(groupAccessor)
                    .entries(dataset)
 // 计算箱线图所需要的最大值、最小值、中位数以及上下四分位数,还有离散值
 const dataByYearAndGroupWithStats = dataByYearAndGroup.map(year => {
    const yearData = year['values'].map(group => {
       const groupYValues = group.values.map(yAccessor).sort((a, b) => a - b)
       const q1 = d3.quantile(groupYValues, 0.25)
       const median = d3.median(groupYValues)
       const q3 = d3.quantile(groupYValues, 0.75)
       const iqr = q3 - q1
       const [min, max] = d3.extent(groupYValues)
       const rangeMin = d3.max([min, q1 - iqr * 1.5])
       const rangeMax = d3.min([max, q3 + iqr * 1.5])
       const outliers = group.values.filter(d => yAccessor(d) < rangeMin || yAccessor(d) > rangeMax)
       return {
          ...group,
          month: +group.key,
          q1, median, q3, iqr, min, max, rangeMin, rangeMax, outliers
         }
       })
    return {
        ...year,
        values: yearData,
     }
  });
 

  4、draw canvas

const mainsvg = d3.select('#group-box-plot')
                    .append('svg')
                    .attr('width', dms.width)
                    .attr('height', dms.height)
const maingroup = mainsvg.append('g')
                    .attr('transform', `translate(${dms.margin.left}, ${dms.margin.top})`)
const boxArea = maingroup.append('g')

其中,

<div class="group-box-plot" id="group-box-plot"></div>
<div id="tooltip"></div>

  5、create scale

// 一级分组
const xScale = d3.scaleBand()
               .domain(dataByYearAndGroupWithStats.map(d=>d.key))
               .rangeRound([0, dms.innerWidth])
               .paddingInner(0.1)
               .paddingOuter(.4)
// 二级分组
const groupScale = d3.scaleBand()
                   .padding(1.9)
const groupKeys = dataByYearAndGroup[0]['values']
                  .map(d => {return d.key})
groupScale.domain(groupKeys).rangeRound([0, xScale.bandwidth()])
const yScale = d3.scaleLinear() .domain([d3.min(dataset, d=>d.y) - 0.1, d3.max(dataset, d=>d.y)]) .range([dms.innerHeight, 0])
 // 颜色
  const colorScale = d3.scaleOrdinal()
                     .range(colors)

  6、draw data

let binGroups = boxArea.selectAll('bin')
                      .data(dataByYearAndGroupWithStats)
 binGroups.exit().remove()
  const newBinGroups = binGroups
              .enter()
              .append("g")
                 .attr("class", "bin")
                       .attr('transform', d=>`translate(${xScale(d.key)}, 0)`)
       
  binGroups = newBinGroups.merge(binGroups)
  const boxWidth = 30, boxPadding = 6;
// 添加绘制区域与悬浮提示
const boxGroups = binGroups.selectAll('box')
              .data(d=>d.values)
                        .join('g')
                        .attr('class', 'box')
                        .on('mouseover', (event) => {
                            d3.select('#tooltip')
                                .style('opacity', 1)
                                .html(boxTooltip(event))
                        })
                        .on('mousemove', () => {
                            const event = d3.event;
                            // console.log(event.pageX, event.pageY)
                            Tooltip
                                .style('left', (event.pageX + dms.tooltipMargin) + 'px')
                                .style('top', (event.pageY + dms.tooltipMargin) + 'px')
                        })
                        .on('mouseout', () => {
                            d3.select('#tooltip').style('opacity', 0);
                        });
  
  // 垂直的线
  const rangeLines = boxGroups.append('line')
                        .attr('class', 'line1')
                        .attr('x1', d=>groupScale(d.key))
                        .attr('x2', d=>groupScale(d.key))
                        .attr('y1', d=>yScale(d.rangeMin))
                        .attr('y2', d=>yScale(d.rangeMax))
                        .style('width', 40)
                        .attr('stroke', d=>colorScale(d.key))
                        .attr("stroke-width", 2)

   // 矩形(箱线图)
   const barRects = boxGroups.append('rect')
        .attr('x', d=>groupScale(d.key) - boxWidth/2)
        .attr('y', d=>yScale(d.q3))
        .attr('width', boxWidth)
        .attr('height', d=>yScale(d.q1) - yScale(d.q3))
        .attr('fill', d=>colorScale(d.key))


   // 中值线
   const mediansLine = boxGroups.append('line')
        .attr('class', 'median')
        .attr('x1', d=>groupScale(d.key) - boxWidth / 2)
        .attr('x2', d=>groupScale(d.key) + boxWidth / 2)
        .attr('y1', d=>yScale(d.median))
        .attr('y2', d=>yScale(d.median))
        .attr("stroke", 'black')
        .style("width", 40)
        .attr("stroke-width", 2)
        .attr('opacity', 0.3)
 

    // 最大垂直线两端横线
    const rangeMins = boxGroups.append('line')
        .attr('class', 'line')
        .attr('x1', d=>groupScale(d.key) - boxWidth / 2 + boxPadding)
        .attr('x2', d=>groupScale(d.key) + boxWidth / 2 - boxPadding)
        .attr('y1', d=>yScale(d.rangeMin))
        .attr('y2', d=>yScale(d.rangeMin))
        .style("width", 40)
        .attr('stroke', d=>colorScale(d.key))
        .attr("stroke-width", 2)

    const rangeMaxs = boxGroups.append('line')
        .attr('class', 'line')
        .attr('x1', d=>groupScale(d.key) - boxWidth / 2 + boxPadding)
        .attr('x2', d=>groupScale(d.key) + boxWidth / 2 - boxPadding)
        .attr('y1', d=>yScale(d.rangeMax))
        .attr('y2', d=>yScale(d.rangeMax))
        .style("width", 40)
        .attr('stroke', d=>colorScale(d.key))
        .attr("stroke-width", 2)

    // 离散点
    const outliers = boxGroups.append('g')
        .attr("transform", d => `translate(${groupScale(d.key)}, 0)`)
        .selectAll('circle')
        .data(d=>d.outliers)
        .join("circle")
        .attr("class", "outlier")
        .attr("cy", d => yScale(yAccessor(d)))
        .attr("r", 2)
        .attr('fill', '#ffffff')
        .attr('stroke', 'black')
 

  7、create axes

  //  设置y坐标轴
    const yAxis = d3.axisLeft(yScale)
        .ticks(5)

    const yAxisGroup = maingroup.append('g')
        .attr('class', 'y-axis')
        .call(yAxis)

    // 添加y轴label
    const yAxisLabel = yAxisGroup.append('text')
        .attr('class', 'axis-label')
        .attr('transform', `rotate(-90)`)
        .attr('x', -dms.innerHeight / 3)
        .attr('y', -dms.margin.left + 10)
        .html("value")
        .attr('fill', 'black')
        .attr('font-size', '12px')
  // 设置x坐标轴
const labels = dataByYearAndGroup.map(d => { return d.key });
   const xAxis
= d3.axisBottom(xScale)
            .tickFormat((d, i)
=> labels[i]);
   const xAxisGroup
= maingroup.append('g')
                    .attr(
'class', 'x-axis')
                    .attr(
'transform', `translate(0, ${dms.innerHeight})`)
                    .call(xAxis)
 
// 添加x轴label
  const xAxisLabel = xAxisGroup.append('text')
                    .attr(
'class', 'axis-label')
                    .attr(
'x', dms.innerWidth / 2)
                    .attr(
'y', 40)
                    .html(
"Year")
                    .attr(
'fill', 'black')
                    .attr('font-size', '12px')

  8、create legend

  // 添加图例
    const legend = maingroup.append('g')
        .selectAll('g')
        .data(dataByYearAndGroupWithStats[0].values)
        .join('g')
        .attr('class', 'legend')
        .attr('transform', (d, i)=>`translate(0, ${i * 22})`)

    legend.append('rect')
        .attr('x', dms.innerWidth + 5)
        .attr('width', 19)
        .attr('height', 16)
        .attr('fill', d=>colorScale(d.key))

    legend.append('text')
        .attr('x', dms.innerWidth + 28)
        .attr('y', 8)
        .attr("dy", "0.32em")
        .text(d=>d.key)

  到这一步骤,箱线图差不多完成了。

  本章节,一并添加了悬浮提示功能,给出了每个箱线图的数据信息,包括最大值、最小值、中位数以及上下两个四分位数。需要说明的是,悬浮提示中的boxTooltip函数为:

function boxTooltip(d) {
    return  `
        <span class="label">group</span>: ${d.key}<br>
        <span class="label">max</span>: ${d.rangeMax}<br>
        <span class="label">q1</span>: ${d.q1}<br>
        <span class="label">median</span>: ${d.median}<br>
        <span class="label">q3</span>: ${d.q3}<br>
        <span class="label">min</span>: ${d.rangeMin}<br>
       `
}

 

posted @ 2022-07-31 21:47  先起这个昵称  阅读(582)  评论(0编辑  收藏  举报