使用 React 绘制国富论 D3 图

Drawing Wealth of Nations D3 graph using React

我想在我的应用程序中使用 React js 绘制国家财富交互式图形可视化,但我不确定如何在 d3 版本 5 中进行。

这就是我想做的:

https://observablehq.com/@mbostock/the-wealth-health-of-nations

我不确定如何继续它以及如何从中创建组件。

任何快速帮助将不胜感激。

我试图解决它并想出了一个创建它的组件。它可能没有百分百完成,但它也获得了具有过渡效果的图表。过渡不是很好,但它对我有用。 我为它创建了两个组件。第一个组件只是一个基本组件,它呈现主图表(BubbleScatter) 以及可以滚动以产生不同年份气泡变化效果的年份。

父组件 - DateSelector

import React from "react";
import BubbleScatterPlot from "./InteractiveScatterPlot";

class DateSelector extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      year: 1800,
      changeTime: false,
      textInput: ""
    };
  }

  enableChange = e => {
    this.setState({ changeTime: true });
  };

  disableChange = () => {
    this.setState({ changeTime: false });
  };

  // Update data on scrolling over date text
  scrollDate = (e, d) => {
    e.persist();
    const dateInterval = 1; // As data increases every 20 years
    const startDate = 1800;
    const endDate = 2008;
    const total_year_count =
      Math.ceil((endDate - startDate) / dateInterval) + 1; // 2008 - 1800 / 20
    const diff = this.state.textInput.offsetWidth / total_year_count;

    let { changeTime } = this.state;
    let currX = e.pageX - this.state.textInput.offsetLeft;
    let currentDate = startDate + dateInterval * Math.floor(currX / diff);
    if (changeTime && currentDate !== this.state.year) {
      this.setState((prevState, props) => ({
        year: Math.min(currentDate, 2008) // Set the date
      }));
    }
  };

  render() {
    return (
      <div className="WealthOfNations">
        <BubbleScatterPlot
          height={this.props.height}
          width={this.props.width}
          margin={this.props.margin}
          year={this.state.year}
        />
        <span
          ref={textInput => {
            this.state.textInput = textInput;
          }}
          onMouseEnter={this.enableChange}
          onMouseMove={this.scrollDate}
          onMouseLeave={this.disableChange}
          style={{
            position: "relative",
            width: "fit-content",
            color: "#9e9e9e63",
            background: "#e8e8e842",
            textAlign: "center",
            cursor: "ew-resize",
            fontSize: "6em",
            marginLeft: "80%"
          }}
        >
          {`${this.state.year}`}
        </span>
      </div>
    );
  }
}

export default DateSelector;

实际的 BubbleScatterPlot

import { max as d3Max, extent as d3ArrayExtent } from "d3-array";
import React from "react";
import { scaleLinear, scaleLog } from "d3-scale";
import {
  axisBottom as d3AxisBottom,
  axisRight as d3AxisRight,
  axisTop as d3AxisTop,
  axisLeft as d3AxisLeft
} from "d3-axis";
import * as d3 from "d3";
import { select as d3Select } from "d3-selection";
import Data from "./nations.json"; // External file containing data
import { Motion, spring } from "react-motion";
import { interpolateValues } from "./Interpolate";

const calculateRange = (data, subFieldName) => {
  let min, max;
  data.forEach(item => {
    let currentRange = d3ArrayExtent(item[subFieldName], item => {
      return item[1];
    });
    if (min === undefined || currentRange[0] < min) min = currentRange[0];
    if (max === undefined || currentRange[1] > max) max = currentRange[1];
  });
  if (min === 0) return [1, max];
  return [min, max];
};

//Helper function to extract value corresponding to year for every country item
const getYearData = (data, subFieldName, year) => {
  let result = data[subFieldName].find(values => {
    return values[0] == year;
  });
  return result ? result[1] : interpolateValues(data, subFieldName, year);
};

//Helper function to calculate radius
const calcRadius = value => {
  return Math.sqrt(value);
};

