【问题标题】:How to apply specific colors to D3.js map based on data values?如何根据数据值将特定颜色应用于 D3.js 地图?
【发布时间】:2017-07-21 15:26:02
【问题描述】:

我有一张美国县地图,该地图应该以动画形式显示县在 14 天内的产水量。我需要以红色(小于 50 毫米)、绿色(大于 49 毫米且小于 100 毫米)和蓝色(大于 100 毫米)显示颜色。我改编自 Mike Bostock 和 Rich Donohue 的以下代码:

<style>
.county {
    fill:steelblue;
    stroke: #fff; /*White*/
    stroke-width: .5px;
}

#play, #clock {
    position: absolute;
    /*top: 15px;*/
}

#play {
    /*left: 15px;*/
    left: 160px;
    top: 140px;
}

#clock {
    left: 220px;
    top: 148px;
}

<button id="play">Play</button>
<span id="clock">Day</span>
<h1 style="text-align:center">14-Day Water Yield By County</h1>

<div id="svgDiv1" style="text-align:center">
<svg width="960" height="600" stroke-linejoin="round" stroke-linecap="round">
    <defs>
        <filter id="blur">
            <feGaussianBlur stdDeviation="5"></feGaussianBlur>
        </filter>
    </defs>
</svg>

<script>

//globals
var width, height, projection, path, group, graticule, svg, defs, attributeArray = [], currentAttribute = 0, playing = false;

function init() {

    setMap();
    animateMap();

}

function setMap() {

    svg = d3.select("svg");

    defs = svg.select("defs");

    path = d3.geoPath();

    d3.json("/topo/us-10m.v1.json", function (error, us) {
        if (error) throw error;

        defs.append("path")
            .attr("id", "nation")
            .attr("d", path(topojson.feature(us, us.objects.counties)));

        svg.append("use")
            .attr("xlink:href", "#nation")
            .attr("fill-opacity", 0.2)
            .attr("filter", "url(#blur)");

        svg.append("use")
            .attr("xlink:href", "#nation")
            .attr("fill", "#fff");

        svg.append("path")
            .attr("fill", "none")
            .attr("stroke", "#777")
            .attr("stroke-width", 0.70)
            .attr("d", path(topojson.mesh(us, us.objects.counties, function (a, b) { return a !== b; })));
    });

    loadData();  // let's load our data next
}

function loadData() {
    queue()   // queue function loads all external data files asynchronously
      .defer(d3.json, "/topo/us-10m.v1.json")  // our geometries
      .defer(d3.csv, "/data/wtryld.csv")  // and associated data in csv file
      .await(processData);   // once all files are loaded, call the processData function passing the loaded objects as arguments
}

function processData(error, us, countyData) {
    // function accepts any errors from the queue function as first argument, then
    // each data object in the order of chained defer() methods above
    if (error) throw error;

    //Get values from geojson
    var conus = topojson.feature(us, us.objects.counties); // store the path in variable for ease

    //Get values from csv file
    for (var i in conus.features) {    // for each geometry object
        for (var j in countyData) {  // for each row in the CSV
            if (conus.features[i].id == countyData[j].id) {  // if they match
                for (var k in countyData[i]) {   // for each column in the a row within the CSV
                    if (k != 'id' && k != 'County') {  // select only number of days as column headings
                        if (attributeArray.indexOf(k) == -1) {
                            attributeArray.push(k);  // add new column headings to our array for later
                        }
                        conus.features[i].properties[k] = Number(countyData[j][k])  // add each CSV column key/value to geometry object
                    }
                }
                break;  // stop looking through the CSV since we made our match
            }
        }
    }
    d3.select('#clock').html(attributeArray[currentAttribute]);  // populate the clock initially with the current day
    drawMap(conus);  // let's mug the map now with our newly populated data object
}

//Sort function; can specify multiple columns to sort: propSort("STATE", "COUNTY");
function propSort(props) {
    if (!props instanceof Array) props = props.split(",");
    return function sort(a, b) {
        var p;
        a = a.properties;
        b = b.properties;
        for (var i = 0; i < props.length; i++) {
            p = props[i];
            if (typeof a[p] === "undefined") return -1;
            if (a[p] < b[p]) return -1;
            if (a[p] > b[p]) return 1;
        }
        return 0;
    };
}

function drawMap(conus) {

    svg.selectAll(".feature")   // select country objects (which don't exist yet)
      .data(conus.features)   // bind data to these non-existent objects
      .enter().append("path") // prepare data to be appended to paths
      .attr("class", "county") // give them a class for styling and access later
      .attr("id", function (d) { return d.properties.id; }, true)  // give each a unique id for access later
      .attr("d", path); // create them using the svg path generator defined above

    var dataRange = getDataRange(); // get the min/max values from the current day's range of data values
    d3.selectAll('.county')  // select all the counties
        .attr('fill-opacity', function (d) {
            return getColor(d.properties[attributeArray[currentAttribute]], dataRange);  // give them an opacity value based on their current value
        });
}

function sequenceMap() {
    var dataRange = getDataRange(); // get the min/max values from the current year's range of data values
    d3.selectAll('.county').transition()  //select all the counties and prepare for a transition to new values
      .duration(300)  // give it a smooth time period for the transition
      .attr('fill-opacity', function (d) {
          return getColor(d.properties[attributeArray[currentAttribute]], dataRange);  // the end color value
      })
}

function getColor(valueIn, valuesIn) {
    // create a linear scale
    var color = d3.scale.linear()
      .domain([valuesIn[0], valuesIn[1]])  // input uses min and max values
      .range([.3, 1]);   // output for opacity between .3 and 1 %

    return color(valueIn);  // return that number to the caller
}

