ng-alain+D3绘制地图

22 篇文章 2 订阅
15 篇文章 3 订阅

一、获取中国的地图数据

    地图json数据下载(域名失效)

    世界地图json数据(域名失效)

    中国地图json文件

    json文件简化

    地图json文件

二、绘制过程中遇到的问题

    1、理解SVG viewport,viewBox,preserveAspectRatio缩放

1)viewBox

        viewBox="x, y, width, height" // x:左上角横坐标,y:左上角纵坐标,width:宽度,height:高度

 

        作用:viewBox顾名思意是“视区盒子”的意思,定义该属性,内容会根据你的设置,铺满区域。

2)viewport

3)preserveAspectRation

    2、增加缩放、拖拽 

/**
 * 绘制地图
 */
drawMap() {
  const svg = this.svg;
  const width = Number(this.width);
  const height = Number(this.height);
  let color = this.util.getColor(null);
  color = color.concat(color, color, color);

  const g = svg.append('g');
  svg.call(d3.zoom()
    .scaleExtent([1 / 2, 8])
    .on("zoom", zoomed));

  const me = this;
  // https://raw.githubusercontent.com/aruis/chinamap/master/data/cn.json
  d3.json(DATA_CHINA_JSON).then((res)=>{
    // fitExtent方法填充画布 语法检测不过,通过取巧方式通过
    // 先定义一个数组,将对象push进去,将data[0]作为参数传入
    const data = [];
    data.push(res)
    // center() 函数是用于设定地图的中心位置,[107,31] 指的是经度和纬度。
    // scale() 函数用于设定放大的比例。
    // translate() 函数用于设定平移。
    const proj = d3.geoMercator().center([105, 38])
      .fitExtent([[0, 0], [width, height]], data[0]);
      // .scale(850)
      // .translate([ width / 2, height/2 ]);
    const path = d3.geoPath().projection(proj);

    g.append('g')
      .selectAll('path')
      .data(res['features'])
      .enter()
      .append('path')
      .on('mouseover', function (d, i) {
        d3.select(this).attr('fill','yellow');
        me.tip.html('<div>'+d.properties.name+'</div>');
        me.tip.show();
      })
      .on('mouseout', function (d, i) {
        d3.select(this)
          .attr('fill',color[i]);
          me.tip.hide();
      })
      .attr('fill',function (d, i) {
        return color[i];
      })
      .attr('d', path);
  })
  function zoomed() {
    g.attr("transform", d3.event.transform);
  }
  function dragged(d) {
    d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
  }
}

遇到的问题问题:

Argument of type '{}' is not assignable to parameter of type 'ExtendedGeometryCollection<GeoGeometryObjects>'.
  Property 'type' is missing in type '{}'.

    语法检查一直报错

    解决办法:将数据包裹到数组中,让后把数组的[0]传进去

    

      // fitExtent方法填充画布 语法检测不过,通过取巧方式通过
      // 先定义一个数组,将对象push进去,将data[0]作为参数传入
      const data = [];
      data.push(res)
      // center() 函数是用于设定地图的中心位置,[107,31] 指的是经度和纬度。
      // scale() 函数用于设定放大的比例。
      // translate() 函数用于设定平移。
      const proj = d3.geoMercator().center([105, 38])
        .fitExtent([[0, 0], [width, height]], data[0]);
        // .scale(850)
        // .translate([ width / 2, height/2 ]);
      const path = d3.geoPath().projection(proj);

    3、完整代码

const DATA_CHINA_JSON = 'assets/axis/d3_china.json';
const DATA_SHAN3XI_JSON = 'assets/axis/d3_shan3xi.json';
const DATA_WORLD_JSON = 'assets/axis/world.json';

import {
  Component,
  OnDestroy,
  OnInit,
  Input,
  AfterContentInit,
  Output,
  EventEmitter,
  ElementRef,
} from '@angular/core';

import { Animate } from '@core/animate/animate';
import * as d3 from 'd3';
import { NzMessageService } from 'ng-zorro-antd';
import { _HttpClient } from '@delon/theme';
import { AppUtil } from '@core/util/util.service';

