这很奇怪。它也有点长,所以提前道歉。 更新 - 最终出现2个问题,请参阅下面的答案。
这是我的错误:EXCEPTION: this.svg.selectAll(...).data(...).enter is not a function
我有一个angular-cli客户端和一个节点api服务器。我可以使用observable从服务中检索states.json文件(下面的代码)。 d3喜欢该文件并显示预期的美国地图。
当我将api服务器中的服务目标从文件更改为bluemix-cloudant服务器时,我在客户端收到上述错误。
当我使用ngOnInit在一个变体中使用console.log输出时,最初mapData打印为一个空数组并且抛出错误。这是错误的明显来源,因为没有数据,但是Chrome调试程序会显示待处理的get请求。请求完成后,数据将在控制台中按预期打印。
map.component.ts:
import { Component, ElementRef, Input } from '@angular/core';
import * as D3 from 'd3';
import '../rxjs-operators';
import { MapService } from '../map.service';
@Component({
selector: 'map-component',
templateUrl: './map.component.html',
styleUrls: ['./map.component.css']
})
export class MapComponent {
errorMessage: string;
height;
host;
htmlElement: HTMLElement;
mapData;
margin;
projection;
path;
svg;
width;
constructor (private _element: ElementRef, private _mapService: MapService) {
this.host = D3.select(this._element.nativeElement);
this.getMapData();
this.setup();
this.buildSVG();
}
getMapData() {
this._mapService.getMapData()
.subscribe(
mapData => this.setMap(mapData),
error => this.errorMessage = <any>error
)
}
setup() {
this.margin = {
top: 15,
right: 50,
bottom: 40,
left: 50
};
this.width = document.querySelector('#map').clientWidth - this.margin.left - this.margin.right;
this.height = this.width * 0.6 - this.margin.bottom - this.margin.top;
}
buildSVG() {
this.host.html('');
this.svg = this.host.append('svg')
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
}
setMap(mapData) {
this.mapData = mapData;
this.projection = D3.geoAlbersUsa()
.translate([this.width /2 , this.height /2 ])
.scale(650);
this.path = D3.geoPath()
.projection(this.projection);
this.svg.selectAll('path')
.data(this.mapData.features)
.enter().append('path')
.attr('d', this.path)
.style('stroke', '#fff')
.style('stroke-width', '1')
.style('fill', 'lightgrey');
}
}
map.service.ts:
import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class MapService {
private url = 'http://localhost:3000/api/mapData';
private socket;
constructor (private _http: Http) { }
getMapData(): Observable<any> {
return this._http.get(this.url)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || {};
}
private handleError(error: any) {
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg);
return Promise.reject(errMsg);
}
}
这是否为异步功能,对数据的调用对d3来说需要太长时间?
我曾希望这个问题 Uncaught TypeError: canvas.selectAll(...).data(...).enter is not a function in d3会提供一些见解,但我没有看到任何见解。
非常感谢任何帮助或见解!
修改 以下是Chrome per Marks请求中标题部分的屏幕截图。响应选项卡显示正确作为GeoJSON对象发送的数据。我还将该响应复制到本地文件中,并将其用作具有正面结果的地图源。
到目前为止的数据测试:GeoJSON文件(2.1mb)
答案 0 :(得分:4)
我的猜测是,角度会混淆构造函数与请求返回时间之间map
元素的引用。我的建议是,当服务器的响应到达时,开始在svg
内构建ngAfterViewInit
,甚至更好。我相信这个问题主要是基于时机。当然,如果从服务器收到的数据没有格式错误,您实际上可以在控制台中记录一组很好的映射数据。
如果视图尚未就绪,并且document.querySelector('#map').clientWidth
在#map
内,map.component.html
将返回0或未定义。
当您处理模板中的元素时,请始终使用ngAfterViewInit
生命周期钩子。
除此之外,您似乎没有在组件内部使用任何角度变化检测。我会建议你,以防止对你的元素的任何干扰,从ChangeDetectorRef
:
@Component({
selector: 'map-component',
templateUrl: './map.component.html',
styleUrls: ['./map.component.css']
})
export class MapComponent implement AfterViewInit {
private mapData;
constructor (
private _element: ElementRef,
private _mapService: MapService,
private _changeRef: ChangeDetectorRef
){}
ngAfterViewInit(): void {
this._changeRef.detach();
this.getMapData();
}
getMapData() {
this._mapService.getMapData().subscribe((mapData) => {
this.mapData = mapData;
this.setup();
this.buildSvg();
this.setMapData();
});
}
setup() {
//...
}
buildSVG() {
//...
}
setMapData(mapData) {
//...
}
}
<强>附录强>
另一方面,在分析您的步骤时:
g
selectAll('path')
path
您可以尝试先添加路径,然后再添加数据吗?或者使用
this.svg.selectAll('g')
对我来说更有意义,或者我真的不明白selectAll
是如何运作的。
第二个附录
我想我现在真的得到了你:D你可以将你的extractData
功能更改为:
private extractData(res: Response) {
return res.json()
}
我的猜测是你的网络服务器不会在带有数据属性的对象中返回mapdata,而只是立即返回对象,你的实现似乎是直接来自angular.io cookbook :)
答案 1 :(得分:2)
哇。这是一次旅行!
这里是tl; dr - 我遇到了两个问题:返回的数据格式和数据延迟。
这里是修改后的代码和tl部分:
map.component.ts:
import { Component, ElementRef, Input, AfterViewInit, ChangeDetectorRef } from '@angular/core';
import * as d3 from 'd3/index';
import '../rxjs-operators';
import { MapService } from '../shared/map.service';
@Component({
selector: 'map-component',
templateUrl: './map.component.html',
styleUrls: ['./map.component.css']
})
export class MapComponent implements AfterViewInit {
errorMessage: string;
height;
host;
htmlElement: HTMLElement;
mapData;
margin;
projection;
path;
svg;
width;
constructor (
private _element: ElementRef,
private _mapService: MapService,
private _changeRef: ChangeDetectorRef
) { }
ngAfterViewInit(): void {
this._changeRef.detach();
this.getMapData();
}
getMapData() {
this._mapService.getMapData().subscribe(mapData => this.mapData = mapData, err => {}, () => this.setMap(this.mapData));
this.host = d3.select(this._element.nativeElement);
this.setup();
this.buildSVG();
}
setup() {
console.log('In setup()')
this.margin = {
top: 15,
right: 50,
bottom: 40,
left: 50
};
this.width = document.querySelector('#map').clientWidth - this.margin.left - this.margin.right;
this.height = this.width * 0.6 - this.margin.bottom - this.margin.top;
}
buildSVG() {
console.log('In buildSVG()');
this.host.html('');
this.svg = this.host.append('svg')
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
}
setMap(mapData) {
console.log('In setMap(mapData), mapData getting assigned');
this.mapData = mapData;
console.log('mapData assigned as ' + this.mapData);
this.projection = d3.geoAlbersUsa()
.translate([this.width /2 , this.height /2 ])
.scale(650);
this.path = d3.geoPath()
.projection(this.projection);
this.svg.selectAll('path')
.data(this.mapData.features)
.enter().append('path')
.attr('d', this.path)
.style('stroke', '#fff')
.style('stroke-width', '1')
.style('fill', 'lightgrey');
}
}
map.service.ts:
import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class MapService {
// private url = 'http://localhost:3000/mapData'; // TopoJSON file on the server (5.6 ms)
// private url = 'http://localhost:3000/mapDataAPI'; // GeoJSON file on the server (54 ms)
// private url = 'http://localhost:3000/api/mapData'; // get json data from a local server connecting to cloudant for the data (750ms)
private url = 'https://???.mybluemix.net/api/mapData'; // get GeoJSON from the cloud-side server api getting data from cloudant (1974 ms per Postman)
constructor (private _http: Http) { }
getMapData(): Observable<any> {
return this._http.get(this.url)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body; // the data returned from cloudant doesn't get wrapped in a { data: } object
// return body.data; // this works for files served from the server that get wrapped in a { data: } object
}
private handleError(error: any) {
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg);
return Promise.reject(errMsg);
}
}
我非常感谢大家的输入 - 我仍然需要对代码进行一些清理 - 可能还有一些事情要处理,但数据会创建地图。我接下来的任务是添加数据和动画。我正在拍摄类似于此的演示文稿:http://ww2.kqed.org/lowdown/2015/09/21/now-that-summers-over-what-do-californias-reservoirs-look-like-a-real-time-visualization/
您可以在此处找到相应的代码:https://github.com/vicapow/water-supply
答案 2 :(得分:0)
这更像是一个“创可贴”,但请尝试将getMapData
更改为:
getMapData() {
this._mapService.getMapData()
.subscribe(
mapData => {
if (mapData.features) {
this.setMap(mapData);
}
},
error => this.errorMessage = <any>error
)
}
这样可以防止setMap
在没有mapData.features
的情况下被调用。
答案 3 :(得分:0)
它不能用Promise而不是Observable吗?像
这样的东西在您的服务中:
getMapData (): Promise<any> {
return this._http.get(this.url)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
您也可以直接在此功能中提取您的数据,例如:
.then(response => response.json().data)
并在您的组件中:
getMapData() {
this._mapService.getMapData()
.then(
mapData => mapData = this.setMap(mapData),
error => this.errorMessage = <any>error
)
}
我唯一关心的是在上面的代码中调用setMap函数的位置。由于我无法测试,我希望它可以提供帮助。
答案 4 :(得分:0)
您是否尝试将函数从构造函数移动到ngOnInit,例如:
import { Component, ElementRef, Input, OnInit } from '@angular/core';
import * as D3 from 'd3';
import '../rxjs-operators';
import { MapService } from '../map.service';
@Component({
selector: 'map-component',
templateUrl: './map.component.html',
styleUrls: ['./map.component.css']
})
export class MapComponent implements OnInit {
errorMessage: string;
height;
host;
htmlElement: HTMLElement;
mapData;
margin;
projection;
path;
svg;
width;
constructor (private _element: ElementRef, private _mapService: MapService) {}
setup() {
this.margin = {
top: 15,
right: 50,
bottom: 40,
left: 50
};
this.width = document.querySelector('#map').clientWidth - this.margin.left - this.margin.right;
this.height = this.width * 0.6 - this.margin.bottom - this.margin.top;
}
buildSVG() {
this.host.html('');
this.svg = this.host.append('svg')
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
}
setMap(mapData) {
this.mapData = mapData;
this.projection = D3.geoAlbersUsa()
.translate([this.width /2 , this.height /2 ])
.scale(650);
this.path = D3.geoPath()
.projection(this.projection);
this.svg.selectAll('path')
.data(this.mapData.features)
.enter().append('path')
.attr('d', this.path)
.style('stroke', '#fff')
.style('stroke-width', '1')
.style('fill', 'lightgrey');
}
ngOnInit() {
this.host = D3.select(this._element.nativeElement);
this.setup();
this.buildSVG();
this._mapService.getMapData()
.subscribe(
mapData => this.setMap(mapData),
error => this.errorMessage = <any>error
)
}
}
现在,我不确定它会改变什么,但使用生命周期钩子(OnInit)而不是构造函数被认为是一种好习惯。请参阅Difference between Constructor and ngOnInit。