2017-01-25 6 views
12

Это странно. Это также немного долго, поэтому извиняйтесь заранее. обновление - это оказалось 2 проблемы, см. Мой ответ ниже.Ошибка d3.js или rxjs? this.svg.selectAll (...). data (...). enter не является функцией

Вот моя ошибка: EXCEPTION: this.svg.selectAll(...).data(...).enter is not a function

У меня есть угловой-кли-клиент и сервер узла апи. Я могу получить файл states.json из службы с помощью наблюдаемого (код ниже). d3 нравится файл и отображает ожидаемую карту США.

Момент, когда я изменяю цель службы на моем сервере api из файла на сервер bluemix-cloudant, я получаю ошибку выше в моем клиенте.

Когда I console.log выводит в варианте с использованием ngOnInit, изначально mapData печатает как пустой массив и генерируется ошибка. Это очевидный источник ошибки, так как данных нет, но отладчик Chrome показывает запрос на получение запроса. Когда запрос завершается, данные печатаются, как ожидалось, в консоли.

  • угловой кли версия 1.0.0-beta.26
  • угловая версия^2.3.1
  • d3 версия^4.4.4
  • rxjs версия^5.0.1

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); 
    } 
} 

Является ли это функцией Async, и вызов данных занимает слишком много времени для d3?

У меня были надежды, что этот вопрос Uncaught TypeError: canvas.selectAll(...).data(...).enter is not a function in d3 предложит некоторое представление, но я его не вижу.

Любая помощь или понимание очень ценятся!

РЕДАКТИРОВАТЬ: Вот скриншот раздела заголовков из запроса Chrome для каждой марки. На вкладке ответа отображаются данные, которые, как правило, отображаются как объект GeoJSON. Я также скопировал этот ответ в файл локально и использовал его в качестве источника карты с положительными результатами.

Тесты данных до сих пор: GeoJSON файл (2.1MB)

  • Локальный файл, локальный сервер: Success (время отклика 54ms)
  • же файл, удаленный сервер: ошибки D3, прежде чем данные, возвращаемые в браузере (750ms)
  • Запрос API с удаленного сервера: ошибки D3 перед возвратом данных в браузер (2.1 с)

snap of Chrome Headers

+0

Вы можете показать вывод отладочной 'mapData.features'? – Assan

+0

@Assan - это части данных GeoJSON, которые определяют границы состояний. Вот пример: '" features ": [ {" type ":" Feature "," properties ": {" GEO_ID ":" 0400000US01 "," STATE ":" 01 "," NAME ":" Alabama "," LSAD ":" "," CENSUSAREA ": 50645.326000}," geometry ": {" type ":" MultiPolygon "," Coordinates ": [[[-88.124658, 30.283640], [-88.086812, 30.259864], [-88.074854 , 30.249119], [-88.075856, 30.246139], [-88.078786, 30.245039], ...}, {next state and so on ...}] ' –

+0

Вы убедитесь, что' this.mapData.features' - это то, что вы ожидаете (должен быть массив) внутри функции setMap? – Assan

ответ

2

Wow. Это была поездка!

Это tl; dr - У меня было две проблемы, с которыми я имел дело: формат возвращаемых данных и латентность данных.

  1. Формат данных: когда мой файл JSON был на сервере вызов апи бы обернуть его в {данных:} объект, но когда он был подан от апи вызывающему моей clouodant базы данных обертка не была. @PierreDuc, спасибо за это.
  2. Я нашел этот SO ответ решить проблему задержки ->Queue/callback function after fetching data in an Observable in Angular 2

Вот измененный код и Т.Л. часть:

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'); 
    } 

    } 

карта. 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); 
    } 
} 

Я действительно ценю всех, s ввода - у меня все еще есть очистка для кода - все равно могут быть некоторые вещи, но данные создают карту. Моими следующими задачами являются добавление данных и анимация.Я снимаюсь для презентации похожее на это: 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

0

Это более "пластырем", но попробуйте изменить getMapData к этому:

getMapData() { 
    this._mapService.getMapData() 
    .subscribe(
     mapData => { 
     if (mapData.features) { 
      this.setMap(mapData); 
     } 
     }, 
     error => this.errorMessage = <any>error 
    ) 
} 