@Component({
  selector: 'app-d3chart',
  template: `
    <div class="nz-content-map" 
                [ngStyle]="{'padding': 0,'width': width,'height':height}"
                xmlns="http://www.w3.org/1999/html">
      <svg class="chart-cursor-d3"
           [ngStyle]="{'height':height,'background-color': getColor('bg')}"></svg>
      <div id="app-D3chart_tip" class="chart-tip-d3"></div>
      <canvas id="app-D3chart_canvas" height="0" width="0"></canvas>
      <div class="grid-content-buttom-left">
        <div *ngFor="let item of legendData;">
          <nz-tooltip nzTitle="{{item.name + ':' + item.value}}">
            <div class="grid-content-buttom-Legend" nz-tooltip
                 [ngStyle]="{'background-color': item.color}"></div>
            <div style="float: left" nz-tooltip>{{item.value}}</div>
          </nz-tooltip>
        </div>
      </div>
    </div>
  `,
  styleUrls: ['./d3.widget.less'],
})
export class D3WidgetComponent implements OnInit, OnDestroy, AfterContentInit {
  constructor(
    private http: _HttpClient,
    public msg: NzMessageService,
    public util: AppUtil,
    public el: ElementRef,
    public animate: Animate,
  ) {}

  static readonly KEY = 'app-d3chart';
  dom: any; // dom相关
  svg: any; // svg相关
  tip: any; // tip相关
  projection: any; // d3提供的转换坐标的对象
  map: any; // 地图的dom
  mapData: any; // 地图的坐标数据
  centerPosition: [number, number] = [107, 31]; // 默认中心位置 经纬度
  centerPositionLoc: any; // 默认中心位置 坐标
  path: any; // 地图的路径
  baseR = 10; // 摄像头底座半径
  svgWidth: any; // svg宽度
  middleGridItems: any;
  zoom: any;
  legendData: any;
  @Input() width: any = '100%'; // 默认宽度
  @Input() height = '100%'; // 默认高度
  @Input() cardId = ''; // 默认高度
  @Input() allowZoom = true; // 默认高度
  @Output('getWidth') // 动态获取宽度 动态调整
  getWidth: EventEmitter<number> = new EventEmitter<number>();

  /**
   * @函数名称:ngOnInit
   * @param 无
   * @作用:入口函数 业务代码开始执行
   * @date 2018/7/10
   */
  ngOnInit() {
    this.initDom();
    this.tip = this.initTip();
    this.drawMap();
  }

  /**
   * @函数名称:initDom*
   * @param
   * @作用:dom初始化 保存相关的dom对象,方便后面绘制的时候使用
   * @date 2018/7/10
   */
  initDom() {
    const dom = this.el.nativeElement.querySelector('div');
    // 宽高赋值
    this.dom = dom;
    this.getWidth.emit();

    this.width =
      dom.offsetWidth || (this.cardId && document.getElementById(this.cardId))
        ? document.getElementById(this.cardId).offsetWidth
        : this.width;

    this.height = dom.offsetHeight || parseInt(this.height, 10);
    const viewBox = '0 0 ' + this.width + ' ' + this.height;
    // ng card组件有padding:24的样式 此处填满card 设置样式给抵消掉
    this.svgWidth = Number(this.width) - 24;
    // SVG自适应填充屏幕
    const svg = d3
      .select(this.el.nativeElement.querySelector('svg'))
      .attr('width', '100%')
      .attr('height', this.height)
      .attr('preserveAspectRatio', 'xMidYMid')
      .attr('viewBox', viewBox);

    this.zoom = d3.zoom();
    this.svg = svg;
    this.initSvg();
    this.initMap();
  }

  /**
   * @函数名称:initSvg
   * @param
   * @作用:处理svg初始化位置及放大缩小范围约定
   * @return:obj
   * @date 2018/8/2
   */
  initSvg() {
    // 设置初始比例,并注入函数
    this.svg.call(
      this.zoom
        .scaleExtent([1 / 4, 8]) // 缩放比例 1到8
        .on('start', this.zoomeStart)
        .on('zoom', this.allowZoom ? this.zoomed : null)
        .on('end', this.zoomeEnd),
    );
  }

  /**
   * @函数名称:initMap
   * @param
   * @作用:主画布初始化
   * @return:obj
   * @date 2018/8/2
   */
  initMap() {
    // 主画布
    this.map = this.svg.append('g');
  }

  /**
   * @函数名称:initProjection
   * @param
   * @作用:设置Projection的值 用于将经纬度转换成坐标
   * @return:obj
   * @date 2018/8/2
   */
  initProjection() {
    const me = this;
    const width = this.svgWidth;
    const height = Number(this.height);
    // fitExtent方法填充画布 语法检测不过,通过取巧方式通过
    // 先定义一个数组,将对象push进去,将data[0]作为参数传入
    const jsonData = [];
    jsonData.push(me.mapData);
    // center() 函数是用于设定地图的中心位置,[107,31] 指的是经度和纬度。
    // scale() 函数用于设定放大的比例。
    // translate() 函数用于设定平移。
    // 105 38
    this.projection = d3
      .geoMercator()
      .center(me.centerPosition)
      .fitExtent([[0, 0], [width, height]], jsonData[0]);
    //   .scale(me.getScale())
    //   .translate([width / 2, height / 2]);

    this.path = d3.geoPath().projection(me.projection);
  }

