Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
205 views
in Technique[技术] by (71.8m points)

javascript - D3: slow zoomable heatmap

I have this zoomable heatmap, which looks too slow when zooming-in or out. Is there anything to make it faster/smoother or it is just too many points and that is the best I can have. I was wondering if there is some trick to make it lighter for the browser please while keeping enhancements like tooltips. Or maybe my code handling the zoom feature is not great .

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <style>
        .axis text {
            font: 10px sans-serif;
        }

        .axis path,
        .axis line {
            fill: none;
            stroke: #000000;
        }

        .x.axis path {
            //display: none;
        }

        .chart rect {
            fill: steelblue;
        }

        .chart text {
            fill: white;
            font: 10px sans-serif;
            text-anchor: end;
        }
        
        #tooltip {
          position:absolute;
          background-color: #2B292E;
          color: white;
          font-family: sans-serif;
          font-size: 15px;
          pointer-events: none; /*dont trigger events on the tooltip*/
          padding: 15px 20px 10px 20px;
          text-align: center;
          opacity: 0;
          border-radius: 4px;
        }
    </style>
    <title>Bar Chart</title>

    <!-- Reference style.css -->
    <!--    <link rel="stylesheet" type="text/css" href="style.css">-->

    <!-- Reference minified version of D3 -->
    <script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>

<body>
    <div id="chart" style="width: 700px; height: 500px"></div>
    <script>
        var dataset = [];
        for (let i = 1; i < 360; i++) {
            for (j = 1; j < 75; j++) {
                dataset.push({
                    day: i,
                    hour: j,
                    tOutC: Math.random() * 25,
                })
            }
        };


        var days = d3.max(dataset, function(d) {
                return d.day;
            }) -
            d3.min(dataset, function(d) {
                return d.day;
            });
        var hours = d3.max(dataset, function(d) {
                return d.hour;
            }) -
            d3.min(dataset, function(d) {
                return d.hour;
            });

        var tMin = d3.min(dataset, function(d) {
                return d.tOutC;
            }),
            tMax = d3.max(dataset, function(d) {
                return d.tOutC;
            });

        var dotWidth = 1,
            dotHeight = 3,
            dotSpacing = 0.5;

        var margin = {
                top: 0,
                right: 25,
                bottom: 40,
                left: 25
            },
            width = (dotWidth * 2 + dotSpacing) * days,
            height = (dotHeight * 2 + dotSpacing) * hours; 


        var colors = ['#2C7BB6', '#00A6CA','#00CCBC','#90EB9D','#FFFF8C','#F9D057','#F29E2E','#E76818','#D7191C'];

        var xScale = d3.scaleLinear()
            .domain(d3.extent(dataset, function(d){return d.day}))
            .range([0, width]);

        var yScale = d3.scaleLinear()
            .domain(d3.extent(dataset, function(d){return d.hour}))
            .range([(dotHeight * 2 + dotSpacing) * hours, dotHeight * 2 + dotSpacing]);

        var colorScale = d3.scaleQuantile()
            .domain([0, colors.length - 1, d3.max(dataset, function(d) {
                return d.tOutC;
            })])
            .range(colors);

        var xAxis = d3.axisBottom().scale(xScale);



        // Define Y axis
        var yAxis = d3.axisLeft().scale(yScale);


        var zoom = d3.zoom()
            .scaleExtent([dotWidth, dotHeight])
            .translateExtent([
                [80, 20],
                [width, height]
            ])
            .on("zoom", zoomed);

        var tooltip = d3.select("body").append("div")
        .attr("id", "tooltip")
        .style("opacity", 0);

        // SVG canvas
        var svg = d3.select("#chart")
            .append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .call(zoom)
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        // Clip path
        svg.append("clipPath")
            .attr("id", "clip")
            .append("rect")
            .attr("width", width)
            .attr("height", height);


        // Heatmap dots
        svg.append("g")
            .attr("clip-path", "url(#clip)")
            .selectAll("ellipse")
            .data(dataset)
            .enter()
            .append("ellipse")
            .attr("cx", function(d) {
                return xScale(d.day);
            })
            .attr("cy", function(d) {
                return yScale(d.hour);
            })
            .attr("rx", dotWidth)
            .attr("ry", dotHeight)
            .attr("fill", function(d) {
                return colorScale(d.tOutC);
            })
            .on("mouseover", function(d){
                $("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
                var xpos = d3.event.pageX +10;
                var ypos = d3.event.pageY +20;
                $("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
            }).on("mouseout", function(){
                $("#tooltip").animate({duration: 500}).css("opacity",0);
            }); 

        //Create X axis
        var renderXAxis = svg.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + yScale(0) + ")")
            .call(xAxis)

        //Create Y axis
        var renderYAxis = svg.append("g")
            .attr("class", "y axis")
            .call(yAxis);


        function zoomed() {
            // update: rescale x axis
            renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));

            update();
        }

        function update() {
            // update: cache rescaleX value
            var rescaleX = d3.event.transform.rescaleX(xScale);
            svg.selectAll("ellipse")
                .attr('clip-path', 'url(#clip)')
                // update: apply rescaleX value
                .attr("cx", function(d) {
                    return rescaleX(d.day);
                })
//                .attr("cy", function(d) {
//                    return yScale(d.hour);
//                })
                // update: apply rescaleX value
                .attr("rx", function(d) {
                    return (dotWidth * d3.event.transform.k);
                })
                .attr("fill", function(d) {
                    return colorScale(d.tOutC);
                });
        }
        

        
        
    </script>
</body>

</html>
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The solution is not to update all the dots for the zoom but to apply the zoom transform to the group containing the dots. Clipping of the group needs to be done on an additional parent g heatDotsGroup.

The zoom scale of y is taken care of (set it fixed to 1) with a regex replace, limit translate in y by setting the transform.y to 0, and limit the translate of x based on the current scale.

Allow a little translate past 0 to show the first dot complete when zoomed in.

    var zoom = d3.zoom()
        .scaleExtent([dotWidth, dotHeight])
        .on("zoom", zoomed);

    // Heatmap dots
    var heatDotsGroup = svg.append("g")
        .attr("clip-path", "url(#clip)")
        .append("g");

    heatDotsGroup.selectAll("ellipse")
        .data(dataset)
        .enter()
        .append("ellipse")
        .attr("cx", function(d) { return xScale(d.day); })
        .attr("cy", function(d) { return yScale(d.hour); })
        .attr("rx", dotWidth)
        .attr("ry", dotHeight)
        .attr("fill", function(d) { return colorScale(d.tOutC); })
        .on("mouseover", function(d){
            $("#tooltip").html("X: "+d.day+"<br/>Y:"+d.hour+"<br/>Value:"+Math.round(d.tOutC*100)/100);
            var xpos = d3.event.pageX +10;
            var ypos = d3.event.pageY +20;
            $("#tooltip").css("left",xpos+"px").css("top",ypos+"px").animate().css("opacity",1);
        }).on("mouseout", function(){
            $("#tooltip").animate({duration: 500}).css("opacity",0);
        }); 

    function zoomed() {
        d3.event.transform.y = 0;
        d3.event.transform.x = Math.min(d3.event.transform.x, 5);
        d3.event.transform.x = Math.max(d3.event.transform.x, (1-d3.event.transform.k) * width );

        // update: rescale x axis
        renderXAxis.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));

        heatDotsGroup.attr("transform", d3.event.transform.toString().replace(/scale((.*?))/, "scale($1, 1)"));
    }

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...