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

时间:2017-01-25 03:18:02

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

这很奇怪。它也有点长,所以提前道歉。 更新 - 最终出现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 version 1.0.0-beta.26
  • angular version ^ 2.3.1
  • d3版本^ 4.4.4
  • rxjs version ^ 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 per Marks请求中标题部分的屏幕截图。响应选项卡显示正确作为GeoJSON对象发送的数据。我还将该响应复制到本地文件中,并将其用作具有正面结果的地图源。

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

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

snap of Chrome Headers

5 个答案:

答案 0 :(得分:4)

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

如果视图尚未就绪,并且document.querySelector('#map').clientWidth#map内,map.component.html将返回0或未定义。

当您处理模板中的元素时,请始终使用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,而只是立即返回对象,你的实现似乎是直接来自angular.io cookbook :)

答案 1 :(得分:2)

哇。这是一次旅行!

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

  1. 数据格式:当我的json文件在服务器上时,api调用将它包装在{data:}对象中,但当它从api调用我的clouodant数据库时,包装器就不存在了。 @PierreDuc,谢谢你。
  2. 我找到了解决延迟问题的SO答案 - &gt; Queue/callback function after fetching data in an Observable in Angular 2
  3. 这里是修改后的代码和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 :(得分:0)

这更像是一个“创可贴”,但请尝试将getMapData更改为:

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

这样可以防止setMap在没有mapData.features的情况下被调用。

答案 3 :(得分: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函数的位置。由于我无法测试,我希望它可以提供帮助。

答案 4 :(得分: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