D3环形热力图:鼠标悬停时增加段落高度

4
我创建了一个声学数据的圆形热力图,每个圆环层代表同一天,圆环被分为24个部分,分别代表一天中的24小时。我想实现这样一个功能:当鼠标悬停在某个圆环上时,增加该日期所有圆环段的高度,同时相应地减小其他圆环段的高度,以保持弧半径不变。目前,我只能选择同一日期的所有圆环段,但我无法找到如何操作高度的方法。有人可以指导我吗?
以下是图片链接,当你悬停在某个圆环段上时会出现如下效果: enter image description here 以下是我的代码:
    var radial_labels = ['2016-10-22', '2016-10-23', '2016-10-24', '2016-10-25', '2016-10-26', '2016-10-27', '2016-10-28', '2016-10-29', '2016-10-30'];

    var segment_labels = ['0:00', '1:00', '2:00', '3:00', '4:00', '5:00', '6:00', '7:00', '8:00', '9:00', '10:00', '11:00','12:00','13:00','14:00','15:00','16:00','17:00','18:00','19:00','20:00','21:00','22:00','23:00'];

    loadCircularHeatMap(data,"#chart",radial_labels, segment_labels);


    function loadCircularHeatMap (dataset, dom_element_to_append_to,radial_labels,segment_labels) {

    var margin = {top: 50, right: 50, bottom: 50, left: 50};
    var width = 1000 - margin.left - margin.right;

    var height = width;
    var innerRadius = 100;// width/14;

    var segmentHeight = (width - margin.top - margin.bottom - 2*innerRadius )/(2*radial_labels.length);

    var chart = circularHeatChart()
    .innerRadius(innerRadius)
    .segmentHeight(segmentHeight)
    .domain([0,0.5,1])
    .range(["#ffffd9", "#7fcdbb" ,"#225ea8"])
    .radialLabels(radial_labels)
    .segmentLabels(segment_labels);

    chart.accessor(function(d) {return d.Average;})

    var svg = d3.select(dom_element_to_append_to)
    .selectAll('svg')
    .data([dataset])
    .enter()
    .append('svg')
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append('g')
    .attr("transform",
        "translate(" + ( (width )/2 - (radial_labels.length*segmentHeight + innerRadius)  ) + "," + margin.top + ")")
    .call(chart);




    var tooltip = d3.select(dom_element_to_append_to)
    .append('div')
    .attr('class', 'tooltip');

    tooltip.append('div')
    .attr('class', 'time');
    tooltip.append('div')
    .attr('class', 'average');
    tooltip.append('div')
    .attr('class', 'day');

    svg.selectAll("path")
    .on('mouseover', function(d) {
        console.log(d.Day);
        // increase the segment height of the one being hovered as well as all others of the same date
        // while decreasing the height of all others accordingly

        d3.selectAll("path.segment-"+d.Day).style("opacity", function (p) {return 0.6});

        tooltip.select('.time').html("<b> Time: " + d.Time + "</b>");
        tooltip.select('.day').html("<b> Date: " + d.Day + "</b>");
        tooltip.select('.average').html("<b> Value: " + d.Average + "</b>");
        tooltip.style('display', 'block');
        tooltip.style('opacity',2);
    })
    .on('mousemove', function(d) {
        tooltip.style('top', (d3.event.layerY + 10) + 'px')
        .style('left', (d3.event.layerX - 25) + 'px');
    })
    .on('mouseout', function(d) {
        tooltip.style('display', 'none');
        tooltip.style('opacity',0);
       //  var time = d.Time;
       //  var timeCleaned = time.split(":").join("-");
       //  var segment = d3.select("#segment-"+d.Day +"-"+timeCleaned); //designate selector variable for brevity
       //  var fillcolor = segment.select("desc").text();  //access original color from desc
       //  segment.style("fill", fillcolor);

        d3.selectAll("path.segment-"+d.Day).style("opacity", function (p) {return 1});
    })
    .append("desc") //append the current color as a desc element
    .text(function(d){ 
            var color = d3.scale.linear().domain([0,0.5,1]).range(["#ffffd9", "#7fcdbb" ,"#225ea8"]);
            // how to access a function within reusable charts
            console.log(color(d.Average));
            return color(d.Average);
        });
    }

