【实现自己的可视化引擎10】散点图

1,511 阅读4分钟

前言

散点图也叫 X-Y 图,它将所有的数据以点的形式展现在笛卡尔坐标系上,以显示变量之间的相互影响程度,点的位置由变量的数值决定。假设我们向要将男女生的升高体重展示在散点图上,数据格式如下:

[
  { key: 'man', weight: 60, height: 173 },
  { key: 'man', weight: 68, height: 179 },
  { key: 'man', weight: 63, height: 175 },
  { key: 'man', weight: 80, height: 183 },
  { key: 'man', weight: 70, height: 173 },
  { key: 'man', weight: 90, height: 183 },
  { key: 'man', weight: 75, height: 178 },
  { key: 'man', weight: 70, height: 174 },
  { key: 'man', weight: 64, height: 179 },
  { key: 'man', weight: 60, height: 178 },
  { key: 'man', weight: 59, height: 168 },
  { key: 'man', weight: 64, height: 177 },
  { key: 'women', weight: 51, height: 164 },
  { key: 'women', weight: 52, height: 159 },
  { key: 'women', weight: 45, height: 161 },
  { key: 'women', weight: 58, height: 154 },
  { key: 'women', weight: 48, height: 164 },
  { key: 'women', weight: 56, height: 171 },
  { key: 'women', weight: 60, height: 163 },
  { key: 'women', weight: 54, height: 166 },
  { key: 'women', weight: 53, height: 168 },
]

效果图如下:

散点图层

散点图的实现相对其他图表比较简单,散点图只需要计算两个属性在相应坐标轴的位置上即可确定该坐标点在图表上的位置,并绘制相应的标记图形。散点图层的算法如下:

  1. 计算X方向上的最大值与最小值,并计算单位长度的单位数值;
  2. 计算Y方向上的最大值与最小值,并计算单位长度的单位数值;
  3. 遍历数据,根据计算数据的相应坐标点,绘制标记图形; 代码如下:
this.clearEventListener();
this.childs.splice(0, this.childs.length);
if (this.data.length <= 0 || !this.xAxis || !this.yAxis) {
  return;
}
let xMax = this.data[0][this.xAxis];  // x方向最大值
let xMin = xMax;                      // x方向最小值
let yMax = this.data[0][this.yAxis];  // y方向最大值
let yMin = yMax;                      // y方向最小值
// 遍历数据计算X方向与Y方向的最大值与最小值
for (let i = 0; i < this.data.length; i++) {
  if (xMax < this.data[i][this.xAxis]) {
    xMax = this.data[i][this.xAxis];
  }
  if (xMin > this.data[i][this.xAxis]) {
    xMin = this.data[i][this.xAxis];
  }
  if (yMax < this.data[i][this.yAxis]) {
    yMax = this.data[i][this.yAxis];
  }
  if (yMin > this.data[i][this.yAxis]) {
    yMin = this.data[i][this.yAxis];
  }
}
// x方向单位数值的单位长度
const xStep = this.width / (xMax - xMin);
// y方向单位数值的单位长度
const yStep = this.height / (yMax - yMin);
// 遍历数据,绘制散点图
for (let i = 0; i < this.data.length; i++) {
  const data = this.data[i];
  let circle = new Circle(this.canvas, {
    position: new Point(
      this.position.x + (data[this.xAxis] - xMin) * xStep,
      this.position.y + (data[this.yAxis] - yMin) * yStep,
    ),
    radius: this.radius,
    color: this.colors[data.key],
    type: Circle.TYPE.FILL,
  });
  // 绑定数据到图元上
  circle.ext = data;
  this.addChild(circle);
  // 监听鼠标进入事件
  this.onMouseOver && (
    circle.addEventListener(Event.EVENT_MOUSE_UP, (e) => {
      this.onMouseOver(e);
    })
  );
}
// 绘图结束
/**
 * 构建结束回调
 * **/
this.onMaked && this.onMaked(this, {
  xMax,
  xMin,
  yMax,
  yMin,
  xStep,
  yStep,
});

React 组件封装

React封装需要DOM的挂载完成,所以我们在生命周期componentDidMount函数中构建我们的图层。散点图组件我们分为坐标层与散点图层,散点图层构建完成后我们通知坐标层更新坐标。

