C'est bizarre. C'est aussi un peu long donc excuses à l'avance. mise à jour - il a fini par poser deux problèmes, voir ma réponse ci-dessous.

Voici mon erreur: EXCEPTION: this.svg.selectAll(...).data(...).enter is not a function

J'ai un client angular-cli et un serveur api de nœud. Je peux récupérer un fichier states.json à partir d'un service en utilisant un observable (code ci-dessous). d3 aime le fichier et affiche la carte américaine attendue.

Au moment où je change la cible du service dans mon serveur api d'un fichier à un serveur bluemix-cloudant, j'obtiens l'erreur ci-dessus dans mon client.

Lorsque je console.log la sortie dans une variante à l'aide de ngOnInit, initialement mapData s'imprime comme un tableau vide et l'erreur est renvoyée. C'est la source évidente de l'erreur car il n'y a pas de données, mais le débogueur Chrome montre la demande d'obtention en attente. Une fois la demande terminée, les données s'impriment comme prévu dans la console.

  • angular-cli version 1.0.0-beta.26
  • version angulaire ^ 2.3.1
  • version d3 ^ 4.4.4
  • version 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);
  }
}

Est-ce une fonction d'être Async et l'appel aux données prend trop de temps pour d3?

J'espérais que cette question Uncaught TypeError: canvas.selectAll (. ..). data (...). enter n'est pas une fonction dans d3 offrirait un aperçu mais je n'en vois aucun.

Toute aide ou perspicacité est grandement appréciée!

MODIFIER: Voici une capture d'écran de la section des en-têtes de la demande Chrome par marque ci-dessous. L'onglet de réponse affiche les données correctement présentées en tant qu'objet GeoJSON. J'ai également copié cette réponse dans un fichier localement et l'ai utilisée comme source cartographique avec des résultats positifs.

Tests de données jusqu'à présent: fichier GeoJSON (2,1 Mo)

  • Fichier local, serveur local: succès (temps de réponse 54 ms)
  • Même fichier, serveur distant: erreurs D3 avant que les données ne soient renvoyées au navigateur (750 ms)
  • Appel API à partir du serveur distant: erreurs D3 avant que les données ne soient renvoyées au navigateur (2,1 s)

snap of Chrome Headers

12
Bruce MacDonald 25 janv. 2017 à 06:18

4 réponses

C'est plus un "pansement", mais essayez de remplacer getMapData par ceci:

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

Cela empêchera setMap d'être appelé sans mapData.features.

0
MattDionis 27 janv. 2017 à 20:55

Avez-vous essayé de déplacer vos fonctions du constructeur vers ngOnInit, quelque chose comme:

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

Maintenant, je ne suis pas sûr que cela changera quoi que ce soit, mais il est considéré comme une bonne pratique d'utiliser le hook de cycle de vie (OnInit) au lieu du constructeur. Voir Différence entre Constructor et ngOnInit.

0
Community 23 mai 2017 à 12:17

Cela ne fonctionnerait-il pas avec une promesse au lieu d'un observable? Quelque chose comme

A votre service:

getMapData (): Promise<any> {
  return this._http.get(this.url)
                  .toPromise()
                  .then(this.extractData)
                  .catch(this.handleError);
}

Vous pouvez également extraire directement vos données dans cette fonction, quelque chose comme:

.then(response => response.json().data)

Et dans votre composant:

getMapData() {
    this._mapService.getMapData()
        .then(
            mapData => mapData = this.setMap(mapData),
            error =>  this.errorMessage = <any>error
         )
}

Ma seule préoccupation est de savoir où appeler la fonction setMap dans le code ci-dessus. Comme je ne peux pas le tester, j'espère que cela pourra vous aider.

0
Fabien 31 janv. 2017 à 10:38

Je suppose que angulaire gâche la référence à votre élément map entre le constructeur et le moment où votre requête revient. Mon conseil est de commencer à construire le svg à l'intérieur de ngAfterViewInit ou même mieux, lorsque la réponse du serveur est arrivée. Je crois que cette question est principalement basée sur le timing. Si bien sûr les données reçues du serveur ne sont pas mal formées et que vous pouvez en fait enregistrer un joli tableau de données de mappage dans votre console.

De plus, le document.querySelector('#map').clientWidth retournera 0 ou indéfini si la vue n'est pas encore prête, et lorsque le #map est à l'intérieur du map.component.html.

Lorsque vous travaillez sur des éléments à l'intérieur du modèle, utilisez toujours le hook de cycle de vie ngAfterViewInit.

De plus, il ne semble pas que vous utilisiez une détection de changement angulaire à l'intérieur de votre composant. Je vous conseille, pour éviter toute interférence avec vos éléments, de vous détacher du 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

En revanche, lors de l'analyse de vos pas:

  • vous créez un svg
  • y ajouter un g
  • alors vous faites un selectAll('path')
  • et essayez d'ajouter des données à cette sélection
  • et seulement après cela, vous essayez d'ajouter un path

Pouvez-vous essayer d'ajouter d'abord le chemin et ensuite y ajouter des données? Ou utiliser

this.svg.selectAll('g') 

Cela a plus de sens pour moi, ou peut-être que je ne comprends pas vraiment comment selectAll fonctionne.

2e addendum

Je pense que je l'ai vraiment compris pour vous: D pouvez-vous changer votre fonction extractData en ceci:

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

Je suppose que votre serveur Web ne renvoie pas les données cartographiques dans un objet avec une propriété data, mais juste l'objet immédiatement, et votre implémentation semble provenir directement du livre de recettes angular.io :)

4
Poul Kruijt 4 févr. 2017 à 21:17