// Main component
const BubbleScatterPlot = props => {
  let xScale = scaleLog()
    .domain(calculateRange(Data, "income"))
    .range([0, props.width * 1.1]);
  let yScale = scaleLinear()
    .domain(calculateRange(Data, "lifeExpectancy"))
    .range([props.height * 1.1, 0])
    .clamp(true);

  // Radius scale calculations based on population field
  let rRange = calculateRange(Data, "population");
  let rScale = scaleLinear()
    .domain([calcRadius(rRange[0]), calcRadius(rRange[1])])
    .range([0, 80]);
  let color = d3
    .scaleOrdinal(
      Data.map(d => d.region),
      d3.schemeCategory10
    )
    .unknown("black");

  return (
    <svg
      width={props.width + props.margin}
      height={props.height + props.margin}
      margin={props.margin}
      style={{ overflow: "visible" }}
    >
      <g
        className="margins"
        style={{ transform: `translate(${props.margin}px, ${props.margin}px)` }}
      >
        <Axis
          h="x-bottom"
          {...props}
          data={Data}
          Scale={xScale}
          translateY={yScale(0)}
        />
        <Axis h="y-left" {...props} data={Data} Scale={yScale} />
        <g className="scatter">
          {Data.map(circlePoint => (
            <Motion
              style={{
                // Get income and scale it for x-axis for all items for 2006
                x: spring(getYearData(circlePoint, "income", props.year), {
                  stiffness: 200,
                  damping: 50
                }),
                // Get lifeExpectancy and scale it for y-axis for all items for 2006
                y: spring(
                  getYearData(circlePoint, "lifeExpectancy", props.year),
                  {
                    stiffness: 200,
                    damping: 50
                  }
                ),
                // Get radius and scale it for x-axis for all items for 2006
                r: spring(getYearData(circlePoint, "population", props.year), {
                  stiffness: 150,
                  damping: 50
                })
              }}
              key={`${circlePoint.name}`}
            >
              {({ x, y, r }) =>
                // Due to nature of log Scale !== 1, we dont render these items
                (x !== 1 || y !== 1) && (
                  <circle
                    cx={xScale(x)}
                    cy={yScale(y)}
                    r={rScale(calcRadius(r))}
                    fill={color(circlePoint.region)}
                    style={{ opacity: "0.7" }}
                  />
                )
              }
            </Motion>
          ))}
        </g>
      </g>
    </svg>
  );
};

// Basic Axis unit
const AxisUnit = props => {
  let axis = props
    .orientation()
    .scale(props.Scale)
    .ticks(props.numberOfTicks || props.data.length / 2)
    .tickFormat(item => {
      return item;
    });

  return (
    <g
      className={"Axis "}
      ref={node => d3Select(node).call(axis)}
      style={{
        transform: `translate(${props.translateX || 0}px,${props.translateY ||
          0}px)`
      }}
    />
  );
};

const xAxis = "x",
  yAxis = "y",
  bottom = "bottom",
  top = "top",
  left = "left",
  right = "right";

const Axes = function axisHOC(WrapperComponent) {
  return class AxisHOC extends React.Component {
    constructor(props) {
      super(props);
    }
    getOrientation = () => {
      switch (this.props.h) {
        case xAxis + "-" + bottom:
          return {
            orientation: d3AxisBottom,
            className: xAxis + "-" + bottom,
            translateY: this.props.translateY || this.props.height
          };
        case yAxis + "-" + left:
          return {
            orientation: d3AxisLeft,
            className: yAxis + "-" + left,
            transform: -90
          };
        case xAxis + "-" + top:
          return {
            orientation: d3AxisTop,
            className: xAxis + "-" + top,
            translateY: this.props.translateY || this.props.height
          };
        case yAxis + "-" + right:
          return {
            orientation: d3AxisRight,
            className: yAxis + "-" + right,
            transform: 90
          };
        default:
          return { orientation: d3AxisLeft, className: yAxis + "-" + left };
      }
    };

    render() {
      const newProps = this.getOrientation();
      return <WrapperComponent {...this.props} {...newProps} />;
    }
  };
};

const Axis = Axes(AxisUnit);

export default BubbleScatterPlot;

除了平滑过渡和插入缺失数字之外,还添加了此插值函数:

import _ from "lodash";

export const interpolateValues = (data, subFieldName, year) => {
  // Get value of the last available year
  let prevVal = _.findLast(data[subFieldName], item => item[0] < year);
  // Get value of next available year
  let nextVal = data[subFieldName].find(item => item[0] > year);
  // Interpolation
  if (!prevVal && !nextVal) {
    return 1;
  } //In case country field is empty
  else if (!prevVal) {
    return nextVal[1];
  } //In case there is no available prior date
  else if (!nextVal) {
    return prevVal[1];
  } //In case there is no available next date
  else {
    let totalSteps = nextVal[0] - prevVal[0];
    let yearDistance = year - prevVal[0];
    //Linear interpolation
    return prevVal[1] + ((nextVal[1] - prevVal[1]) / totalSteps) * yearDistance;
  }
};