D3带触摸设备的缩放有时会怪异(使用离子)



我有一个d3&可以在Github上找到的离子项目。我没有创建plunkr,因为必须通过Android模拟器或Android设备复制该错误。

我的图形可以竖起和缩放。我试图一次将大约60个数据点保留在图中。当用户达到"边缘",这意味着域和数据点之间的距离大约为2,然后我根据新域刷新数据附加和删除数据。在异步调用之前,变焦似乎被弄乱了,平移使变焦的作用好像我在捏。当平台时,这将发生这种情况。我不知道为什么。

我正在使用离子和D3:这就是我的缩放代码看起来

图形代码

import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Content, LoadingController, NavController, PopoverController } from 'ionic-angular';
import { UsageLayer } from './usage-layer';
import { Observable } from 'rxjs/Rx';
import { UsageService } from './usage.service';
import * as Utils from './utils';
import * as d3 from 'd3';
import *  as moment from 'moment';
@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  constructor(public navCtrl: NavController,private el: ElementRef,private usageService: UsageService) {}
        //public properties
        @ViewChild('loader') loader: any;
        @ViewChild(Content) content: Content;
        billPeriod: any;
        costOverlay: boolean;
        graphType: string = 'usage';
        isWaiting: boolean = false;
        viewType: string;
        monthlyData: any;
        dailyData: any;
        minData: any;
        //private properties
        /**
         * Lets the Component know which whether or not to initialize the imported graphs
         */
        // graph properties
        private chart: any;
        private data: any;
        private numOfDaysInDomain: number;
        private graphCanvas: any;
        private isZooming: boolean  = false;
        private svg: any;
        private chartHeight: any;
        private height: number;
        private margin: any;
        private mode: string = 'daily';
        private selectedNode: any;
        private viewEl: any;
        private viewPortData: any;
        private xAxis: any;
        private yAxis: any;
        private xScale: any;
        private x2Scale: any;
        private usageLayer: UsageLayer;
        private width: number;
        private yScale: any;
        private zoom: any;
        private k: number;

        ngOnInit() {
            this.billPeriod = {start:'2016-10-07T22:17:48-05:00',end:'2016-11-07T22:17:48-05:00'};
            let buffer:any = 15;
            let bUnit:string = 'days';
            let queryDates = {
                start: moment(this.billPeriod.start).subtract(buffer,bUnit).format(),
                end: moment(this.billPeriod.end).add(buffer,bUnit).format()
            };
            let query = this.usageService.queryBuilder('daily',"SELECT * FROM ${{tablename}} where date(kDateTime) > date('" + queryDates.start + "') AND date(kDateTime) <= date('" + queryDates.end + "')");
            this.viewEl = d3.select(this.el.nativeElement);
            this.usageService.queryDaily(query).subscribe((x)=>{
                console.log('data returned',x);
                this.initializeGraph(x,this.billPeriod,this.viewEl,this.content);
            },(err)=>{
                console.log(err);
            })
        }
        /**
         * @name initializeGraph
         * @description Initialize the graph, main canvas (g element), and layers.
         * The canvas is referring to the main g element that holds all of the layers (usage,weather, cost bars).
         * The canvas is appended as a G Element to the SVG element.
         */
        initializeGraph(dailyData, billPeriod, viewElement,content:Content) {
            this.dailyData = dailyData;
            this.viewEl = viewElement;
            this.content = content;
            this.data = this.dailyData;
            this.mode = 'daily';
            this.billPeriod = billPeriod;
            this.costOverlay = true;
            this.calculateChartDimensions();
            this.initializeScales();
            this.numOfDaysInDomain = Utils.getNumberOfDaysInDomain(this.xScale.domain()[0], this.xScale.domain()[1]);
            this.initializeCanvasElement();
            this.initializeDefs();
            this.initializeCanvasLayers();
            this.initializeGraphAxis();
            this.initializeZoom();
            this.zoomAndPanTo('bill');
            setTimeout(()=>{
                this.triggerZoomLoader('hide');   
            }, 3000)
        }
        /**
         * @name initializeCanvasElement
         * @description The Canvas refers to the G Element that holds all of the 
         * graph layers. Including
         * -Usage Layer
         * -Weather Layer
         * -Cost Bars Layer
         */
        initializeCanvasElement() {
            this.graphCanvas = this.svg.append("g")
                .attr("class", "graphCanvas")
                .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");
        }
        /**
         * @name initializeDefs
         * @description Initializes several SVG defs.
         * Gradients for Usage Layers
         * Gradients for Cost Bars Layers
         * ClipPath for canvas
         */
        initializeDefs() {
            let defs = this.graphCanvas.append('defs');
            // append clipping path
            this.svg.append('defs').append("clipPath")
                .attr("id", "clip")
                .append("rect")
                .attr("width", this.width)
                .attr('transform', 'translate(0,-20)')
                .attr("height", this.height + 20);
            // append usage path gradient
            let gradient = defs
                .append('linearGradient')
                .attr('id', 'gradient')
                .attr('x1', '0%')
                .attr('y1', '0%')
                .attr('x2', '0%')
                .attr('y2', '100%');
            gradient.append("stop")
                .attr("offset", "0%")
                .attr("stop-color", "#51D0D7")
                .attr("stop-opacity", 1);
            gradient.append("stop")
                .attr("offset", "100%")
                .attr("stop-color", "#9FE25E")
                .attr("stop-opacity", 1);
            // append cost bar gradients
            let barGradient = defs
                .append('linearGradient')
                .attr('id', 'bar-gradient')
                .attr('x1', '0%')
                .attr('y1', '0%')
                .attr('x2', '0%')
                .attr('y2', '100%');
            barGradient.append("stop")
                .attr("offset", "0%")
                .attr("stop-color", "#AAE8EC")
                .attr("stop-opacity", 1);
            barGradient.append("stop")
                .attr("offset", "50%")
                .attr("stop-color", "#C2EEDF")
                .attr("stop-opacity", 1);
            barGradient.append("stop")
                .attr("offset", "100%")
                .attr("stop-color", "#E2F6D9")
                .attr("stop-opacity", 1);
            var weatherGradient = defs
                .append('linearGradient')
                .attr('id', 'weather-gradient')
                .attr('x1', '0%')
                .attr('y1', '0%')
                .attr('x2', '0%')
                .attr('y2', '100%');
            weatherGradient.append("stop")
                .attr("offset", "0%")
                .attr("stop-color", "#FFB4AA") //#FFAD27
                .attr("stop-opacity", 1);
            weatherGradient.append("stop")
                .attr("offset", "100%")
                .attr("stop-color", "#AADAFF") //#FFAD27
                .attr("stop-opacity", 1);
        }
        /**
         * @name initializeCanvasLayers
         * @description Initializes all of the graph layers.
         * UsageLayer - showing the kWh usage
         * WeatherLayer - showing the high and low temperature line and area
         * CostBarsLayer - showing the cost bar correlation to usage
         */
        initializeCanvasLayers() {
            this.usageLayer = new UsageLayer(this.data, this.graphCanvas, this.viewEl, this.height, { x: this.xScale, y: this.yScale }, this.mode);
        }
        /**
         * @name initializeGraphAxis
         * @descrpition initializes the canvas axis
         */
        initializeGraphAxis() {
            // setup axis
            this.xAxis = d3.axisBottom(this.xScale).tickSize(0).tickFormat(d3.timeFormat('%b %e')).ticks(5);
            this.yAxis = d3.axisLeft(this.yScale).tickValues(this.yScale.domain()).ticks(3).tickSize(0);
            this.graphCanvas.append("g")
                .attr("class", "axis axis--x")
                .attr("transform", "translate(0," + (this.height + 90) + ")")
                .call(this.xAxis);
            this.graphCanvas.append("g")
                .attr("class", "axis axis--y axis--kWh-y")
                .attr("transform", "translate(0," + (80) + ")")
                .call(this.yAxis);
            // change the axis label to show kWh
            setTimeout(() => {
                this.addkWhToAxis(this.graphCanvas.select('.axis--kWh-y'));
            }, 500);
        }
        /**
         * @name toggleCostOverlay
         * @descriptions Toggles the cost overlay bars for analysis
         */
        toggleCostOverlay() {
            this.costOverlay = !this.costOverlay;
            d3.select('.axis-cost').classed('on', this.costOverlay);
            d3.selectAll('.cost-bar').classed('on', this.costOverlay);
        }
        /**
         * @name zoomAndPanTo
         * @description Zooms to the identified levels
         * @param {string} level - The desired zoom level
         * Possible levels are - yearly, bill, weekly, daily
         */
        zoomAndPanTo = (level: string) => {
            let startDate,
                endDate,
                k,
                tx;
            if (level == 'yearly') {
                // Zoom all the way out
                this.svg.call(this.zoom.scaleBy, 0);
                return;
            } else {
                if (level == 'bill') {
                    // Get end date
                    endDate = moment(this.billPeriod.end);
                    // Get start date
                    startDate = moment(this.billPeriod.start);
                } else {
                    // if is weekly than add 7 days
                    // if is daily than add one day
                    let amountOfDaysToAdd = level === 'weekly' ? 7 : 1;
                    // Get start and end date
                    endDate = moment(this.xScale.domain()[1]);
                    startDate = moment(endDate).subtract(amountOfDaysToAdd, 'days');
                }
            }
            // Get scale k
            k = this.width / (this.xScale(endDate) - this.xScale(startDate));
            // Get transform value
            this.svg.call(this.zoom.scaleBy, k);
            tx = 0 - k * this.xScale(startDate);
            // if daily mode don't translate
            if (level == 'daily') return;
            this.svg.call(this.zoom.translateBy, tx, 0);
        }
        // private methods
        /**
         * @name changeDataSource
         * @description Changes the data source. There are three important data arrays: Minute, Daily, and Monthly data. 
         * As the user zooms in or out, the data source is changed. This methods accepts the new data and uses it to redraw the graphs
         */
        private changeDataSource(data) {
            var yAxisEl,
                yAxisWeather;
            this.drawXAxisTicks();
            this.yScale.domain([0, d3.max(data, (d: any) => { return d.kWh; })]);
            this.usageLayer.redraw(this.mode, data, { x: this.xScale, y: this.yScale });
            if (this.mode != 'minute') {
                this.costOverlay = true;
            } else {
                if (this.costOverlay === true) this.toggleCostOverlay();
            }
            this.yAxis = d3.axisLeft(this.yScale).tickValues(this.yScale.domain()).ticks(3).tickSize(0);
            yAxisEl = this.graphCanvas.select('.axis--kWh-y').call(this.yAxis);
            this.addkWhToAxis(yAxisEl);
            this.triggerZoomLoader('hide');
            this.isZooming = false;        
        }
        /**
         * @name addkWhToAxis
         * @description Manually adds the text kWh to the left axis of the graph for aesthetics.
         */
        private addkWhToAxis(yAxis) {
            var yAxisHeight = yAxis.node().getBBox().height;
            yAxis.append('g')
                .attr('class', 'tick')
                .attr('transform', 'translate(0,' + (yAxisHeight / 2) + ')')
                .append('text').attr('fill', '#000').html('kWh');
        }
        /**
         * @name initializeZoom
         * @description initializes the zoom generator
         */
        private initializeZoom = () => {
            this.zoom = d3.zoom()
                .scaleExtent([1, this.numOfDaysInDomain * 12])
                .translateExtent([[0, 0], [this.width, this.height]])
                .extent([[0, 0], [this.width, this.height]])
                .on("zoom", this.zoomed)
            // setup zoom on svg
            this.svg.call(this.zoom);
            this.svg.on("mousedown.zoom", null)
            this.svg.on("mousewheel.zoom", null)
            this.svg.on("mousemove.zoom", null)
            this.svg.on("DOMMouseScroll.zoom", null)
            this.svg.on("dblclick.zoom", null)
        }
            /**
         * @name initializeScales
         * @description initializes the zoom generator
         */
        private initializeScales() {
            // setup scales
            this.xScale = d3.scaleTime().range([0, this.width]);
            this.x2Scale = d3.scaleTime().range([0, this.width]);
            this.yScale = d3.scaleLinear().range([this.height, 0]);
            let xDomain = d3.extent(this.data, (d: any) => { return Utils.getDataPointDate(d); });
            let yDomain = [0, d3.max(this.data, (d: any) => { return d.kWh; })];
            this.xScale.domain(xDomain);
            this.yScale.domain(yDomain);
            this.x2Scale.domain(this.xScale.domain());
        }
        /**
         * @name calculateChartDimensions
         * @description calculate the height and width of the chart
         * @returns {Object}
         */
        private calculateChartDimensions() {
            let contentDimensions = this.content.getContentDimensions();
            let contentViewHeight = contentDimensions.contentHeight;
            this.svg = this.viewEl.select('svg#svgChart');
            this.chart = this.viewEl.select('div.chart');
            let chartHeight = this.chart.node().offsetHeight;
            this.svg.attr('height', contentViewHeight - 84 - 50);
            let chartWidth = contentDimensions.contentWidth;
            this.margin = { top: 20, right: 40, bottom: 30, left: 40 };
            // assign to global variables    
            this.width = chartWidth - this.margin.left - this.margin.right,
                this.height = +this.svg.attr("height") - this.margin.top - this.margin.bottom - 80;
            this.svg.attr('width', chartWidth);
        }
        /**
         * @name drawXAxisTicks
         * @description Determines how many ticks and what date format to show them in based upon the data granulatiry.
         * Calculates the amount of days between the start and end date to determine the format and number.
         */
        private drawXAxisTicks() {
            let diff = Utils.getNumberOfDaysInDomain(this.xScale.domain()[0], this.xScale.domain()[1]);
            let tickFormat: string;
            let tickNumber: number;
            if (this.mode === 'minute') {
                tickFormat = '%b %e %I:%M %p'; tickNumber = 2;
            }
            else if (diff >= 100) {
                tickFormat = '%b'; tickNumber = 7;
            } else if (diff < 100) {
                tickFormat = '%b %e'; tickNumber = 4;
            } else if (diff <= 7 && diff > 4) {
                tickFormat = '%b %e'; tickNumber = 6;
            } else if (diff == 4) {
                tickFormat = '%b %e'; tickNumber = 3;
            } else if (diff < 4) {
                tickFormat = '%b %e %I:%M %p'; tickNumber = 2;
            }
            // Assign Tick Format and Number.
            this.xAxis.tickFormat(d3.timeFormat(tickFormat)).ticks(tickNumber);
            // Apply new Format
            this.graphCanvas.select(".axis--x").call(this.xAxis);
        }
        /**
         * @name reDrawGraphElements
         * @description Draws or re-draws all graph elements, based on current xScales and generators.
         */
        private reDrawGraphElements(data?,scales?){
            this.redrawUsageGraphElements(data,scales);
        }
        private redrawUsageGraphElements(data?,scales?) {
            this.usageLayer.redraw(this.mode,data,scales);
        }
        /**
         * @name isChangeMode
         * @description Determines whether to change current mode
         */
        private isChangeMode():boolean {
            // // get distance between domains , x1 and x2
            let diff = Utils.getNumberOfDaysInDomain(this.xScale.domain()[0], this.xScale.domain()[1]);
            if (diff > 120 && this.mode !== 'monthly') {
                this.mode = 'monthly';
                return true;
            } else if ((diff <= 120 && diff > 2) && this.mode !== 'daily') {
                this.mode = 'daily';
                return true;
            } else if (diff <= 2 && this.mode !== 'minute') {
                this.mode = 'minute';
                return true;
            } else {
                return false;
            }
        }
        /**
         * @name isRefreshThreshold
         * @description determin whether not if data threshold should be refreshed based on the extreminities
         * of the domain and data. We will compare (data[0] and domain[0]) and (data[data.length - 1] and domain[1]) to find out if the threshold has been
         * trangressed
         */
        isRefreshThreshold():boolean{
            // TODO determine threshold for yearly mode
            if(this.mode === 'monthly') return false;
            let domain,
                threshold, 
                x1Diff, 
                x2Diff;
            if(this.mode === 'minute'){
                threshold = {
                    unit: 'seconds',
                    value: '86400'
                };
            } else if(this.mode === 'daily'){
                threshold = {
                    unit: 'days',
                    value: '2'
                };
            }
            domain = this.xScale.domain();
            x1Diff = moment(domain[0]).diff(this.data[0].kDateTime,threshold.unit);
            x2Diff = moment(this.data[this.data.length-1].kDateTime).diff(domain[1],threshold.unit);
            return (x1Diff <= threshold.value || x2Diff <= threshold.value);
        }


        /**
         * @name zoomed
         * @description Callback for zoom functionality.
         */
        private zoomed = () => {
            // if(this.isZooming){
            //     return;
            // }
            let t = d3.event.transform;
            console.log(t);
            if (isNaN(t.k)) return;
            this.xScale.domain(t.rescaleX(this.x2Scale).domain());
            this.drawXAxisTicks();
            // Do we change the mode
            if(this.isChangeMode()){
                console.log('changedMode');
                this.getData().subscribe((x:any)=>{
                    this.data = x;
                    this.changeDataSource(this.data);
                });
            } else if(this.isRefreshThreshold()) {
                console.log('refreshing threshold');
                this.isZooming = true;
                this.triggerZoomLoader('show');
                this.getData().subscribe((x:any)=>{
                    this.data = x;
                    this.changeDataSource(this.data);
                });
            } else {
                console.log('didn't do anything');
                // plainly render the graph updating it regularly
                this.reDrawGraphElements();
            }
        }
        private getData(){
            // if the mode is yearly then return the data immediately
            // return immediate data because we are not buffering data right now
            if(this.mode == 'monthly'){
                this.data = this.monthlyData;
                return Observable.of(this.data);
            }
            // if(this.mode == 'daily'){
            //     this.data = this.dailyData;
            //     return Observable.of(this.data);
            // }
            let xMin,
                xMax,
                buffer: number,
                bufferUnit: string = 'seconds',
                bufferXmin,
                bufferXmax,
                numberOfPoints,
                distanceBtwnXminXmax,
                dataLength,
                domain;
                domain = this.xScale.domain();
                xMin = moment(domain[0]);
                xMax = moment(domain[1]);
                // calculate buffer
                if(this.mode == 'daily'){
                    bufferUnit = 'days';
                    distanceBtwnXminXmax = xMax.diff(xMin,bufferUnit);
                    buffer = 100;
                } else if (this.mode == 'minute'){
                    bufferUnit = 'seconds';
                    distanceBtwnXminXmax = xMax.diff(xMin,bufferUnit);
                    buffer = 86400;
                }
                bufferXmin = xMin.subtract(buffer,bufferUnit);
                bufferXmax = xMax.add(buffer,bufferUnit);

                let query = this.usageService.queryBuilder(this.mode,"SELECT * FROM ${{tablename}} where date(kDateTime) > date('" + bufferXmin.format() + "') AND date(kDateTime) <= date('" + bufferXmax.format() + "')");
                return this.mode === 'minute' ? this.usageService.queryMin(query) : this.usageService.queryDaily(query);
        }

        /**
         * @name triggerZoomLoader
         * @description Hide/Show the zoom loader
         */
        triggerZoomLoader(action:string = 'show'){
            if(action == 'show'){
                this.loader.nativeElement.classList.remove('hidden');
            } else {
                this.loader.nativeElement.classList.add('hidden');            
            }
        }
}
export interface BillPeriod {
    start: string,
    end: string
}

