使用d3.js画一个BoxPlot

Box Plot

在画Box Plot之前,先来了解下Box Plot是什么?

箱线图(Box Plot)也称盒须图、盒式图或箱型图,是一种用于展示数据分布特征的统计图表。

它由以下几个部分组成:

  1. 箱子:表示数据的四分位数范围,箱子的上下边界分别为上四分位数(Q3)和下四分位数(Q1)。
  2. 中间的横线:表示中位数。
  3. 胡须:也称为触须,分别延伸到最小值和最大值。

箱线图的优点包括:

  1. 直观展示数据的分布情况,包括中心位置、离散程度等。
  2. 快速比较多个数据集的特征。
  3. 检测异常值。

它常用于:

  1. 展示一组数据的分布特征。
  2. 比较不同组数据的分布情况。
  3. 识别可能的异常值。

通过箱线图,可以快速了解数据的关键特征,帮助分析和解释数据。

效果图

在这里插入图片描述

代码实现

第一步,先来绘制画布。
  const chart_id = '#box_plot'

  const margin = ({top: 10, right: 10, bottom: 20, left: 30})
  const height = 600

  document.getElementById('box_plot').innerHTML = ''

  const width = d3
      .select(chart_id).node().getBoundingClientRect().width


  const boxWidth = 50
  const rd = dataset.slice().sort((a, b) => d3.ascending(a.name, b.name))
  
  const chart = d3
      .select(chart_id)
      .attr('height', height)
第二步,生成 x 轴 和 y 轴比例尺
  const yScale = d3.scaleLinear()
      .domain(d3.extent(rd, d => d.value))
      .range([height - margin.bottom, margin.top])
      .nice()

  const xScale = d3.scaleBand()
      .domain(boxes().map(d => d.key))
      .range([margin.left, width - margin.right])
      .paddingInner(1)
      .paddingOuter(.5)
第三步,生成 box
/* 生成 box 方法 */
const boxes = () => {
  let arrMap = Array.from(d3.group(dataset, d => d.name), ([key, dat]) => ({key, dat}))
  arrMap.map(o => {
    const values = o.dat.map(d => d.value);
    const min = d3.min(values);
    const max = d3.max(values);
    const q1 = d3.quantile(values, .25);
    const q2 = d3.quantile(values, .5);
    const q3 = d3.quantile(values, .75);
    const iqr = q3 - q1;
    const r0 = Math.max(min, q1 - iqr * 1.5);
    const r1 = Math.min(max, q3 + iqr * 1.5);
    o.quartiles = [q1, q2, q3];
    o.range = [r0, r1];
    o.outliers = values.filter(v => v < r0 || v > r1);
    return o;
  });
  return (arrMap)
};
第四步,添加 box 组,设置其偏移量
  const groups = chart.selectAll("g")
      .data(boxes())
      .join("g")
      .attr("transform", d => `translate(${xScale(d.key)}, 0)`)
      .attr('class', 'ind')
第五步,添加垂直方向上的线
  groups
      .selectAll("vertLine")
      .data(d => [d.range])
      .join("line")
      .attr("class", "vertLine")
      .attr("stroke", "#7e7e7e")
      .attr('stroke-width', '1px')
      .attr("x1", 0)
      .attr("x2", 0)
      .attr("y1", d => yScale(d[0]))
      .attr("y2", d => yScale(d[1]))
第六步,添加水平方向上的线

水平方向上的三条线分别是 q1(第一四分位),median(中位数),q3(第三四分位),有的需求的第二条线不一定是中位数,也有可能是平均数(mean)。

  groups
      .selectAll('horizontalLine')
      .data((d) => [d.range[0], d.quartiles[1], d.range[1]])
      .join('line')
      .attr('class', 'horizontalLine')
      .attr('stroke', '#7e7e7e')
      .attr('stroke-width', '1px')
      .style('width', boxWidth)
      .attr('x1', -boxWidth / 2)
      .attr('x2', boxWidth / 2)
      .attr('y1', (d) => yScale(d))
      .attr('y2', (d) => yScale(d))
第七步,添加数据点
 groups
      .selectAll("points")
      .data(d => d.dat)
      .join("circle")
      .attr("cx", () => 0 - 30 / 2 + Math.random() * 30)
      .attr("cy", d => yScale(d.value))
      .attr("r", 2)
      .style("fill", "#1867c0")
      .attr("fill-opacity", 1)