function circularHeatChart() {
    var margin = {top: 20, right: 50, bottom: 50, left: 20},
    innerRadius = 20,
    numSegments = 24,
    segmentHeight = 20,
    domain = null,
    range = ["white", "red"],
    accessor = function(d) {return d;},
    radialLabels = segmentLabels = [];

    function chart(selection) {
        selection.each(function(data) {
            var svg = d3.select(this);

            var offset = innerRadius + Math.ceil(data.length / numSegments) * segmentHeight;
            g = svg.append("g")
                .classed("circular-heat", true)
                .attr("transform", "translate(" + parseInt(margin.left + offset) + "," + parseInt(margin.top + offset) + ")");

            var autoDomain = false;
            if (domain === null) {
                domain = d3.extent(data, accessor);
                autoDomain = true;
            }
            var color = d3.scale.linear().domain(domain).range(range);
            if(autoDomain)
                domain = null;

            g.selectAll("path").data(data)
                .enter().append("path")
                // .attr("class","segment")
                .attr("class",function(d){return "segment-"+d.Day})
                .attr("id",function(d){
                     var time = d.Time;
                     var timeCleaned = time.split(":").join("-");
                     return "segment-"+d.Day +"-"+timeCleaned;})
                .attr("d", d3.svg.arc().innerRadius(ir).outerRadius(or).startAngle(sa).endAngle(ea))
                .attr("stroke", function(d) {return '#252525';})
                .attr("fill", function(d) {return color(accessor(d));});

            // Unique id so that the text path defs are unique - is there a better way to do this?
            var id = d3.selectAll(".circular-heat")[0].length;


            //Segment labels
            var segmentLabelOffset = 5;
            var r = innerRadius + Math.ceil(data.length / numSegments) * segmentHeight + segmentLabelOffset;
            labels = svg.append("g")
                .classed("labels", true)
                .classed("segment", true)
                .attr("transform", "translate(" + parseInt(margin.left + offset) + "," + parseInt(margin.top + offset) + ")");

            labels.append("def")
                .append("path")
                .attr("id", "segment-label-path-"+id)
                .attr("d", "m0 -" + r + " a" + r + " " + r + " 0 1 1 -1 0");

            labels.selectAll("text")
                .data(segmentLabels).enter()
                .append("text")
                .append("textPath")
                .attr("xlink:href", "#segment-label-path-"+id)
                .style("font-size", "12px")
                .attr("startOffset", function(d, i) {return i * 100 / numSegments + 1.5+ "%";})
                .text(function(d) {return d;});
        });

    }

    /* Arc functions */
    ir = function(d, i) {
        return innerRadius + Math.floor(i/numSegments) * segmentHeight;
    }
    or = function(d, i) {
        return innerRadius + segmentHeight + Math.floor(i/numSegments) * segmentHeight;
    }
    sa = function(d, i) {
        return (i * 2 * Math.PI) / numSegments;
    }
    ea = function(d, i) {
        return ((i + 1) * 2 * Math.PI) / numSegments;
    }

    /* Configuration getters/setters */
    chart.margin = function(_) {
        if (!arguments.length) return margin;
        margin = _;
        return chart;
    };

    chart.innerRadius = function(_) {
        if (!arguments.length) return innerRadius;
        innerRadius = _;
        return chart;
    };

    chart.numSegments = function(_) {
        if (!arguments.length) return numSegments;
        numSegments = _;
        return chart;
    };

    chart.segmentHeight = function(_) {
        if (!arguments.length) return segmentHeight;
        segmentHeight = _;
        return chart;
    };

    chart.domain = function(_) {
        if (!arguments.length) return domain;
        domain = _;
        return chart;
    };

    chart.range = function(_) {
        if (!arguments.length) return range;
        range = _;
        return chart;
    };

    chart.radialLabels = function(_) {
        if (!arguments.length) return radialLabels;
        if (_ == null) _ = [];
        radialLabels = _;
        return chart;
    };

    chart.segmentLabels = function(_) {
        if (!arguments.length) return segmentLabels;
        if (_ == null) _ = [];
        segmentLabels = _;
        return chart;
    };

    chart.accessor = function(_) {
        if (!arguments.length) return accessor;
        accessor = _;
        return chart;
    };

    return chart;
}

以下是当前的演示: http://jhjanicki.github.io/circular_heat_acoustic


2
我用您的代码和数据数组制作了一个fiddle,这样可以更轻松地帮助您:https://jsfiddle.net/gerardofurtado/fw9k5gLf/ - Gerardo Furtado
增加分段高度时是否使用过渡效果? - blackmiaool
那是理想的情况,但即使首先找出如何在没有过渡的情况下完成它也是不错的。 - jhjanicki
@blackmiaool,你是否熟悉可重复使用的图表更新?如果是的话,如果你有时间,可以看一下https://stackoverflow.com/questions/44192283/d3-reusable-circular-heat-chart-cannot-successfully-update - jhjanicki
2个回答

