gpt4 book ai didi

javascript - 使用触摸设备的 d3 zoom 有时会表现得很奇怪(使用 IONIC)

转载 作者:搜寻专家 更新时间:2023-11-01 04:13:15 25 4
gpt4 key购买 nike

我有一个 d3 & Ionic 项目,可以在 github 上找到 here .我没有创建 Plunkr,因为必须通过 android 模拟器或 android 设备重现该错误。

我有一个可以平移和缩放的图表。我试图一次在图表中保留大约 60 个数据点。当用户到达“边缘”意味着域和数据点之间的距离约为 2 时,我会刷新数据,根据新域添加和删除数据。在异步调用之前,缩放似乎搞砸了,平移导致缩放就像我在捏一样。当平移时重新绘制图形时会发生这种情况。我不明白为什么。

我正在使用 Ionic 和 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 之上有一个层来处理事件。因为这一层不会在重绘时消失。

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

并在此缩放器元素上调用缩放处理程序:

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

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

关于javascript - 使用触摸设备的 d3 zoom 有时会表现得很奇怪(使用 IONIC),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46553982/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com