  /**
   * @函数名称:initCenterPositionLoc
   * @param
   * @作用:设置中心坐标
   * @return:obj
   * @date 2018/8/2
   */
  initCenterPositionLoc() {
    // 中心坐标保存
    const me = this;
    this.centerPositionLoc = this.projection(me.centerPosition);
  }
  /**
   * @函数名称:drawMap
   * @param
   * @作用:绘制地图
   * @date 2018/7/10
   */
  drawMap() {
    const me = this;
    // https://raw.githubusercontent.com/aruis/chinamap/master/data/cn.json
    d3.json(DATA_CHINA_JSON).then(res => {
      me.mapData = res;
      me.initProjection();
      me.initCenterPositionLoc();
      // 绘制省市
      me.drawPath();
      // 绘制省市名称
      me.drawText();

      me.loadData(null);
    });
  }
  /**
   * @函数名称:zoomeStart
   * @param
   * @作用:移动前处理
   * @return:obj
   * @date 2018/8/2
   */
  zoomeStart() {}

  /**
   * @函数名称:zoomeEnd
   * @param
   * @作用:移动后处理
   * @return:obj
   * @date 2018/8/2
   */
  zoomeEnd() {}
  /**
   * @函数名称:zoomed
   * @param dom  'g' element
   * @param transform {
   *                    x: 100,
   *                    y:100,
   *                    k:0.1,
   *                    changeCenter:boolean 是否需要改变中心点
   *                  }
   * @作用:定位并且放大缩小函数 相似功能使用函数处理
   * @date 2018/7/9
   */
  zoomed(dom, transform, changeCenter) {
    const me = this;
    // TODO 此处有个bug 拖拽原点设置有问题  需要处理
    const d = dom || d3.select('g');
    d.attr(
      'transform',
      transform
        ? 'translate(' +
          transform.x +
          ',' +
          transform.y +
          ') scale(' +
          transform.k +
          ')'
        : d3.event.transform,
    );
  }

  /**
   * @函数名称:getLegendData
   * @param
   * @作用:获取图例的值
   * @returns {Array}
   * @date 2018/8/2
   */
  getLegendData() {
    const h: any = this.height;
    const arr = [];
    const data = {
      危急: '>800',
      高危: '500-800',
      中危: '300-500',
      低危: '100-300',
      未知: '<100',
    };
    let i = 5;
    for (const key in data) {
      arr.push({
        name: key,
        level: i,
        location: this.projection([
          this.util.getRandomNum(80, 107),
          this.util.getRandomNum(30, 37),
        ]),
        value: data[key],
        color: this.util.getColor(key),
      });
      i--;
    }
    return arr;
  }
  drawLegend() {
    this.legendData = this.getLegendData();
    // const legendData = this.getLegendData();
    // const g = this.map;
    // const c = this.getColor('text');
    // g.append('g')
    //   .selectAll('text')
    //   .data(legendData)
    //   .enter()
    //   .append('svg:text')
    //   .attr('x', function (d) {
    //     return d.x;
    //   })
    //   .attr('y', function(d) {
    //     return d.y;
    //   })
    //   .text(function(d, i) {
    //     return d.value;
    //   })
    //   .attr('fill', c)
    //   .attr('dy', '12px')
    //   .attr('font-size', '12px');
  }

  /**
   * @函数名称:drawPath
   * @param
   * @作用:绘制省市
   * @return:obj
   * @date 2018/7/10
   */
  drawPath() {
    const me = this;
    const g = me.map;
    const res = me.mapData;
    const path = this.path;
    const bag = me.getColor('bg');
    const sl = me.getColor('stroke');
    const fl = me.getColor('fill');

    g
      .append('g')
      .selectAll('path')
      .data(res['features'])
      .enter()
      .append('path')
      .on('mouseover', function(d, i) {
        // d3.select(this).attr('fill','yellow');
        me.tip.html(d.properties.name);
        me.tip.show();
      })
      .on('mouseout', function(d, i) {
        // d3.select(this).attr('fill','#ffffff');
        me.tip.hide();
      })
      .attr('fill', function(d, i) {
        return fl;
      })
      .attr('stroke', bag)
      .attr('d', path);
  }

