关于可视化:d3地图开发笔记

30次阅读

共计 8327 个字符,预计需要花费 21 分钟才能阅读完成。

最近做的一个 d3 地图工作,需要是画出中国地图,在指定区域显示亮色并在指定城市显示光晕动画,做下笔记备忘。

标几个将来可能会再次遇到的需要点:

地图中指定区域设置色彩

通过.style(fill) 回调返回自定义填充色,指定区域上的 text 也可通过此办法自定义色彩

let gmap = svg.append("g")

gmap.selectAll("path")
      .data(mapdata)
      .enter()
      .append("path")
      .style("fill", (d, i, a) => {if (Object.keys(cityordinate).includes(d.properties.name)) {return 'rgba(29, 223, 201, 1)'
        }
        return "rgba(62, 241, 230, .4)"
      });

光晕动画

用 d3.interval 设置 circle 的大小和透明度;d3.scaleLinear 设置映射参数和对应值的区间,用时间差取余作为参数造成 3000 毫秒一周期,绘制光晕成果

var scale = d3.scaleLinear();
    scale.domain([0, 1800, 3000])// 设置淡 > 亮 > 淡的趋势
      .range([0, 0.9, 0]);
    var scale2 = d3.scaleLinear();
    scale2.domain([0, 3000])
      .range([0, 8]);// 设置从小变大的趋势
    var start = Date.now();
 this.dInter = d3.interval(function () {var s1 = scale((Date.now() - start) % 3000);
      var s2 = scale2((Date.now() - start) % 3000) < 0 ? -scale2((Date.now() - start) % 3000) : scale2((Date.now() - start) % 3000);
      gmap.select("circle#hangzhouC")
        .attr("fill-opacity", s1)
        .attr("r", s2)
      marker.select("circle#markerC")
        .attr("fill-opacity", s1)
        .attr("r", s2)
    }, 100);

画两点射线

  • 调用之前建设的 project()办法,将起点经纬度转换为平面坐标。
  • 计算终点(长沙)和起点之前的间隔,做为线条长度和动画工夫参数。
  • 在线条上绘制一个圆形标记,并实现从终点到起点的挪动动画。
  • 标记挪动到起点后,即删除,节俭资源。
