d3.js或rxjs错误?this.svg.selectAll(...).data(...).enter不是一个函数。

12

这是一个奇怪的问题,而且有点复杂,提前道歉。 更新 - 最后发现是两个问题请看我的答案。

以下是我的错误:EXCEPTION: this.svg.selectAll(...).data(...).enter is not a function

我有一个 Angular CLI 客户端和一个 Node API 服务器。我可以通过可观察对象 (下面的代码) 从服务中检索一个 states.json 文件。d3 喜欢这个文件并显示了预期的美国地图。

一旦我将服务的目标从文件更改为 IBM Bluemix Cloudant 服务器,我在客户端中就会得到上面的错误。

当我使用 ngOnInit 进行变化并控制台记录输出时,最初 mapData 打印为空数组,并且抛出错误。 这是错误的明显来源,因为没有数据,但 Chrome 调试器显示获取请求处于挂起状态。当请求完成时,数据按预期打印在控制台中。

  • angular-cli 版本为 1.0.0-beta.26
  • angular 版本为 ^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');
  }
}

地图服务.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);
  }
}
这是因为异步处理且调用数据时间过长导致的吗?我曾希望这个Uncaught TypeError: canvas.selectAll(...).data(...).enter is not a function in d3问题能够提供一些见解,但我并没有看到任何信息。非常感谢您的任何帮助和见解!

编辑:
以下是来自Mark要求的Chrome标题部分的屏幕截图。响应选项卡显示数据正常传输为GeoJSON对象。我还将该响应复制到本地文件中,并使用它作为地图源进行了测试,结果为正面反馈。

迄今为止的数据测试:GeoJSON 文件(2.1mb)
  • 本地文件,本地服务器:成功(响应时间 54ms)
  • 相同的文件,远程服务器:D3 在数据返回浏览器之前出现错误(750ms)
  • 从远程服务器调用 API:D3 在数据返回浏览器之前出现错误(2.1 秒)

snap of Chrome Headers

你能展示一下记录 mapData.features 的输出吗? - Assan
setMap() 方法会被调用两次吗?一次是传递一个空数组,另一次是传递预期的数组吗? - wdickerson
嗨,将图表HTML用*ngIf包装起来,只有在您拥有API响应时才将其设置为true,这个想法怎么样? - Vlad
嗨Bruce,看到这个问题还没有解决。你能否在CodePen/Playground/JSFiddle上为我们创建一些代码,以便我们查看和调试? - Mark van Straten
已添加截图。@Assan之前询问了有关数据的问题,我已添加了用于验证来自API的数据的测试方法。 - Bruce MacDonald
显示剩余5条评论
5个回答

4
我的猜测是Angular在构造函数和请求返回之间混淆了对您的map元素的引用。我的建议是在ngAfterViewInit中开始构建svg,或者更好的方法是在从服务器收到响应时开始构建。我认为这个问题主要是基于时间的。当然,如果从服务器接收到的数据没有格式错误,并且您可以在控制台中实际记录到一组映射数据,则可以使用。

此外,如果视图尚未准备就绪,则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的工作原理。
第二个补充:
我现在对你的问题有更好的理解了 :D 你能把你的extractData函数改成这样:
private extractData(res: Response) {
    return res.json()
} 

我猜测你的Web服务器没有将mapdata返回为一个带有data属性的对象,而是直接返回了该对象。你的实现似乎直接来自angular.io的教程。

这绝对是一个时间问题。我按照建议将代码移到了 AfterViewInit 中,但仍然出现相同的结果:错误信息。关于 ChangeDetectorRef 的有趣点-我之前不知道,所以很感激能学到新东西! - Bruce MacDonald
@BruceMacDonald 我已经更新了我的答案,并添加了一个附录。也许结合我上面所说的内容,这会起作用。 - Poul Kruijt
这就是d3的美妙之处和神秘之处!enter().append('path')实际上正在通过添加从传递给它的数据中所需的路径来完成繁重的工作,即使DOM元素尚不存在。 - Bruce MacDonald
但它在“enter”处已经失败了,这让我想到“selectAll”返回一个空选择,进而使“data”失败。您的SVG中还没有“path”,只有一个“g”。 - Poul Kruijt
请参见下面@MattDionis的答案。 - Bruce MacDonald
显示剩余3条评论

2
哇,这真是一次旅行!
这里是简短版 - 我遇到了两个问题:返回数据的格式和数据延迟。
  1. 数据格式:当我的JSON文件在服务器上时,API调用会将其包装在一个{ data: }对象中,但当它从调用我的Cloudant数据库的API服务中提供时,包装器就不存在了。@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');
    }

  }

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

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函数。由于我无法测试它,希望它能有所帮助。


我会试一试并发布结果。 - Bruce MacDonald
同样的结果。好的一面是,转换到 Promise 就像在 angular.io 上所宣传的那样容易。我唯一需要寻找的东西是将 import 'rxjs/add/operator/toPromise'; 添加到 map.service.ts 中,但其他所有内容都很简单明了。 - Bruce MacDonald

0
这更像是一种“临时措施”,但尝试将getMapData更改为以下内容:
getMapData() {
  this._mapService.getMapData()
    .subscribe(
      mapData => {
        if (mapData.features) {
          this.setMap(mapData);
        }
      },
      error =>  this.errorMessage = <any>error
    )
}

这将防止在没有 mapData.features 的情况下调用 setMap


我修改了getMapData - 这是结果(没有成功):当我从服务器提供us-states.json文件时,响应是GET /mapData 304 5.589 ms - -,地图将显示。但是,如果从api到cloudant提供内容,则响应为GET /api/mapData 200 702.774 ms - -,并且不会显示地图。如何暂停setMap(mapData)函数,直到数据从慢服务器到达? - Bruce MacDonald
@BruceMacDonald,我在我的GET /mapData端点中添加了setTimeout来模拟缓慢的响应,但无论我设置超时时间多长,它仍然有效(地图在超时期间成功加载)。 - MattDionis
谢谢您的关注,我还需要进行一些研究。至少对getMapData的更改已经停止了d3错误,但我仍然没有从API调用中看到地图。 - Bruce MacDonald
我也会继续努力攻克它。 - MattDionis

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)而不是构造函数被认为是良好的实践。请参见构造函数和ngOnInit之间的区别


将调用从构造函数移动到ngOnInit后,结果相同。 - Bruce MacDonald

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接