第八步,添加盒子
  groups
      .selectAll("box")
      .data(d => [d])
      .join("rect")
      .attr("class", "box")
      .attr("x", -boxWidth / 2)
      .attr("y", d => yScale(d.quartiles[2]))
      .attr("height", d => yScale(d.quartiles[0]) - yScale(d.quartiles[2]))
      .attr("width", boxWidth)
      .attr("stroke", "#545454")
      .style("fill", "#1890ff")
      .style("fill-opacity", 0.3)
第九步,添加 X 轴 和 Y 轴
/* Y 轴 */
chart.append("g")
  .style("font", "12px")
  .style('stroke-width', '1px')
  .call(d3.axisLeft(yScale).tickSizeOuter(0))
  .attr('transform', `translate(${margin.left},0)`)
  .call(g => g.selectAll('.tick line').clone()
      .attr('x2', width - margin.left - margin.right)
      .attr('stroke-opacity', 0.2)
  )

/* X 轴 */
chart.append('g')
  .style('font', '12px')
  .style('stroke-width', '1px')
  .attr("transform", `translate(0,${height - margin.bottom})`)
  .call(d3.axisBottom(xScale))

整体代码

<template>
  <div style="width: 100%">
    <svg id="box_plot" style="width: 100%"/>
  </div>
</template>

<script setup>
import {onMounted} from "vue";
import * as d3 from "d3";
import dataset from "@/mock/dataset_boxplot"


onMounted(() => {
  drawBoxPlot()
})


function drawBoxPlot() {

  const chart_id = '#box_plot'

  const margin = ({top: 10, right: 10, bottom: 20, left: 30})
  const height = 600

  document.getElementById('box_plot').innerHTML = ''

  const width = d3
      .select(chart_id).node().getBoundingClientRect().width


  const boxWidth = 50
  const rd = dataset.slice().sort((a, b) => d3.ascending(a.name, b.name))

  const yScale = d3.scaleLinear()
      .domain(d3.extent(rd, d => d.value))
      .range([height - margin.bottom, margin.top])
      .nice()

  const xScale = d3.scaleBand()
      .domain(boxes().map(d => d.key))
      .range([margin.left, width - margin.right])
      .paddingInner(1)
      .paddingOuter(.5)


  const chart = d3
      .select(chart_id)
      .attr('height', height)


  const groups = chart.selectAll("g")
      .data(boxes())
      .join("g")
      .attr("transform", d => `translate(${xScale(d.key)}, 0)`)
      .attr('class', 'ind')
  groups
      .selectAll("vertLine")
      .data(d => [d.range])
      .join("line")
      .attr("class", "vertLine")
      .attr("stroke", "#7e7e7e")
      .attr('stroke-width', '1px')
      .attr("x1", 0)
      .attr("x2", 0)
      .attr("y1", d => yScale(d[0]))
      .attr("y2", d => yScale(d[1]))
  groups
      .selectAll('horizontalLine')
      .data((d) => [d.range[0], d.quartiles[1], d.range[1]])
      .join('line')
      .attr('class', 'horizontalLine')
      .attr('stroke', '#7e7e7e')
      .attr('stroke-width', '1px')
      .style('width', boxWidth)
      .attr('x1', -boxWidth / 2)
      .attr('x2', boxWidth / 2)
      .attr('y1', (d) => yScale(d))
      .attr('y2', (d) => yScale(d))
  groups
      .selectAll("points")
      .data(d => d.dat)
      .join("circle")
      .attr("cx", () => 0 - 30 / 2 + Math.random() * 30)
      .attr("cy", d => yScale(d.value))
      .attr("r", 2)
      .style("fill", "#1867c0")
      .attr("fill-opacity", 1)

  // 添加盒子
  groups
      .selectAll("box")
      .data(d => [d])
      .join("rect")
      .attr("class", "box")
      .attr("x", -boxWidth / 2)
      .attr("y", d => yScale(d.quartiles[2]))
      .attr("height", d => yScale(d.quartiles[0]) - yScale(d.quartiles[2]))
      .attr("width", boxWidth)
      .attr("stroke", "#545454")
      .style("fill", "#1890ff")
      .style("fill-opacity", 0.3)

  /* Y 轴 */
  chart.append("g")
      .style("font", "12px")
      .style('stroke-width', '1px')
      .call(d3.axisLeft(yScale).tickSizeOuter(0))
      .attr('transform', `translate(${margin.left},0)`)
      .call(g => g.selectAll('.tick line').clone()
          .attr('x2', width - margin.left - margin.right)
          .attr('stroke-opacity', 0.2)
      )

  /* X 轴 */
  chart.append('g')
      .style('font', '12px')
      .style('stroke-width', '1px')
      .attr("transform", `translate(0,${height - margin.bottom})`)
      .call(d3.axisBottom(xScale))


  const tooltip = d3.select(chart_id).append('div')

  /* 设置鼠标进入显示提交框 */
  chart.selectAll('.ind').on("mousemove", function (event) {
    tooltip
        .attr('class', 'tooltip')
        .style('opacity', 1)
        .style('transform', `translate(${event.clientX - 50}px,${event.clientY - 50}px)`)
        .text('test: tooltip')
  })
  groups.on("mouseleave", function () {
    tooltip
        .style('opacity', 0)
  })

  return chart.node()
}