function getDataRange() {
    // function loops through all the data values from the current data attribute
    // and returns the min and max values

    var min = Infinity, max = -Infinity;
    d3.selectAll('.county')
        .each(function (d, i) {
          var currentValue = d.properties[attributeArray[currentAttribute]];
          if (currentValue <= min && currentValue != -99 && currentValue != 'undefined') {
              min = currentValue;
          }
          if (currentValue >= max && currentValue != -99 && currentValue != 'undefined') {
              max = currentValue;
          }
      });
    return [min, max];
}

function animateMap() {

    var timer;  // create timer object
    d3.select('#play')
      .on('click', function () {  // when user clicks the play button
          if (playing == false) {  // if the map is currently playing
              timer = setInterval(function () {   // set a JS interval
                  if (currentAttribute < attributeArray.length - 1) {
                      currentAttribute += 1;  // increment the current attribute counter
                  } else {
                      currentAttribute = 0;  // or reset it to zero
                  }
                  sequenceMap();  // update the representation of the map
                  d3.select('#clock').html(attributeArray[currentAttribute]);  // update the clock
              }, 2000);

              d3.select(this).html('Stop');  // change the button label to stop
              playing = true;   // change the status of the animation
          } else {    // else if is currently playing
              clearInterval(timer);   // stop the animation by clearing the interval
              d3.select(this).html('Play');   // change the button label to play
              playing = false;   // change the status again
          }
      });
}


window.onload = init();  // magic starts here

上面的代码通过使用填充不透明度来应用“choropleth”颜色。只有深浅不一的钢蓝色。但我需要应用绿色、蓝色和红色。

感谢任何帮助。

【问题讨论】:

    标签: d3.js


    【解决方案1】:

    您可以直接使用比例输出颜色(D3 比例范围接受颜色),而不是使用 css 设置所有特征的颜色,然后将线性比例的不透明度值应用于每个特征。然后,无需设置填充不透明度,只需设置填充即可。

    例如:

    var color = d3.scale.linear()
      .domain([0, 9])  
      .range(["blue", "green"]); 
      
    var svg = d3.select('body')
        .append('svg')
        .attr('width',500)
        .attr('height',200);
        
    svg.selectAll('rect')
        .data(d3.range(10))
        .enter()
        .append('rect')
        .attr('x',function(d,i) { return i * 40; })
        .attr('y',30)
        .attr('width',30)
        .attr('height',30)
        .attr('fill',function(d,i) { return color(i); });
          
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"&gt;&lt;/script&gt;

    请确保您的 css 仍然没有指定钢蓝色。

    您还可以使用十六进制颜色代码或指定多个步骤:

    var color = d3.scale.linear()
      .domain([0, 5, 9])  
      .range(["blue", "yellow", "green"]); 
      
    var svg = d3.select('body')
        .append('svg')
        .attr('width',500)
        .attr('height',200);
        
    svg.selectAll('rect')
        .data(d3.range(10))
        .enter()
        .append('rect')
        .attr('x',function(d,i) { return i * 40; })
        .attr('y',30)
        .attr('width',30)
        .attr('height',30)
        .attr('fill',function(d,i) { return color(i); });
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"&gt;&lt;/script&gt;

    但是,如果您希望每个值都有明确的步骤,您可能需要一个阈值比例:

    var color = d3.scale.threshold()
      .domain([2, 5, 9])  
      .range(["blue","yellow","green","orange"]); 
      
    var svg = d3.select('body')
        .append('svg')
        .attr('width',500)
        .attr('height',200);
        
    svg.selectAll('rect')
        .data(d3.range(10))
        .enter()
        .append('rect')
        .attr('x',function(d,i) { return i * 40; })
        .attr('y',30)
        .attr('width',30)
        .attr('height',30)
        .attr('fill',function(d,i) { return color(i); });
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"&gt;&lt;/script&gt;

    范围中的元素比阈值尺度的域多一个。想象一个单一的阈值,它会有一个大于一个值,一个小于一个值。

    【讨论】:

    • 感谢您的回复,安德鲁。我试过: var color = d3.scale.linear() .domain([0, 160, 325] //这些是 csv 文件中的最小值、平均值、最大值 .range["red", "green", "blue" ]); //干、中、湿颜色我无法让它访问实际的 csv 文件以读取每日值并根据读取的值应用颜色。因此,单击按钮时它没有动画。我希望我可以附上我的 csv 文件,但不知道如何在这个论坛中。
    • 需要澄清一些细节。原始脚本是否根据 csv 值成功修改了县的透明度?在没有任何动画的情况下,你可以在绘制县时为县上色吗?
    • 是的。当单击播放按钮并且透明胶片正常时,原始脚本会制作动画。此外,初始颜色设置为显示第 1 天数据。所以原始脚本正在运行。但是我的老板想要颜色,我认为类似于“热图”,您可以在其中看到随着时间的推移哪些县有干/湿天的趋势,因此这些地区的农民可以决定是否施肥以及他们的其他做法需要做的。
    • 为了更好地理解:当您在此处更改比例范围时:.range([.3, 1]); 仅为两种颜色(为了测试),删除填充的 css,并更改 fill-opacity 的所有引用到您的示例代码中的fill,您会得到黑色填充的所有内容吗?在这种情况下会发生什么?
    • 在将 'fill-opacity' 替换为 'fill' 后,一切都被黑色填充。单击按钮似乎执行动画,因为这一天从第 1 天循环到第 14 天。但是,填充部分没有着色。还是一片漆黑。
    猜你喜欢
    • 2021-07-24
    • 2019-12-10
    • 2018-10-14
    • 1970-01-01
    • 2021-12-06
    • 1970-01-01
    • 2016-06-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多