D3 Circular Heat Chart increase segment height on mouseover

时间:2017-04-10 02:21:15

标签: d3.js charts

I created a circular heat chart of acoustic data, each layer of a circle is the same day and there are 24 parts of the circle representing the 24 hours in a day. I want to implement something so I could increase the segment height of all segments of the same date as the segment that is being moused over, while decreasing the height of all other segments accordingly in order to keep the arc radius the same. Currently I am only able to select all segments from the same date, but I am having trouble figuring out how to manipulate the heights. Could someone point me in the right direction?

Here is an image where you mouseover a segment right now: enter image description here

Here is my code:

    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;
}

And here is a demo of what it is currently like: http://jhjanicki.github.io/circular_heat_acoustic

2 个答案:

答案 0 :(得分:2)

由于这是一条路径,因此无法增加它的高度。但是你可以扩展路径。

我的算法是

  • 在鼠标悬停时获取细分的质心。
  • 将鼠标上的路径缩放到路径的1.5 w.r.t质心。
  • 将所选路径移至顶部
  • 将鼠标移出的路径缩小为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把它放在一个小提琴上:)

工作代码here

答案 1 :(得分:2)

My demo

核心代码段如下:

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);
}