【问题标题】:d3.js or rxjs error? this.svg.selectAll(...).data(...).enter is not a functiond3.js 或 rxjs 错误? this.svg.selectAll(...).data(...).enter 不是函数
【发布时间】:2017-06-10 02:46:04
【问题描述】:

这是一个奇怪的问题。它也有点长,所以提前道歉。 更新 - 它最终成为 2 个问题,请参阅下面的答案。

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

我有一个 angular-cli 客户端和一个节点 api 服务器。我可以使用 observable(下面的代码)从服务中检索 states.json 文件。 d3 喜欢该文件并显示预期的美国地图。

当我将 api 服务器中的服务目标从文件更改为 bluemix-cloudant 服务器时,我在客户端中收到上述错误。

当我使用 ngOnInit 在变体中 console.log 输出时,最初 mapData 打印为空数组并引发错误。这是错误的明显来源,因为没有数据,但是Chrome 调试器显示 get 请求待处理。请求完成后,控制台中会按预期打印数据。

  • angular-cli 版本 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);
  }
}

这是一个异步的函数并且对数据的调用对于 d3 来说需要太长时间吗?

我希望这个问题 Uncaught TypeError: canvas.selectAll(...).data(...).enter is not a function in d3 会提供一些见解,但我没有看到任何见解。

非常感谢任何帮助或见解!

编辑: 以下是 Chrome 每个 Marks 请求的标头部分的屏幕截图。响应选项卡显示正确作为 GeoJSON 对象出现的数据。我还将该响应复制到本地文件中,并将其用作具有积极结果的地图源。

到目前为止的数据测试:GeoJSON 文件 (2.1mb)

  • 本地文件,本地服务器:成功(响应时间 54ms)
  • 同一个文件,远程服务器:数据返回浏览器之前的 D3 错误(750 毫秒)
  • 来自远程服务器的 API 调用:数据返回浏览器之前的 D3 错误(2.1 秒)

【问题讨论】:

  • 你能显示记录mapData.features的输出吗?
  • @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...}]
  • 您确定this.mapData.features 是您在setMap 函数中所期望的(必须是一个数组)吗?
  • @Assan 与节点服务器上文件的内容完全相同。正如我所说,d3 喜欢该文件并显示地图。 this.mapData.features 是 d3 生成地图所需的数组。
  • setMap() 会被调用两次吗?一次是空数组,一次是预期数组?

标签: angular d3.js ibm-cloud rxjs cloudant


【解决方案1】:

我的猜测是,角度会在构造函数和请求返回的时间之间混淆对 map 元素的引用。我的建议是在服务器响应到达时开始在ngAfterViewInit 内构建svg,甚至更好。我相信这个问题主要是基于时间。当然,如果从服务器接收到的数据没有格式错误,并且您实际上可以在控制台中记录一组不错的映射数据。

如果视图还没有准备好,并且#mapmap.component.html 内,document.querySelector('#map').clientWidth 也将返回 0 或未定义。

当您处理模板内的元素时,请始终使用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()
} 

我的猜测是您的网络服务器不会在具有数据属性的对象中返回 mapdata,而只是立即返回该对象,并且您的实现似乎直接来自 angular.io 食谱:)

【讨论】:

  • 绝对是时间问题。我按照建议将代码移至AfterViewInit,但结果仍然相同:错误消息。 ChangeDetectorRef 上的有趣点 - 我不知道,所以我很感激学习新东西!
  • @BruceMacDonald 我已经用附录更新了我的答案。结合我上面所说的,也许这会起作用
  • selectAll 将选择与您传递给它的字符串或函数匹配的所有 DOM 元素。很像 CSS 选择器,它可以与标签、类、id 等一起使用。我对构建地图的 d3 步骤的连续性感到满意,但你让我重新认为整个生命周期是 angular 和 d3交互以更改 DOM 和事件触发序列 - 特别是对于高延迟数据源。我还有更多的学习要做......
  • @BruceMacDonald 但您正在执行 selectAll('path') 即使您尚未添加路径元素。对我来说听起来违反直觉:)
  • 这就是 d3 的美丽和神秘! enter().append('path') 实际上是通过从传递给它的数据中添加地图所需的路径来完成繁重的工作,即使 DOM 元素还不存在。
【解决方案2】:

哇。这是一次旅行!

这是 tl;dr - 我遇到了两个问题:返回的数据格式和数据延迟。

  1. 数据格式:当我的 json 文件在服务器上时,api 调用会将它包装在一个 { data: } 对象中,但是当它从一个调用我的 clouodant 数据库的 api 提供时,包装器不存在。 @PierreDuc,谢谢。
  2. 我找到了解决延迟问题的 SO 答案 -> Queue/callback function after fetching data in an Observable in Angular 2

这是修改后的代码和 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

【讨论】:

    【解决方案3】:

    这更像是一种“创可贴”,但请尝试将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) 函数?
    • @BruceMacDonald,我将setTimeout 添加到我的GET /mapData 端点以模拟缓慢的响应,但无论我设置超时多长时间它仍然有效(超时后地图成功加载期间)。
    • 感谢您的检查,我还有一些研究要做。更改为getMapData 至少停止了 d3 错误,但我仍然没有看到来自 api 调用的地图。
    • 我也将继续破解它。
    【解决方案4】:

    难道不能使用 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 函数的位置。由于无法测试,希望对您有所帮助。

    【讨论】:

    • 我会尝试并发布结果
    • 相同的结果。从好的方面来说,切换到 Promise 就像在 angular.io 上宣传的一样简单。我唯一需要寻找的是将import 'rxjs/add/operator/toPromise'; 添加到map.service.ts,但其他一切都很简单。
    【解决方案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) 而不是构造函数被认为是一种好习惯。见Difference between Constructor and ngOnInit

    【讨论】:

    • 将调用从构造函数转移到 ngOnInit 后结果相同。
    猜你喜欢
    • 2017-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-10
    • 2023-03-22
    • 2018-02-12
    • 1970-01-01
    • 2012-05-04
    相关资源
    最近更新 更多