/* 生成 box 方法 */
const boxes = () => {
  let arrMap = Array.from(d3.group(dataset, d => d.name), ([key, dat]) => ({key, dat}))
  arrMap.map(o => {
    const values = o.dat.map(d => d.value);
    const min = d3.min(values);
    const max = d3.max(values);
    const q1 = d3.quantile(values, .25);
    const q2 = d3.quantile(values, .5);
    const q3 = d3.quantile(values, .75);
    const iqr = q3 - q1;
    const r0 = Math.max(min, q1 - iqr * 1.5);
    const r1 = Math.min(max, q3 + iqr * 1.5);
    o.quartiles = [q1, q2, q3];
    o.range = [r0, r1];
    o.outliers = values.filter(v => v < r0 || v > r1);
    return o;
  });
  return (arrMap)
};


</script>

源码地址

源码和 mock 数据都在git仓库上,想要的小伙伴可以自己去git上拉一下。

gitee:https://gitee.com/li-jiayin167/data-visualization.git

github:https://github.com/Jane167/Data-visualization.git

如果觉得不错的话,点个 star

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/570036.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

阶段性学习汇报 4月19日

目录 一、毕业设计和毕业论文 二、学习python和vue 三、阅读知识图谱 四、下周规划 一、毕业设计和毕业论文 毕业设计后端功能基本实现&#xff0c;但是还有些具体的细节需要优化。前端小程序部分只有个前端页面以及部分交互逻辑&#xff0c;还需进一步完善。在疾病预测这里本…

3d模型合并怎么样不丢材质?---模大狮模型网

在3D设计中&#xff0c;合并模型是常见的操作&#xff0c;它可以帮助设计师将多个单独的模型组合成一个&#xff0c;从而简化场景并提高渲染效率。然而&#xff0c;合并模型时常常会面临一个棘手的问题&#xff1a;如何确保合并后的模型不丢失原有的材质?本文将探讨如何在合并…

电力调度自动化系统由什么构成?

电力调度自动化系统由什么构成&#xff1f; 电力调度自动化系统通过数据采集与传输、数据处理与存储、监视与控制、优化与决策、通信网络和系统应用软件等构成&#xff0c;实现对电力系统的监控、控制和优化。 电力调度自动化系统是一种集成了计算机技术、通信技术、自动化技术…

推荐5款我每次系统重装必装的软件

​ 你电脑中用的最久的软件是哪些&#xff1f;以下是否有你曾经使用过的软件呢&#xff1f;工欲善其事&#xff0c;必先利其器&#xff0c;今天继续分享五款实用的办公软件。 1.素材管理——Billfish ​ Billfish是一款专业的素材管理工具&#xff0c;适用于设计师、摄影师等…

2023中国便利店TOP100公示

转载来源&#xff1a;中国连锁经营协会

unity 录制360全景渲染图

1.打开pakcageManager &#xff0c;选择packages为 unityRegisty&#xff0c;找到unityRecorder插件下载&#xff0c;点击右下角instant安装&#xff0c;如果插件列表为空&#xff0c;检查是否连接网络&#xff0c;重启Unity 2.打开录制面板 3.add recorder 选择ImageSequence …

风险防不胜防?看YashanDB如何守护你的数据库安全(上篇)

数据库作为信息系统的核心&#xff0c;不仅承载着海量的关键数据&#xff0c;还负责向各类用户提供高效、可靠的信息服务&#xff0c;数据库的安全性显得尤为关键&#xff0c;已成为信息安全体系的重中之重。 什么是数据库安全&#xff1f; 数据库安全是数据安全的一个子集&a…

制造业小企业内部小程序简单设计

也没什么需求&#xff0c;就是看企业内部外来单位就餐还需要打印客饭单拿去食堂给打饭师傅&#xff0c;出门单还需要打印纸质版&#xff0c;车间PDA扫码出问题需要人手动处理&#xff0c;会议室需要OA申请&#xff0c;但是申请前不知道哪些会议室事空的(因为不是每个人都下载OA…

