This is a weird one. It's also a bit long so apologies in advance. update - it ended up being 2 problems see my answer below.
Here's my error: EXCEPTION: this.svg.selectAll(...).data(...).enter is not a function
I have an angular-cli client and a node api server. I can retrieve a states.json file from a service using an observable (code below). d3 likes the file and displays the expected US map.
The moment I change the target of the service in my api server from a file to a bluemix-cloudant server I get the error above in my client.
When I console.log the output in a variation using ngOnInit, initially mapData prints as an empty array and the error gets thrown. This is the obvious source of the error since there's no data, but the Chrome debugger shows the get request pending. When the request completes, the data prints as expected in the console.
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);
}
}
Is this a function of being Async and the call to the data takes too long for d3?
I had hopes that this question Uncaught TypeError: canvas.selectAll(...).data(...).enter is not a function in d3 would offer some insight but I don't see any.
Any help or insight is greatly appreciated!
EDIT: Here's a screenshot of the headers section from Chrome per Marks request below. The response tab shows the data properly coming across as a GeoJSON object. I've also copied that response into a file locally and used it as a map source with positive results.
Data Tests so far: GeoJSON file (2.1mb)
My guess is that angular messes up the reference to your map
element between the constructor and the time that your request comes back. My advice is to start building the svg
inside ngAfterViewInit
or even better, when the response from the server has arrived. I believe this issue is mainly based on timing. If of course the data received from the server is not malformed and you can actually log a nice array of mapping data in your console.
Also the document.querySelector('#map').clientWidth
will return 0 or undefined if the view is not ready yet, and when the #map
is inside the map.component.html
.
When you are working on elements inside the template, always use the ngAfterViewInit
life cycle hook.
Besides that, it doesn't seem like you are using any of angular's change detection inside your component. I would advice you, to prevent any interference with your elements, to detach from the 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) {
//...
}
}
addendum
On the other hand, when analyzing your steps:
g
to itselectAll('path')
path
Can you try appending the path first and after that add data to it? Or use
this.svg.selectAll('g')
Makes more sense to me, or perhaps I don't really understand how selectAll
works.
2nd addendum
I think I really got it now for you :D can you change your extractData
function to this:
private extractData(res: Response) {
return res.json()
}
My guess is that your webserver doesn't return the mapdata in an object with a data property, but just the object immediately, and your implementation seems to be straight from the angular.io cookbook :)
Wow. This has been a trip!
Here's the tl;dr - I had two issues I was dealing with: the format of the data being returned and data latency.
Here's the modified code and the tl part:
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);
}
}
I really appreciate everyone's input - I still have some cleanup to do on the code - there may still be some things to do but the data creates the map. My next tasks are adding data and animation. I'm shooting for a presentation similar to this: http://ww2.kqed.org/lowdown/2015/09/21/now-that-summers-over-what-do-californias-reservoirs-look-like-a-real-time-visualization/
You can find the code for it here: https://github.com/vicapow/water-supply
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With