componentDidMount () {
    const { data = [], style } = this.props;
    this.canvas = new Canvas({
      ele: this.ref.current,
      canAction: false,
    });
    const xFontSize = Number(style.xFontSize || 20);
    const yFontSize = Number(style.yFontSize || 20);
    // 坐标轴标准配置
    this.axisLayer = new AxisLayer(this.canvas, {
      yAxisType: AxisLayer.AxisType.NUMBER, // y轴为数值型
      xAxisType: AxisLayer.AxisType.NUMBER,  // x轴时间为字符型
      xAxisGraduations: style.xAxis || 5,   // 网格5列
      yAxisGraduations: style.yAxis || 5,   // 网格5行
      xAxisPosition: AxisLayer.AxisPosition.BLOCK,  // X轴坐标不计算
      yAxisPosition: AxisLayer.AxisPosition.BLOCK,  // Y轴坐标计算
      yAxisRender: (value) => {
        const enob = style.xEnob || 2;
        return {
          text: Number(value).toFixed(enob),
          size: yFontSize,
          color: style.axisColor || '#999999',
          font: style.fontFamily || 'PingFang SC',
        };
      },
      xAxisRender: (value) => {
        const enob = style.yEnob || 2;
        return {
          text: Number(value).toFixed(enob),
          size: xFontSize,
          color: style.axisColor || '#999999',
          font: style.fontFamily || 'PingFang SC',
        };
      },
      color: style.color,
    });
    // 散点图层
    this.scatter = new ScatterLayer(this.canvas, {
      width: (this.canvas.width - yFontSize * 4 * this.canvas.ratio) * 0.8, // 预留20%的空白空间
      height: (this.canvas.height - xFontSize * this.canvas.ratio) * 0.8, // 预留20%的空白空间
      colors: style.colors,
      color: style.color,
      radius: style.radius,
      xAxis: style.xAxisAttr, // x轴坐标属性
      yAxis:  style.yAxisAttr, // y轴坐标属性
      onMouseOver: this.props.onMouseOver,
      position: new Point(
        yFontSize * 3.6 * this.canvas.ratio + this.canvas.width * 0.1,
        xFontSize * this.canvas.ratio * 0.9 + this.canvas.height * 0.1
      )
    }, data);
    this.scatter.onMaked = (layer, option) => {
      const { xMax, xMin, yMax, yMin, xStep, yStep } = option;
      // 计算坐标系坐标数值
      const yAxisMax = yMax + (this.canvas.height - xFontSize * this.canvas.ratio) * 0.1 / yStep;
      const yAxisMin = yMin - (this.canvas.height - xFontSize * this.canvas.ratio) * 0.1 / yStep;
      const xAxisMax = xMax + (this.canvas.width - yFontSize * 4 * this.canvas.ratio) * 0.1 / xStep;
      const xAxisMin = xMin - (this.canvas.width - yFontSize * 4 * this.canvas.ratio) * 0.1 / xStep;
      // 设置坐标轴的最大最小值
      this.axisLayer.yAxisMin = yAxisMin;
      this.axisLayer.yAxisMax = yAxisMax;
      this.axisLayer.xAxisMin = xAxisMin;
      this.axisLayer.xAxisMax = xAxisMax;
      this.axisLayer.make();
    }
    this.canvas.addChild(this.axisLayer, this.scatter);
    this.scatter.make();
    this.canvas.paint();
}

目录

【实现自己的可视化引擎01】认识Canvas
【实现自己的可视化框架引擎02】抽象图像元素
【实现自己的可视化引擎03】构建基础图元库
【实现自己的可视化引擎04】图像元素动画
【实现自己的可视化引擎05】交互与事件
【实现自己的可视化引擎06】折线图
【实现自己的可视化引擎07】柱状图
【实现自己的可视化引擎08】条形图
【实现自己的可视化引擎09】饼图
【实现自己的可视化引擎10】散点图
【实现自己的可视化引擎11】雷达图
【实现自己的可视化引擎12】K线图
【实现自己的可视化引擎13】仪表盘
【实现自己的可视化引擎14】地图
【实现自己的可视化引擎15】关系图