  /**
   * @函数名称:drawText todo 命名要改 业务函数要抽出来
   * @param
   * @作用:绘制省市地名
   * @date 2018/7/10
   */
  drawText() {
    const me = this;
    const g = me.map.append('g');
    const res = me.mapData;
    const path = this.path;
    const c = this.getColor('text');
    const projection = this.projection;
    g
      .selectAll('text')
      .data(res['features'])
      .enter()
      .append('svg:text')
      .text(function(d, i) {
        return d.properties.name;
      })
      .attr('fill', c)
      .attr('transform', function(d) {
        return 'translate(' + projection(d.properties.cp) + ')';
      })
      .attr('dy', '12px')
      .attr('font-size', '12px')
      .text(function(d) {
        return d.properties.name;
      });
  }
  drawLine() {
    // 2. Use the margin convention practice
    const h = parseInt(this.height, 10);
    const lineH = 100;
    const top = h - lineH - 20;
    const margin = { top: top, right: 20, bottom: 50, left: 50 },
      width = this.svgWidth - margin.left - margin.right, // Use the window's width
      // height = parseInt(this.height, 10) - margin.top - margin.bottom; // Use the window's height
      height = lineH; // Use the window's height

    // The number of datapoints
    const n = 25;

    // 5. X scale will use the index of our data
    const xScale = d3
      .scaleLinear()
      .domain([0, n - 1]) // input
      .range([0, width]); // output

    // 6. Y scale will use the randomly generate number
    const yScale = d3
      .scaleLinear()
      .domain([0, 1]) // input
      .range([height, 0]); // output
    const lineColor = this.getColor('line');
    // 7. d3's line generator
    const line = d3
      .line()
      .x(function(d, i) {
        // set the x values for the line generator
        return xScale(i);
      })
      .y(function(d, i) {
        // set the y values for the line generator
        return yScale(d['y']);
      })
      .curve(d3.curveMonotoneX); // apply smoothing to the line

    // 8. An array of objects of length N. Each object has key -> value pair, the key being 'y' and the value is a random number
    const dataset = d3.range(n).map(function(d) {
      return { y: d3.randomUniform(1)() };
    });

    // 1. Add the SVG to the page and employ #2
    const g = this.svg
      .append('g')
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom)
      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    // 3. Call the x axis in a group tag
    g
      .append('g')
      .attr('class', 'x axis')
      .attr('transform', 'translate(0,' + height + ')')
      .call(d3.axisBottom(xScale))
      .attr('class', 'd3-line-axis'); // Create an axis component with d3.axisBottom

    // 4. Call the y axis in a group tag
    g
      .append('g')
      .attr('class', 'y axis')
      .call(d3.axisLeft(yScale))
      .attr('class', 'd3-line-axis'); // Create an axis component with d3.axisLeft

    // 9. Append the path, bind the data, and call the line generator
    g
      .append('path')
      .datum(dataset) // 10. Binds data to the line
      .attr('fill', 'none') // Assign a class for styling
      .attr('stroke', lineColor) // Assign a class for styling
      .attr('stroke-width', '1') // Assign a class for styling
      .attr('d', line); // 11. Calls the line generator