Это охраняющие против setMap вызывается без mapData.features.

+0

Я изменил getMapData - вот результаты (без радости): Когда я обслуживаю файл us-states.json с сервера, ответ будет «GET/mapData 304 5.589 ms - -», и карта будет отображаться. Но, обслуживая содержимое от api до cloudant, ответ «GET/api/mapData 200 702,774 мс - -» и карта не отображается. Как заблокировать функцию setMap (mapData) до тех пор, пока данные не поступят с медленного сервера? –

+0

@BruceMacDonald, я добавил 'setTimeout' в мою конечную точку GET'/mapData' для имитации медленного ответа, но независимо от того, как долго я устанавливаю тайм-аут для него, все еще работает (карта успешно загружается после периода таймаута). – MattDionis

+0

Спасибо, что посмотрели, у меня все еще есть исследование. Как правило, изменение 'getMapData' останавливает ошибку d3, но я все еще не вижу карту из вызова api. –

0

Не будет ли это работать с обещанием вместо наблюдаемого? Что-то вроде

В К вашим услугам:

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 в приведенном выше коде. Поскольку я не могу проверить это, я надеюсь, что это может помочь.

+0

Я попробую и опубликуйте результаты –

+0

Те же результаты. С яркой стороны переключатель в Promise так же прост, как рекламируется на угловом.io. Единственное, на что я должен был охотиться, это добавить 'import 'rxjs/add/operator/toPromise';' to map.service.ts, но все остальное было просто. –

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.

+0

Те же результаты после переадресации вызовов из конструктора в ngOnInit. –

4

Моя догадка заключается в том, что угловые помехи ссылаются на ваш элемент map между конструктором и временем, когда ваш запрос возвращается. Мой совет - начать строить svg внутри ngAfterViewInit или даже лучше, когда поступит ответ от сервера. Я считаю, что этот вопрос в основном основан на сроках. Если, конечно, данные, полученные с сервера, не являются искаженными, и вы действительно можете зарегистрировать хороший массив данных сопоставления в консоли.

Также document.querySelector('#map').clientWidth вернет 0 или не определено, если представление еще не готово, и когда #map находится внутри map.component.html.

Когда вы работаете с элементами внутри шаблона, всегда используйте крюк жизненного цикла 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) { 
    //... 
    } 

} 

Добавление

С другой стороны, при анализе ваших шагов:

  • вам создать svg
  • добавить g к нему
  • тогда вы делаете selectAll('path')
  • и попытаться добавить данные в этой подборке
  • и только после того, что вы пытаетесь добавить path

Можете ли вы попробуйте добавить путь первого и после этого добавить в него данные? Или используйте

this.svg.selectAll('g') 

делает больше смысла для меня, или, возможно, я не очень понимаю, как selectAll работы.

второе добавление

Я думаю, что я действительно получил это сейчас для вас: D вы можете изменить extractData функцию следующим образом:

private extractData(res: Response) { 
    return res.json() 
} 

Я думаю, что ваш веб-сервер не возвращает mapdata в объекте с свойством данных, но просто объект немедленно, и ваша реализация кажется прямой из угловой.io cookbook :)

+0

Это определенно проблема времени. Я переместил код в 'AfterViewInit', как и предполагалось, но все тот же результат: сообщение об ошибке. Интересные моменты на 'ChangeDetectorRef' - я не знал об этом, поэтому я ценю изучение чего-то нового! –

+0

@BruceMacDonald Я обновил свой ответ с добавлением. Возможно, это сработает, в сочетании с тем, что я сказал выше – PierreDuc

+0

'selectAll' выберет все элементы DOM, соответствующие строке или функции, которые вы передадите ей. Подобно селектору CSS, он может работать с тегами, классами, идентификаторами и т. Д. Мне было комфортно следовать последовательности шагов d3 для построения карты, но вы заставили меня переосмыслить весь жизненный цикл как угловой и d3 взаимодействовать для изменения DOM и последовательности срабатывания событий, особенно с источником с высокой задержкой. У меня есть больше обучения, чтобы сделать ... –