2

由于这是一条路径,您无法增加其高度。但是您可以缩放该路径。

我的算法如下:

  • 在鼠标悬停时获取线段的质心。
  • 相对于路径的质心将路径缩放到1.5。
  • 将所选路径移动到顶部
  • 在鼠标移出时将路径缩放回1

获取质心的代码片段:

function getBoundingBoxCenter (selection) {
  // get the DOM element from a D3 selection
  // you could also use "this" inside .each()
  var element = selection.node();
  // use the native SVG interface to get the bounding box
  var bbox = element.getBBox();
  // return the center of the bounding box
  return [bbox.x + bbox.width/2, bbox.y + bbox.height/2];
}

将所选元素移到顶部的代码片段

d3.selection.prototype.moveToFront = function() {
  return this.each(function(){
    this.parentNode.appendChild(this);
  });
};

当鼠标悬停时,您需要执行以下操作:

        var sel = d3.select(this);
    sel.moveToFront()

        var centroid = getBoundingBoxCenter(d3.select(this));
        //zoom in to the centroid
        d3.select(this).attr("transform", function(p) {
                return "translate(" + centroid[0] + "," + centroid[1] + ")"
                        + "scale(" + 1.5 + ")"
                        + "translate(" + -centroid[0] + "," + -centroid[1] + ")";
  });

鼠标移出时执行:
        var centroid = getBoundingBoxCenter(d3.select(this));
        d3.select(this).attr("transform", function(p) {
                return "translate(" + centroid[0] + "," + centroid[1] + ")"
                        + "scale(" + 1 + ")"
                        + "translate(" + -centroid[0] + "," + -centroid[1] + ")";
  });

感谢@Gerardo在fiddle上提供此代码 :)

可工作的代码在这里


1
谢谢您的解释,我现在更好地理解路径是如何工作的。扩展您的代码以使同一层中的所有段都被放大并缩小其他层(那些与悬停日期不同的层)是否很简单? - jhjanicki
哦,对于整行缩放的问题是,段落会重叠在一起...看起来不太好,但是你可以尝试一下。 - Cyril Cherian
好的,我可能还是会尝试一下。所以从我的理解来看,也许我正在尝试使用路径实现的目标很难? - jhjanicki
你可以对路径进行缩放以增加图案的大小...这并不难。 - Cyril Cherian
如果您想放大所有路径,那么它看起来会像这样 https://jsfiddle.net/m7gqvjxk/1/,显得很混乱。 - Cyril Cherian
你好,你知道这个问题的答案吗?http://stackoverflow.com/questions/43696831/d3-force-directed-graph-why-dont-the-flags-appear - Coder1000

2

我的演示

核心代码如下:

var targetIndex=Math.floor(i/numSegments);//the layer you are hovering
var zoomSize=10;//inner 10px and outer 10px
var layerCnt=data.length/numSegments;


d3.selectAll("path.segment")//.arc indicates segment
    .transition().duration(200)//transtion effect
    .attr("d", d3.svg.arc()//set d again
        .innerRadius(ir)
        .outerRadius(or)
        .startAngle(sa)
        .endAngle(ea))


function getRadius(floor) {
    if(floor===0){//inner radius doesn't change
        return innerRadius;
    }
    if(floor===layerCnt){//outer radius doesn't change
        return innerRadius+layerCnt*segmentHeight;
    } 
    if(floor<=targetIndex){//it's math
        return innerRadius + floor * segmentHeight - zoomSize *(floor/targetIndex);    
    }else{//math again
        return innerRadius + floor * segmentHeight + zoomSize*((layerCnt-floor)/(layerCnt-targetIndex));  
    }                    
}

function ir(d, i) {                    
    return getRadius(Math.floor(i / numSegments));
}

function or(d, i) {
    return getRadius(Math.floor(i / numSegments) + 1);
}

太棒了,谢谢你,这正是我在寻找的! - jhjanicki
我再次阅读代码,你能解释一下为什么需要将它放在绘制函数中并使用setTimeout吗? - jhjanicki
@jhjanicki 我把我的草稿留在那里了...我更新了我的答案以尽量减少更改。 - blackmiaool
谢谢。你能解释一下getRadius函数吗? - jhjanicki
@jhjanicki 这是纯数学,很难解释 :( - blackmiaool

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接