redraw函数在您的代码中调用时,就会发生问题。以某种方式D3在重新绘制后不会发射touchendtouchcancel事件。即使移除了一根手指,似乎仍在保持联系。因此,接下来的开始触摸使其成为多点触摸。这可能是D3侧或Webkit本身中的错误。

d3-zoom/src/zoom.js文件中touchStarted功能中的此更改将其修复为2个触摸。无法测试更多的触摸。

将此行更改为:

  if (!g.touch0) g.touch0 = p, started = true;

这个:

  if (!g.touch0 || (event.touches.length == 1 && touches.length == 1)) g.touch0 = p, started = true;

,如果您可以在D3存储库中以可重复的步骤和一个干净的代码示例打开问题,那将是很好的。

另一个解决方案

而不是在SVG上处理缩放,而是在SVG上方有一层以处理事件。因为这一层不会在Redraw上消失。

  <div class="chart">
    <div style="position: absolute; top: 0; bottom: 0; left: 0; right: 0;" class="zoomer"></div>
    <svg id="svgChart"></svg>
  </div>

并在此Zoomer元素上调用Zoom处理程序:

    private initializeZoom = () => {
        ....
        this.zoomer.call(this.zoom);
        ....
    }

但此层也阻止了点击事件,因此圆圈不会显示。阅读更多有关如何处理这种情况的D3文档。

最新更新