【发布时间】:2021-07-27 13:42:59
【问题描述】:
我们有一个包含许多时间序列图表的应用程序(使用 d3 定制),其中每个图表显示来自工具的不同硬件的值。每个图表都可以以线性、对数或标准化比例显示(域=[0, 1])。我们最近被要求提供一个新的比例选项,我们称之为 config 比例,用户将提供一个范围(配置文件中的最小值/最大值),然后所有图表将具有相同的 y 轴范围。
我想既然我们需要转换数据两次,我会创建 2 个 d3 比例,如下所示:
- yScale => 从原始数据域 => 到配置范围
- configScale => 从配置范围 => 到像素
我尝试在https://jsfiddle.net/XeniaSiskaki/skf42jot/159/ 中创建一个复制品(仅显示新的 config 比例,带有随机数据点)。我遇到的最大问题(假设我对原始请求的解决方案是正确的)是 y 轴 上的缩放有些损坏。拖动图表主体或使用鼠标滚轮放大/缩小时,缩放速度非常快,并且数据和 y 轴似乎没有正确更新。重置y视口也不能正常工作,不知道是不是和缩放问题有关
const zoom = ['x', 'y'];
const range = [-10, 10];
const sampleDate = new Date('2020-01-01T09:00:00.000Z').getTime();
const innerWidth = 390;
const innerHeight = 278;
const chartData = {
a: [
{
v: 0,
t: sampleDate - 10000
},
{
v: -1,
t: sampleDate - 9000
},
{
v: 0.6,
t: sampleDate - 8000
},
{
v: 1,
t: sampleDate - 7000
},
{
v: -2,
t: sampleDate - 6000
},
{
v: 10,
t: sampleDate - 5000
},
{
v: 0.8,
t: sampleDate - 4000
},
{
v: 5.3,
t: sampleDate - 3000
},
{
v: 0,
t: sampleDate - 2000
},
{
v: 1.2,
t: sampleDate - 1000
},
{
v: 3,
t: sampleDate
},
{
v: 4.5,
t: sampleDate + 1000
},
{
v: 10,
t: sampleDate + 2000
}
],
b: [
{
v: 0.8,
t: sampleDate - 10000
},
{
v: 11,
t: sampleDate - 9000
},
{
v: -7.9,
t: sampleDate - 8000
},
{
v: 22.1,
t: sampleDate - 7000
},
{
v: -0.3,
t: sampleDate - 6000
},
{
v: 10,
t: sampleDate - 5000
},
{
v: 1.3,
t: sampleDate - 4000
},
{
v: 4.3,
t: sampleDate - 3000
},
{
v: 0,
t: sampleDate - 2000
},
{
v: -14.4,
t: sampleDate - 1000
},
{
v: 3,
t: sampleDate
},
{
v: -0.1,
t: sampleDate + 1000
},
{
v: 10,
t: sampleDate + 2000
}
]
};
const allPoints = chartData.a.concat(chartData.b);
class Chart extends React.Component {
constructor(props) {
super(props);
const [x0, x1] = d3.extent(allPoints.map(({ t }) => t));
this.state = {
zoom: 1,
viewport: {
x0,
x1
}
};
this.zoomPane = React.createRef();
this.svg = React.createRef();
this.line = d3.svg
.line()
.interpolate('linear')
.x(({ t }) => this.xScale(t))
.y(({ v }) => this.configScale(this.yScale(v)));
}
render() {
return (
<div>
<div>
<button onClick={this.handleZoomChange.bind(this)}>
Set zoom to {zoom[this.getNextZoomIndex()]}
</button>
<button onClick={this.resetViewport.bind(this)}> Reset viewport </button>
</div>
<div
style={{
height: 300
}}>
<svg width={450} height={300} ref={this.svg}>
<rect
ref={this.zoomPane}
style={{
cursor: 'move'
}}
width={innerWidth}
height={innerHeight}
fill='transparent'
transform='translate(55,0)'>
</rect>
<defs>
<clipPath id={'clipId'}>
<rect x={5} y={5} width={innerWidth - 5} height={innerHeight - 5} />
</clipPath>
</defs>
<g className='x axis' transform={'translate(55,281)'} />
<g className='y axis' transform={'translate(55,0)'} />
<g
clipPath={'url(#clipId)'}
width={innerWidth}
height={innerHeight}
transform={'translate(55,0)'}>
{this.renderLines()}
</g>
</svg>
</div>
</div>
);
}
UNSAFE_componentWillMount() {
this.setupScales();
this.updateScaleDomains();
}
componentDidMount() {
this.xAxis = d3.svg
.axis()
.orient('bottom')
.ticks(450 / 150)
.tickFormat(date => new Date(date).toISOString());
this.yAxis = d3.svg.axis().orient('left');
this.zoom = d3.behavior.zoom().on('zoom', this.handleZoom.bind(this));
this.updateAxes();
this.updateZoom();
d3.select(this.zoomPane.current).call(this.zoom);
}
UNSAFE_componentWillUpdate(nextProps, nextState) {
const { viewport, zoom } = this.state;
if (!_.isEqual(nextState.viewport, viewport)) {
this.updateScaleDomains(nextState);
}
}
componentDidUpdate(prevProps, prevState) {
if (!_.isEqual(prevState.viewport, this.state.viewport)) {
this.updateAxes();
if (prevState.viewport.y0 !== this.state.viewport.y0) {
this.updateZoom();
}
} else if (prevState.zoom !== this.state.zoom) {
this.updateZoom();
}
}
setupScales() {
this.xScale = d3.time.scale().range([10, 380]);
this.yScale = d3.scale.linear().range(range);
this.configScale = d3.scale.linear().domain(range).range([263, 15]);
}
updateScaleDomains(state = this.state) {
const { x0, x1, y0, y1 } = state.viewport;
if (x0 && y0) {
this.xScale.domain([x0, x1]);
this.xScale.range([10, 380]);
this.yScale.domain([y0, y1]);
this.yScale.range(range);
return;
}
if (x0) {
this.xScale.domain([x0, x1]);
} else {
this.xScale.domain(d3.extent(allPoints.map(({ t }) => new Date(t))));
this.xScale.range([10, 380]);
}
if (y0) {
this.yScale.domain([y0, y1]);
} else {
this.yScale.domain(d3.extent(allPoints.map(({ v }) => v)));
this.yScale.range(range);
}
}
updateZoom() {
const z = zoom[this.state.zoom];
if (z === 'x') {
this.zoom.x(this.xScale);
const x0 = this.xScale.invert(10);
const x1 = this.xScale.invert(innerWidth - 10);
const maxScale = (x1.getTime() - x0.getTime()) / 3600;
this.zoom.scaleExtent([0, maxScale]);
} else {
this.zoom.x(d3.scale.identity());
this.zoom.scaleExtent([0, Infinity]);
}
if (z === 'y') {
this.zoom.y(this.configScale);
} else {
this.zoom.y(d3.scale.identity());
}
}
resetViewport() {
this.setState(({ viewport }) => ({
viewport: _.omit(viewport, 'y0', 'y1')
}));
}
updateAxes() {
const svg = d3.select(this.svg.current);
this.xAxis.scale(this.xScale).tickSize(-innerHeight);
this.yAxis.scale(this.configScale).tickSize(-innerWidth);
svg.select('.x.axis').call(this.xAxis);
svg.select('.y.axis').call(this.yAxis);
}
getViewport() {
return {
x0: this.xScale.invert(10),
x1: this.xScale.invert(380),
y0: this.yScale.invert(this.configScale.invert(263)),
y1: this.yScale.invert(this.configScale.invert(15))
};
}
handleZoom() {
d3.select(this.zoomPane.current).call(this.zoom);
this.setState({
viewport: this.getViewport()
});
}
handleZoomChange() {
this.setState({
zoom: this.getNextZoomIndex()
});
this.updateZoom();
}
getNextZoomIndex() {
return this.state.zoom + 1 >= zoom.length ? 0 : this.state.zoom + 1;
}
renderLines() {
return Object.keys(chartData).map(key => {
const d = this.line(chartData[key]);
if (!d) {
return null;
}
return (
<path
d={d}
strokeLinecap='round'
strokeLinejoin='bevel'
fill='none'
strokeWidth={1}
stroke='#000000'
key={key} />
);
});
}
}
ReactDOM.render(<Chart />, document.querySelector('#app'));
svg {
font: 10px sans-serif;
}
button {
width: 120px;
margin: 0 15px;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
shape-rendering: crispEdges;
}
.axis path {
display: none;
}
.tick line {
stroke: rgba(220, 220, 220, 0.6);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="app"></div>
PS:使用 d3 v3
【问题讨论】:
-
只是为了澄清。基本上你的变焦不能正常工作?我注意到它不依赖于鼠标滚轮的方向。它总是缩小,这也非常快。我说的对吗?
-
@YogeshYadav 是的,缩放有问题。据我所知,它还会放大,不仅仅是缩小,而且不一致、快速并且扭曲了数据。可能是
getViewport函数没有正确计算y0和y1,这就是为什么它只在y 轴上被破坏。但我不知道如何处理这种“双标”