D3.js или rxjs ошибка? this.svg.selectAll(...). data (...). enter не является функцией

Это странно. Он также немного длинный, поэтому извиняюсь заранее. обновление - в итоге это было 2 проблемы, см. мой ответ ниже.

Здесь моя ошибка: EXCEPTION: this.svg.selectAll(...).data(...).enter is not a function

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

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

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

  • angular -cli version 1.0.0-beta.26
  • angular версия ^ 2.3.1
  • d3 version ^ 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 не является функцией в d3, может дать некоторое представление, но я не вижу Любые.

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

EDIT: Вот скриншот раздела заголовков из запроса Chrome за метки ниже. На вкладке ответа отображаются данные, которые, как правило, отображаются как объект GeoJSON. Я также скопировал этот ответ в файл локально и использовал его в качестве источника карты с положительными результатами.

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

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

привязка заголовков Chrome

Ответ 1

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

Здесь 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

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

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

Когда вы работаете над элементами внутри шаблона, всегда используйте крюк жизненного цикла ngAfterViewInit.

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

2-е добавление

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

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

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

Ответ 3

Это скорее "бандажная помощь", но попробуйте изменить getMapData на это:

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

Это будет защищать от setMap, вызываемого без mapData.features.

Ответ 4

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

В вашем сервисе:

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

Ответ 5

Вы пытались переместить свои функции из конструктора в 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) вместо конструктора. См. Разница между конструктором и ngOnInit.