【问题标题】:d3: zoom on double y scale brokend3:放大双 y 刻度损坏
【发布时间】: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 函数没有正确计算y0y1,这就是为什么它只在y 轴上被破坏。但我不知道如何处理这种“双标”

标签: reactjs d3.js


【解决方案1】:

我认为这里有一些问题。

首先,handleZoom 在每个缩放事件上重复注册this.zoom。这不是必需的;调用者只需要注册一次。这可能是缩放几次后行为变得更加极端的部分原因(因为每个缩放事件实际上都被执行了多次)。

其次,updateZoom 使用configScale,但不使用yScale。对this.zoom.y 的调用绑定d3 的缩放以自动更新所提供的任何内容的域;我们不想更改 configScale 的域,但我们确实想更改 yScale 域。

最后,删除双重缩放可能会更简单,而是在将数据提供给图表之前对数据进行等效计算。这将大大简化您的所有逻辑。

此外,我发现将缩放存储为视口边界的方法有点奇怪;考虑改为存储 d3 zoom transform(并在绘制线和轴时应用变换)。

【讨论】:

  • 如果你能提供一些代码sn-ps会非常有帮助,特别是对于“删除双重缩放可能更简单”部分。
  • 仅供参考,这段代码(这是遗留的,不是我的)适用于我们的其他规模,例如线性和对数。所以handleZoom 反复注册this.zoom 的事实似乎并没有影响到其他尺度。
  • 仅供参考,d3 v3 没有 transform 属性 github.com/d3/d3-3.x-api-reference/blob/master/Zoom-Behavior.md
猜你喜欢
  • 1970-01-01
  • 2022-12-19
  • 1970-01-01
  • 1970-01-01
  • 2023-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多