// 创立办法,输出 data 坐标,绘制发射线,此处按需要把连线 stroke 设为通明
    let mline = svg.append("g").attr("id", "moveto").attr("stroke", "rgba(255, 192, 67, 0)").attr("stroke-width", 1.5).attr("fill", "#FFC043");
    var moveto = function (city, data) {var pf = { x: projection([120.21, 30.25])[0], y: projection([120.21, 30.25])[1] };
      var pt = {x: projection(data)[0], y: projection(data)[1] };
      var distance = Math.sqrt((pt.x - pf.x) ** 2 + (pt.y - pf.y) ** 2);
      
      mline.append("line")
        .attr("x1", pf.x)
        .attr("y1", pf.y)
        .attr("x2", pt.x)
        .attr("y2", pt.y)
        .attr("marker-end", "url(#pointer)") // 设置线尾标识款式
        // .style("stroke-dasharray", "" + distance +", "+ distance +" ") // 可设置虚线然而没有画线成果了
        .transition()
        .duration(distance * 30) // 继续时长
        .styleTween("stroke-dashoffset", function () {return d3.interpolateNumber(distance, 0);
        });
      mline.append("circle")
        .attr("cx", pf.x)
        .attr("cy", pf.y)
        .attr("r", 2)
        .transition()
        .duration(distance * 30)
        .attr("transform", "translate(" + (pt.x - pf.x) + "," + (pt.y - pf.y) + ")")
        .remove(); // 射线头的款式,射线达到指标后移出线头的圆};

残缺的代码

initMap = async () => {let cityordinate = await this.getAllJoinMedical() // 获取已入驻城市数据
    var width = 750.43 * screenWidth / 1920, height = screenWidth <= minWidth ? 620 * screenHeight / 1080 : 690 * screenHeight / 1080;  // 定义 SVG 宽高
    let svg
    if (!this.svg) { // 因为父组件会刷新,这里避免画出第二个地图
      svg = d3.select(".fxmap")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
    } else {svg = this.svg}
    const defs = svg.append("defs"); // 插入 defs
    const linearGradient = defs //defs 中插入 <linearGradient>
      .append("linearGradient")
      .attr("id", "gradient"); // 设置对应 id
    linearGradient //linearGradient 中插入 stop 元素
      .append("stop")
      .attr("offset", "0%") // 设置坡度,下同
      .attr("stop-color", '#60F3F9');// 设置对应色彩,下同
    linearGradient //linearGradient 中插入 stop 元素
      .append("stop")
      .attr("offset", "20%") // 设置坡度,下同
      .attr("stop-color", '#60F3F9');// 设置对应色彩,下同
    linearGradient //linearGradient 中插入 stop 元素
      .append("stop")
      .attr("offset", "98%") // 设置坡度,下同
      .attr("stop-color", '#19B2BC');// 设置对应色彩,下同

    const lineDefs = svg.append("defs"); // 插入 defs
    const linearGradient2 = lineDefs //defs 中插入 <linearGradient>
      .append("linearGradient")
      .attr("id", "lineGradient"); // 设置对应 id
    linearGradient2 //linearGradient 中插入 stop 元素
      .append("stop")
      .attr("offset", "0%") // 设置坡度,下同
      .attr("stop-color", '#60F3F9');// 设置对应色彩,下同
    linearGradient2 //linearGradient 中插入 stop 元素
      .append("stop")
      .attr("offset", "20%") // 设置坡度,下同
      .attr("stop-color", '#60F3F9');// 设置对应色彩,下同
    linearGradient2 //linearGradient 中插入 stop 元素
      .append("stop")
      .attr("offset", "98%") // 设置坡度,下同
      .attr("stop-color", '#19B2BC');// 设置对应色彩,下同
    this.svg = svg
    let gmap2 = svg.append("g").attr("id", "map2") // 因为南海诸岛面积小不显色,边框又和背景色彩雷同,故重建画布用填充色画边框,将南海数据抽出填进去
      .attr("stroke", 'rgba(62, 241, 230, .4)').attr("stroke-width", 1)
      .attr("fill", "rgba(62, 241, 230, .4)");
    let gmap = svg.append("g").attr("id", "map")
      .attr("stroke", 'rgba(4, 23, 62, .6)').attr("stroke-width", 1)

    // var projection = d3.geoEquirectangular()
    var projection = d3.geoMercator() // 不同投影函数成果不同
      .center([465, 395])  // 指定投影核心,留神 [] 中的是经纬度
      .scale(height - 50) // 依据什么进行缩放
      .translate([width / 2 + 17, screenWidth <= minWidth ? height / 2 - 10 : height / 2 - 45]); // 图形在画布中偏移设置
    // var projection = d3
    // .geoStereographic()// 球形投影
    // .center([465, 395])  // 指定投影核心,留神 [] 中的是经纬度
    //   .scale(height)
    //   // .clipAngle(180)
    // .translate([width / 2, height / 2+20])
    var path = d3.geoPath().projection(projection);

    let marker = svg.append("defs")
      .append("marker")
      .append("marker")
      .attr("id", "pointer")
      .attr("viewBox", "0 0 24 24")    // 可见范畴
      .attr("markerWidth", "24")        // 标记宽度
      .attr("markerHeight", "24")       // 标记高度
      .attr("orient", "auto")         //
      .attr("markerUnits", "strokeWidth") // 随连接线宽度进行缩放
      .attr("refX", "6")              // 连接点坐标
      .attr("refY", "6")
    // 绘制标记核心圆
    marker.append("circle")
      .attr("cx", "6")
      .attr("cy", "6")
      .attr("r", "2")
      .attr("fill", "#FFC043");
    // 绘制标记外圆,之后在 timer()中增加闪动成果
    marker.append("circle")
      .attr("id", "markerC")
      .attr("cx", "6")
      .attr("cy", "6")
      .attr("r", "0")
      .attr("fill-opacity", "0")
      .attr("fill", "#FFC043")

    // 记录杭州坐标
    var hangzhou = projection([120.219375416, 30.2592444615]);
    // 读取地图数据,并绘制中国地图
    let mapdata = [];
    // 读取地图数据
    mapdata = chinaJson.features;
    let mapdata2 = chinaJson2.features;
    // 绘制地图
    gmap2.selectAll("path") // 画行政区边界
      .data(mapdata2)
      .enter()
      .append("path")
      .attr("d", path)
      .style("fill", (d, i, a) => {if (Object.keys(cityordinate).includes(d.properties.name)) { // 已入驻地图显示另一种色彩
          return 'rgba(29, 223, 201, 1)'
        }
        return "rgba(62, 241, 230, .4)"
      });

    gmap.selectAll("path")
      .data(mapdata)
      .enter()
      .append("path")
      .attr("d", path)
      .style("fill", (d, i, a) => {if (Object.keys(cityordinate).includes(d.properties.name)) {return 'rgba(29, 223, 201, 1)'
        }
        return "rgba(62, 241, 230, .4)"
      });
    svg
      .selectAll('text')
      .data(mapdata)
      .enter()
      .append('text') // 显示行政区名
      .text((d, i) => {return d.properties.name})
      .attr('font-size', 14)
      .attr("x", function (d) {let xNum = d.properties.centroid && Number(projection(d.properties.centroid)[0]) - (d.properties.name.length) * 14 / 2 || 0
        if (d.properties.name == '香港') return xNum + 20
        return xNum
      })
      .attr("y", function (d) {let yNum = d.properties.centroid && projection(d.properties.centroid)[1] || 0
        if (d.properties.name == '内蒙古') return yNum + 20
        if (d.properties.name == '澳门') return yNum + 12
        return yNum
      })
      .attr("stroke-width", 0)
      .style('fill', (d, i, a) => {if (Object.keys(cityordinate).includes(d.properties.name)) {return 'rgba(3, 2, 27, 1)' }
        return "rgba(29, 223, 201, 1)"
      })
    
    // 标记杭州
    gmap.append("circle").attr("id", "hangzhou")
      .attr("cx", hangzhou[0])
      .attr("cy", hangzhou[1])
      .attr("r", "4")
      .attr("fill", "#FFC043")
    gmap.append("circle").attr("id", "hangzhouC")
      .attr("cx", hangzhou[0])
      .attr("cy", hangzhou[1])
      .attr("r", "0")
      .attr("stroke-width", "2")
      .attr("fill-opacity", "0")
      .attr("fill", "#FFC043")
      .attr("stroke-opacity", 0)
      .attr("stroke", "#FFC043")

    // 创立办法,输出 data 坐标,绘制发射线,此处按需要把连线 stroke 设为通明
    let mline = svg.append("g").attr("id", "moveto").attr("stroke", "rgba(255, 192, 67, 0)").attr("stroke-width", 1.5).attr("fill", "#FFC043");
    var moveto = function (city, data) {var pf = { x: projection([120.21, 30.25])[0], y: projection([120.21, 30.25])[1] };
      var pt = {x: projection(data)[0], y: projection(data)[1] };
      var distance = Math.sqrt((pt.x - pf.x) ** 2 + (pt.y - pf.y) ** 2);
      
      mline.append("line")
        .attr("x1", pf.x)
        .attr("y1", pf.y)
        .attr("x2", pt.x)
        .attr("y2", pt.y)
        .attr("marker-end", "url(#pointer)") // 设置线尾标识款式
        // .style("stroke-dasharray", "" + distance +", "+ distance +" ") // 可设置虚线然而没有画线成果了
        .transition()
        .duration(distance * 30) // 继续时长
        .styleTween("stroke-dashoffset", function () {return d3.interpolateNumber(distance, 0);
        });
      mline.append("circle")
        .attr("cx", pf.x)
        .attr("cy", pf.y)
        .attr("r", 2)
        .transition()
        .duration(distance * 30)
        .attr("transform", "translate(" + (pt.x - pf.x) + "," + (pt.y - pf.y) + ")")
        .remove(); // 射线头的款式,射线达到指标后移出线头的圆};
    gmap2
      .selectAll('text')
      .data(mapdata2)
      .enter()
      .append('text')
      .text((d, i) => {return d.properties.name})
      .attr('font-size', 14)
      .attr("x", function (d) {let xNum = d.properties.centroid && Number(projection(d.properties.centroid)[0]) - (d.properties.name.length) * 14 / 2 || 0
        return xNum
      })
      .attr("y", function (d) {let yNum = d.properties.centroid && projection(d.properties.centroid)[1] || 0
        return yNum + 5
      })
      .attr("stroke-width", 0)
      .style('fill', (d, i, a) => {if (Object.keys(cityordinate).includes(d.properties.name)) {return 'rgba(3, 2, 27, 1)' }
        return "rgba(29, 223, 201, 1)"
      })
    // .style('fill', 'rgba(29, 223, 201, 1)')

    var scale = d3.scaleLinear(); // 映射函数
    scale.domain([0, 1800, 3000])// 映射区间,参数传入 1800 时返回 0.9, 设置淡 > 亮 > 淡的趋势
      .range([0, 0.9, 0]);
    var scale2 = d3.scaleLinear();
    scale2.domain([0, 3000])
      .range([0, 8]);// 设置从小变大的趋势
    var start = Date.now();
    this.dInter = d3.interval(function () {var s1 = scale((Date.now() - start) % 3000); // 三秒为周期由小到大循环
      var s2 = scale2((Date.now() - start) % 3000) < 0 ? -scale2((Date.now() - start) % 3000) : scale2((Date.now() - start) % 3000);
      gmap.select("circle#hangzhouC")
        .attr("fill-opacity", s1)
        .attr("r", s2)
      marker.select("circle#markerC")
        .attr("fill-opacity", s1)
        .attr("r", s2)
    }, 100);
    for (var key in cityordinate) {if (key == '杭州') continue
      moveto(key, cityordinate[key]); // 发射线
    };
  }

技术过程借鉴:无鱼二饼

正文完
 0