一、获取中国的地图数据
二、绘制过程中遇到的问题
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() {}
}