使用 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;
}
};
我想在我的应用程序中使用 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;
}
};