我有一个折线图 - 想要调整它以采用不同的样式/创建趋势线。用轴线显示当前时间并添加趋势线/圆点。
在顶部生成遵循相同比例的图标/警报,并合并到顶部的图表。
https://codesandbox.io/s/strange-surf-wyd6cj
import React from 'react'
import * as d3 from 'd3'
import './LineChart.scss'
class LineChart extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
this.state = {
data: this.props.data ? this.props.data : [],
theme: this.props.theme
? this.props.theme
: ['#bde0fe', '#2698f9', '#71bcfd', '#f1f8fe'],
}
}
componentWillReceiveProps(nextProps) {
// You don't have to do this check first, but it can help prevent an unneeded render
if (nextProps.data !== this.state.data) {
//console.log("PROPS HAVE CHANGED FOR CHART");
this.setState({data: nextProps.data})
this.buildChart()
}
}
componentDidMount() {
this.buildChart()
}
buildChart() {
var $this = this.myRef.current
d3.select($this).selectAll('svg').remove()
var data = this.state.data
//console.log("DATA", data)
var hasLabels = false
data.forEach((item, index) => {
if (item.id.length > 0) {
hasLabels = true
}
})
var yAxisLabel = this.props.yAxisLabel
//console.log("hasLabels", hasLabels)
var width = parseInt(this.props.width, 10),
height = parseInt(this.props.height, 10)
var color = d3.scaleOrdinal().range(this.state.theme)
var parseTime = d3.timeParse('%Y%m%d')
var margin = {
top: 20,
right: hasLabels ? 95 : 25,
bottom: 30,
left: 45,
},
w = width - margin.left - margin.right,
h = height - margin.top - margin.bottom
var x = d3.scaleTime().range([0, w])
var y = d3.scaleLinear().range([h, 0])
var valueline = d3
.line()
.curve(d3.curveBasis)
.x(function (d) {
return x(new Date(d.date))
})
.y(function (d) {
return y(d.value)
})
//valueline.curve(d3.curveBasis)
x.domain([
d3.min(data, function (c) {
return d3.min(c.values, function (d) {
return new Date(d.date)
})
}),
d3.max(data, function (c) {
return d3.max(c.values, function (d) {
return new Date(d.date)
})
}),
])
y.domain([
d3.min(data, function (c) {
return d3.min(c.values, function (d) {
return d.value
})
}),
d3.max(data, function (c) {
return d3.max(c.values, function (d) {
return d.value
})
}),
])
var svg = d3
.select($this)
.append('svg')
.attr('width', w + margin.left + margin.right)
.attr('height', h + margin.top + margin.bottom)
.append('g')
.attr('class', 'linechart')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
var lines = svg.append('g').attr('class', 'lines')
var line = lines
.selectAll('.line')
.data(data)
.enter()
.append('g')
.attr('class', 'line')
line
.append('path')
.attr('d', function (d) {
return valueline(d.values)
})
.attr('stroke', function (d, i) {
return color(i)
})
line
.append('text')
.datum(function (d) {
return {
id: d.id,
value: d.values[d.values.length - 1],
}
})
.attr('transform', function (d) {
return (
'translate(' +
x(new Date(d.value.date)) +
',' +
y(d.value.value) +
')'
)
})
.attr('x', 3)
.attr('dy', '0.35em')
.text(function (d) {
return d.id
})
////return c.date;
var minDate = d3.min(data, function (c) {
return d3.min(c.values, function (d) {
return d.date
})
})
var maxDate = d3.max(data, function (c) {
return d3.max(c.values, function (d) {
return d.date
})
})
//console.log("minDate", minDate);
//console.log("maxDate", maxDate);
var difference = dateDiffInDays(
new Date(minDate.toString()),
new Date(maxDate.toString()),
)
//console.log(difference + ' days')
let tick = d3.timeDay.every(1)
if (difference < 1) {
tick = d3.timeHour.every(2)
}
if (difference > 1 && difference < 7) {
tick = d3.timeDay.every(1)
}
if (difference > 7 && difference < 30) {
tick = d3.timeWeek.every(1)
}
if (difference >= 30 && difference < 90) {
//console.log("jackpot")
tick = d3.timeMonth.every(1)
}
lines
.append('g')
.attr('transform', 'translate(0,' + h + ')')
.call(d3.axisBottom(x).ticks(tick))
lines
.append('g')
.call(d3.axisLeft(y))
.append('text')
.attr('class', 'ylabel')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '0.71em')
.text(yAxisLabel)
function dateDiffInDays(a, b) {
const _MS_PER_DAY = 1000 * 60 * 60 * 24
// Discard the time and time-zone information.
const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate())
const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate())
return Math.floor((utc2 - utc1) / _MS_PER_DAY)
}
}
render() {
return <div ref={this.myRef} className="LineChart" />
}
}
export default LineChart
这是一项非常简单的任务。我将列出实施步骤,而不是详细解释。
分组不是必需的,因为图标(点)将显示在同一层上。它们被展平为一维数据。
const flatData = []
data.forEach(({id: groupId, values}) => {
values.forEach((v) => {
flatData.push({groupId, ...v})
})
})
标签和图标将共享相同的位置。我将它们与
<g/>
元素分组在一起,并使用 .call()
语法轻松访问它们。
gDots
.selectAll('.dot-info')
.data(flatData, (d) => d.date)
.join((enter) =>
enter
.append('g')
.attr('class', 'dot-info')
.attr(
'transform',
(d) =>
// some data were placed in the same X position
// gave different Y per group to avoid collision
`translate(${x(d.date)},${d.groupId === 'New York' ? -30 : -60})`,
)
.call((g) =>
g
.append('circle')
.attr('r', 3)
// I've provided dots with colors, but it can be replaced with icon object if necessary.
.attr('fill', (d) => (d.groupId === 'New York' ? 'blue' : 'red')),
)
.call((g) =>
g
.append('text')
.text((d) => d.value)
.attr('transform', 'translate(0,10)')
.attr('dominant-baseline', 'hanging')
.attr('text-anchor', 'middle'),
),
)
最后,我添加了一个目标光标来模仿给定示例中的用户界面。由于其限制,它在 Codesandbox 上相当慢,但在裸项目上可能工作正常。
const lineCursor = svg
.append('g')
.call((g) =>
g
.append('line')
.attr('x1', 0)
.attr('y1', -60)
.attr('x2', 0)
.attr('y2', h)
.attr('stroke', 'black')
.attr('stroke-width', '1px'),
)
svg.on('mousemove', (ev) => {
// gave the same x margin as the chart
lineCursor.attr('transform', `translate(${ev.clientX - 45})`)
})
这是 codesandbox 上的演示。