    // 12. Appends a circle for each datapoint
    g
      .selectAll('circle')
      .data(dataset)
      .enter()
      .append('circle') // Uses the enter().append() method
      .attr('fill', lineColor) // Assign a class for styling
      // .attr('stroke', '#4e6574') // Assign a class for styling
      .attr('cx', function(d, i) {
        return xScale(i);
      })
      .attr('cy', function(d) {
        return yScale(d.y);
      })
      .attr('r', 3);
  }
  /**
   * @函数名称:loadData
   * @param data 业务模块传进来的数据 用于绘制
   * @作用:给业务模块留的沟子 让模块在数据封装完后调用
   * @return:obj
   * @date 2018/7/13
   */
  loadData(data) {
    // 绘制图例
    this.drawLegend();
    this.drawBase(data);
    this.drawLine();
  }
  /**
   * @函数名称:drawBase
   * @param data  坐标数据集合
   * @作用:绘制摄像头底座的圆圈
   * @date 2018/7/9
   */
  drawBase(data) {
    data = this.legendData;
    const me = this;
    const g = me.map;
    const cbg = me.getColor('cbg');
    const baseR = me.baseR;
    g
      .append('g')
      .selectAll('circle')
      .data(data)
      .enter()
      .append('circle')
      .attr('cx', function(d, i) {
        const x = d.location[0];
        return x;
      })
      .attr('cy', function(d, i) {
        const y = d.location[1];
        return y;
      })
      .attr('fill', function(d, i) {
        return data[i].color;
      }) // assets/img/charts/red.svg
      .attr('r', function(d, i) {
        return baseR;
      })
      .attr('cursor', function(d, i) {
        return me.getCursorByState(d.state);
      })
      .on('mouseover', function(d, i) {
        me.tip.html(d.name);
        me.tip.show();
      })
      .on('mouseout', function(d, i) {
        me.tip.hide();
      })
      .on('click', function(d, i) {
        me.showInterface(d, i);
      });

    this.drawDynamicRipple(data);
  }

  /**
   * @函数名称:drawDynamicRipple
   * @param data 接口数据
   * @param begin  波纹开始时间
   * @作用: 绘制动态波纹动画
   * @return:obj
   * @date 2018/7/16
   */
  drawDynamicRipple(data) {
    const me = this;
    const r = me.baseR;
    const g = me.map;
    const cdom = g.append('g');
    const len = data.length;
    for (let i = 0; i < len; i++) {
      const singleItem = data[i];
      // 先绘制一个circle
      for (let j = 0; j < 2; j++) {
        const circle = cdom
          .append('circle')
          .attr('cx', function() {
            return singleItem.location[0];
          })
          .attr('cy', function() {
            return singleItem.location[1];
          })
          .attr('opacity', 1)
          .attr('fill', function() {
            return singleItem.color;
          })
          .attr('stroke', function() {
            return singleItem.color;
          })
          .attr('cursor', function() {
            return me.getCursorByState(singleItem.state);
          })
          .on('click', function() {
            me.showInterface(singleItem, i);
          })
          .on('mouseover', function() {
            me.tip.html(singleItem.name);
            me.tip.show();
          })
          .on('mouseout', function() {
            me.tip.hide();
          })
          .attr('stroke-width', 2);
        // 在circle里 添加animate
        circle
          .append('animate')
          .attr('attributeName', 'r')
          .attr('from', r)
          .attr('to', singleItem.level * 5 + r)
          .attr('begin', j * 3 + 's')
          .attr('dur', '6s') // dur连续的 end 非连续
          .attr('repeatCount', 'indefinite');
        circle
          .append('animate')
          .attr('attributeName', 'opacity')
          .attr('from', 0.4)
          .attr('to', 0)
          .attr('begin', j * 3 + 's')
          .attr('dur', '6s') // dur连续的 end 非连续
          .attr('repeatCount', 'indefinite');
      }
    }
  }

  /**
   * getCursorByState  根据state状态改变鼠标样式
   * @param state  数据状态
   * @returns {any}
   */
  getCursorByState(state) {
    return 'pointer';
    // if (state === 'abnormal') {
    //   return 'pointer';
    // } else {
    //   return '';
    // }
  }

  /**
   * @函数名称:getScale
   * @作用:绘制加载过程中地图需要动态放大缩小 todo 使用函数 后面允许业务模块 动态改变这个值
   * @date 2018/7/10
   */
  getScale() {
    return 10000;
  }

  /**
   * tio 处理
   * @param d
   * @returns {D3Tooltip}
   */
  initTip() {
    function D3Tooltip() {}

    D3Tooltip.prototype.html = function(html) {
      this.$el = d3.select('#app-D3chart_tip');
      this.$el.html(html);
    };

    D3Tooltip.prototype.show = function() {
      const d3Event = d3.event;
      this.$el
        .transition()
        .duration(200)
        .style('opacity', 0.8);
      this.$el
        .style('left', d3Event.pageX - 128 + 'px')
        .style('top', d3Event.pageY - 128 + 'px');
    };

    D3Tooltip.prototype.hide = function() {
      this.$el
        .transition()
        .duration(500)
        .style('opacity', 0);
    };
    return new D3Tooltip();
  }

  /**
   * 颜色管理
   * @param t
   * @returns {any | string}
   */
  getColor(t) {
    const color = {
      fill: '#11293a', // 陕西省模块填充色
      stroke: '#696969', // 其他省市秒边色
      text: '#04070a', //  文字颜色
      bg: '#153348', // 背景色
      cbg: '#fff', // 圆圈背景色
      abnormal: '#d81e06', // 异常颜色
      normal: '#1f900c', // 正常色
      piping: '#018493', // 管道颜色
      line: '#008AD1',
    };
    return color[t] || '#fff';
  }

  @Output('getInterface') // 获取接口函数
  getInterface: EventEmitter<number> = new EventEmitter<number>();
  /**
   * @函数名称:showInterface
   * @param url  点击数据集合
   * @param index  点击数据的条数
   * @作用: 点击摄像头跳转接口
   * @date 2018/7/18
   */
  showInterface(url, index) {
    this.getInterface.emit(url);
  }
  ngAfterContentInit() {}

  ngOnDestroy() {}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值