毫米波雷达模块用于海洋生态环境监测的技术方案研究

海洋生态环境是地球上重要的自然资源之一&#xff0c;对其进行监测和保护具有重要意义。毫米波雷达技术作为一种先进的感知技术&#xff0c;在海洋生态环境监测中具有广阔的应用前景。本文将探讨毫米波雷达模块用于海洋生态环境监测的技术方案&#xff0c;包括其原理、应用场景…

el-select下拉框远程搜索且多选时,编辑需要回显的一个简单案例

前端业务开发中不管使用vue2~3&#xff0c;还是react&#xff0c;angular各种前端技术栈&#xff0c;经常会遇到这种业务。一个下拉框Select中&#xff0c;不仅需要需要支持远程模糊搜索&#xff0c;还需要支持多选。并且在编辑时&#xff0c;还能正常把已经多选好的内容回显到…

redis主从复制,无法从redis读取最新的数据

目录 一、场景二、redis连接配置三、排查四、原因五、解决 一、场景 1、redis为主从复制模式 2、采用读写分离&#xff08;主节点写入&#xff0c;从节点读取&#xff09; 3、最新数据成功写入主节点&#xff0c;但从节点没有同步最新的数据 二、redis连接配置 #主节点 spr…

Linux——进程基本概念下篇

Linux——进程基本概念下篇 文章目录 Linux——进程基本概念下篇一、环境变量1.1 环境变量的定义1.2 环境变量的相关命令1.3 命令行参数1.4 本地变量和环境变量1.5 常规命令和内建命令 二、进程地址空间2.1 地址空间的概念2.2 页表和MMU2.3 地址空间的作用2.4 地址空间的好处 一…

Docker容器:docker基础

目录 一.docker前言 云计算服务模式 关于 docker 产品 虚拟化产品有哪些&#xff1f; ① 寄居架构 ② 源生架构 虚拟化技术概述 主流虚拟化产品对比 1. VMware系列 2. KVM/OpenStack 3. Xen 4. 其他半/全虚拟化产品 二. docker 的相关知识 1. docker 的概念 doc…

【古琴】倪诗韵古琴雷修系列(形制挺多的)

雷音系列雷修&#xff1a;“修”字取意善、美好的&#xff0c;更有“使之完美”之意。精品桐木或普通杉木制&#xff0c;栗壳色&#xff0c;纯鹿角霜生漆工艺。 方形龙池凤沼。红木配件&#xff0c;龙池上方有“倪诗韵”亲笔签名&#xff0c;凤沼下方&#xff0c;雁足上方居中位…

码头船只出行及配套货柜码放管理系统-毕设

毕业设计说明书 码头船只出行及配套货柜码放 管理系统 码头船只出行及配套货柜码放管理系统 摘要 伴随着全球化的发展&#xff0c;码头的物流和客运增多&#xff0c;码头业务迎来新的高峰。然而码头业务的增加&#xff0c;导致了人员成本和工作量的增多。为了解决这一基本问题&…

Bentley二次开发教程24-交互式类工具

交互式工具概念简述 本次内容主要涉及到交互式工具的使用&#xff0c;在MicroStation中&#xff0c;超过一半的功能都是以交互式工具的形式而存在的&#xff0c;因此交互式工具在MicroStation二次开发中便显得非常重要。当我们的鼠标或键盘在视图中产生交互操作时&#xff0c;…

各平台奇怪问题备忘录

微信小程序 小程序报错Page 页面路径 has not been register yet 描述&#xff1a;uniapp做微信小程序开发时&#xff0c;新增某页面后&#xff0c;小程序跳转该页面报错Page 页面路径 has not been register yet 已知&#xff1a;page.json已添加该页面&#xff0c;小程序a…

【Linux】文件目录及路径表示

1. Linux目录结构 在 Linux 系统中&#xff0c;有几个目录是比较重要的&#xff0c;平时需要注意不要误删除或者随意更改内部文件。 /etc&#xff1a; 这个是系统中的配置文件&#xff0c;如果更改了该目录下的某个文件可能会导致系统不能启动。 /bin, /sbin, /usr/bin, /usr…

vue快速入门(四十一)组件通信-依赖注入

注释很详细&#xff0c;直接上代码 上一篇 新增内容 祖先组件向下传值子代组件接受数据 源码 App.vue <template><div id"app"><sonComponent></sonComponent></div> </template> <script> import sonComponent from &qu…

python绘制随机地形地图

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 当我们谈论计算机编程中的地图生成时&#xff0c;通常会想到游戏开发、仿真模拟或者数据可视…