- Java 双重比较
- java - 比较器与 Apache BeanComparator
- Objective-C 完成 block 导致额外的方法调用?
- database - RESTful URI 是否应该公开数据库主键?
我有一个 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 在重绘后不会触发 touchend
或 touchcancel
事件。即使移开一根手指,也仿佛还在触碰。所以下一个开始的触摸使它成为多点触摸。这可能是 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/
我在学习如何缩放方面经历了一段痛苦的时光。为了学习,我一直在尝试制作树状图缩放。我正在使用这个 jsfiddle 示例( http://jsfiddle.net/6kEpp/1/ )并尝试将其应用于没
使用 orbitcontrols.js(带有 THREE.js),我想在代码中实现与旋转鼠标滚轮相同的效果。例如,我想调用 camera.zoomIn() 之类的东西,让它向目标移动一段设定的距离。有
我已经创建了 map ,如 this link 所示并且运行良好。 但问题是,它只能以一种方式放大(只会变大)。我怎样才能让它以任何一种方式工作? 可能就像我们在 google map 上(加号 -
我正在寻找与 autocad ... 或 solidworks 类似的体验 基本上是内容大小(保持宽高比)Xsize * Ysize 并放大特定区域 并且我已经将 ruby/rails 作为我选择
在bootstrap的重置部分有一个 .clearfix { *zoom: 1; } 请问这里*zoom 和 zoom 有什么区别? 我很感激任何回答。 最佳答案 这是一个 css hack,这意味着
我在单独的子图中有 3 轴加速度计时间序列数据(t,x,y,z)的图,我想一起缩放。也就是说,当我在一个图上使用“缩放到矩形”工具时,当我释放鼠标时,所有 3 个图都会一起缩放。 以前,我只是使用不同
有没有一种简单的方法可以在页面刚加载时默认自动查看 Mapbox map 的所有标记...?这是我的 map :http://www.geometry.be/urbanmaestro/v7/非常感谢您
我已经使用 UIWebView 在 html 文件中设置了以下视口(viewport)标签。 现在,当用户选择时,我得到如图所示的效果。我想禁用此效果但仍然可以放大。 尝试将 UIWebView 的
我现在使用 Core-plot 进行 iPhone 图表开发。但它有一些我无法达到的要求。 我现在使用CorePlot 0.4,示例代码AAPLot来开发 请看下面的图片,然后你就会知道我的问题是什么
最新版本的 Google 地球附带了一项名为“缩放时自动倾斜”的功能。如果启用,Google Earth 会在您拉近表面时自动将相机倾斜到地平线。可以从 GUI 中禁用此功能(首选项 -> 导航选项卡
我找到了使用 rel="smallImage:image1.jpg" 等链接更改缩放图像的方法,但我是在运行时创建图像并将其添加到页面,所以页面首次加载时链接不会存在(这似乎需要存在)。当我在运行时生
我正在使用 ImageView 支持缩放。现在我正在扩展 ImageView 以便我可以在 canvas 上绘图。目前我有 setImageResource 设置一个 drawable 和在 onDr
在缩放行为上手动设置比例后,如何触发缩放事件? var zoom = d3.behavior.zoom() .scaleExtent([0.5, 4]) .on('zoom', onz
我有这个简单的测试代码: ul{ float:left; margin:0; list-style:none; paddi
我正在尝试实现一个具有以下外观的移动网站: 一个固定的标题 可滚动、可缩放的内容 首次加载页面时内容缩小 我一直在试验 IScroll 4,结果似乎不错,但有一个问题我找不到解决办法。我页面的内容是用
嗨,我是 iOS 初学者,在我的项目中,我以编程方式添加了一个 Collection View ,并且已成功添加。 我的要求是,当我单击 UICollectionViewCell 时,它将像下面的第二
遵循此处的建议:How to disable double click zoom for d3.behavior.zoom? 我在页面加载时禁用了双击缩放行为: https://bl.ocks.org
当用户使用鼠标滚轮滚入和滚出时,您可以调整缩放速度吗? 我的理解是 zoom.on (https://github.com/mbostock/d3/wiki/Zoom-Behavior#wiki-on
关于 css 样式和在浏览器中放大和缩小,我遇到了既有趣又奇怪的问题。 我创建了一个 Material ui 卡片,点击它时背景颜色会随着动画而改变。 动画效果很好,但如果您放大或缩小页面,浏览器会在
我将 JQTouch 用于 iPhone 应用程序。 JQtouch 默认情况下禁用捏合和缩放页面的可能性。对于一页(包含大图像),我需要启用捏合和缩放功能。这很容易: var viewport =
我是一名优秀的程序员,十分优秀!