简体   繁体   English

D3-单折线图和多折线图工具提示

[英]D3 - Single and Multi Line chart tooltips

I'm pretty new to D3 and just incorporated the below tooltips into my application. 我是D3的新手,只是将以下工具提示合并到了我的应用程序中。 I have both a single line chart as well as a multi line chart. 我既有单个折线图,也有多个折线图。

Single line: https://bl.ocks.org/alandunning/cfb7dcd7951826b9eacd54f0647f48d3 单行: https : //bl.ocks.org/alandunning/cfb7dcd7951826b9eacd54f0647f48d3

Multi Line: Multiseries line chart with mouseover tooltip 多折线: 具有鼠标悬停工具提示的多系列折线图

As you can see, the functionality of the two tooltips are different. 如您所见,这两个工具提示的功能是不同的。 The Single Line tooltip jumps from each data point whereas the Multi Line continually follows the chart path. 单线工具提示从每个数据点跳转,而多线则持续遵循图表路径。 I want to change the Multi Line functionality to mimic how the Single Line tooltip works. 我想更改多行功能以模仿单行工具提示的工作方式。

Any help would be greatly appreciated. 任何帮助将不胜感激。 Please let me know if I need to provide more information. 如果需要提供更多信息,请告诉我。 Also note that the data I'm working with is an array of arrays 另请注意,我正在使用的数据是数组数组

Below is my code: 下面是我的代码:

Single Line Chart: 单线图:

let g = svg.append('g');
    g.append("path")
    .datum(this.dataObj)
    .attr("class",`line-${this.yAxisData} line`)
    .attr('d', line)
    .attr("stroke",`${this.color(this.dataObj.label)}`)
    .attr("fill",'none')
    .attr("transform",       `translate(${this.margin.left},${this.margin.top})`);

var focus = g.append("g")
    .attr("class", "focus")
    .style("display", "none");

    focus.append("line")
    .datum(this.dataObj)
    .attr("class", "x-hover-line hover-line")
    .attr("transform",`translate(${this.margin.left},${this.margin.top})`)
    .attr("stroke",`${this.color(this.dataObj.label)}`)
    .attr("y1", 0)
    .attr("y2", height);

    focus.append("circle")
    .datum(this.dataObj)
    .attr("transform",`translate(${this.margin.left},${this.margin.top})`)
    .attr("stroke",`${this.color(this.dataObj.label)}`)
    .attr("r", 7.5);

    focus.append("text")
    .attr("class","linetip")
    .attr("x", 40)
    .attr("dy", "0.5em");

    svg.append("rect")
    .attr("transform", `translate(${this.margin.left},${this.margin.top})`)
    .attr("class", "overlay")
    .attr("width", width)
    .attr("height", height)
    .on("mouseover", function() { focus.style("display", null); })
    .on("mouseout", function() { focus.style("display", "none"); })
    .on("mousemove", this.mousemove);

mousemove() {
    var bisectDate = d3.bisector(function(d) { return d.date; }).left;
    let mouse = d3.mouse(d3.event.currentTarget);
    let svg = d3.select(this.container);
    var x0 = this.x.invert(mouse[0]);
    var i = bisectDate(this.dataObj, x0);
    var d0 = this.dataObj[i - 1];
    var d1 = this.dataObj[i];
    var d = x0 - d0.date > d1.date - x0 ? d1 : d0;
    var focus = svg.select(".focus");
    focus.attr("transform", "translate(" + this.x(d[this.xAxisData]) + "," + this.y(d[this.yAxisData]) + ")");
    focus.select("text").text(`[${d[this.yAxisData]}]`);
    focus.select(".x-hover-line").attr("y2", this.height - this.y(d[this.yAxisData]));
    focus.select(".y-hover-line").attr("x2", this.width + this.width);
}

Multi Line Chart: 多折线图:

    //append paths
    let g = svg.append('g');
    let chartLines = g.selectAll('.lines')
    .data(this.dataObj)
    .enter()
    .append('g')
    .attr('class', 'lines');

    chartLines.append('path')
    .attr('class','line')
    .attr('d', d => {
        return line(d);
    })
    .attr('stroke', (d) => color(d[0].label))
    .attr('fill','none')
    .attr("transform", `translate(${this.margin.left},0)`);

    var mouseG = svg.append("g")
    .attr("class", "mouse-over-effects")

    mouseG.append("path") // this is the black vertical line to follow mouse
    .attr("class", "mouse-line")
    .style("stroke", "black")
    .style("stroke-width", "2px")
    .style("stroke-dasharray", "3,3")
    .style("opacity", "0");

    var mousePerLine = mouseG.selectAll('.mouse-per-line')
    .data(this.dataObj)
    .enter()
    .append("g")
    .attr("class", "mouse-per-line");

    mousePerLine.append("circle")
    .datum(d=>{return d})
    .attr("r", 7)
    .attr("stroke", (d,i) => {
        console.log(d)
        return `${this.color(d[i].label)}`
    })
    .style("fill", "none")
    .style("opacity", "0");

    mousePerLine.append("text")
    .datum(d=>{return d})
    .attr("transform", "translate(10,3)");

    mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
    .attr("transform", `translate(${this.margin.left},0)`)
    .attr('width', width) // can't catch mouse events on a g element
    .attr('height', height)
    .attr('fill', 'none')
    .attr('pointer-events', 'all')
    .on('mouseout', () => { // on mouse out hide line, circles and text
        d3.select(".mouse-line")
        .style("opacity", "0");
        d3.selectAll(".mouse-per-line circle")
        .style("opacity", "0");
        d3.selectAll(".mouse-per-line text")
        .style("opacity", "0");
    })
    .on('mouseover', () => { // on mouse in show line, circles and text
        d3.select(".mouse-line")
        .style("opacity", "1");
        d3.selectAll(".mouse-per-line circle")
        .style("opacity", "1");
        d3.selectAll(".mouse-per-line text")
        .style("opacity", "1");
    })
    .on('mousemove', () => {
        let mouse = d3.mouse(d3.event.currentTarget);
        d3.select(".mouse-line")
        .attr("d", () => {
            var d = "M" + mouse[0] + "," + height;
            d += " " + mouse[0] + "," + 0;
            return d;
        });
        d3.selectAll(".mouse-per-line")
        .attr("transform", (d, i) => {
            var lines = document.getElementsByClassName('line')
            var xDate = this.x.invert(mouse[0])
            var bisect = d3.bisector(function(d) { return d.date; }).right;
            var idx = bisect(this.dataObj, xDate);
            var beginning = 0,
            end = lines[i].getTotalLength()
            var target = null;

            while (true){
                var target = Math.floor((beginning + end) / 2);
                var pos = lines[i].getPointAtLength(target);
                if ((target === end || target === beginning) && pos.x !== mouse[0]) {
                    break;
                }
                if (pos.x > mouse[0])      end = target;
                else if (pos.x < mouse[0]) beginning = target;
                else break; //position found
            }

            d3.select('text')
            .text(this.y.invert(pos.y));

            return "translate(" + mouse[0] + "," + pos.y +")";
        });
    });

I took Mark 's answer as a reference from the Multiseries line chart with mouseover tooltip you provided. 我将Mark的答案作为您提供的鼠标悬停工具提示Multiseries折线图中的参考。

Basically, what you need to do is set the tooltips to show on each tick of the x-axis data, so instead of grabbing the position of the mouse with mouse[0] and moving the tooltips, you should move it to the position where the x-axis data is. 基本上,您需要做的是设置工具提示以显示在x轴数据的每个刻度上,因此,与其将鼠标[0]抓住鼠标的位置并移动工具提示,不如将其移动到x轴数据是。

Here's the detail of the changes I made: 这是我所做更改的详细信息:

mouseG.append('svg:rect')
    .attr('width', width)
    .attr('height', height)
    .attr('fill', 'none')
    .attr('pointer-events', 'all')
    .on('mouseout', () => {
        d3.select(".mouse-line")
        .style("opacity", "0");
        d3.selectAll(".mouse-per-line circle")
        .style("opacity", "0");
        d3.selectAll(".mouse-per-line text")
        .style("opacity", "0");
    })
    .on('mouseover', () => {
        d3.select(".mouse-line")
        .style("opacity", "1");
        d3.selectAll(".mouse-per-line circle")
        .style("opacity", "1");
        d3.selectAll(".mouse-per-line text")
        .style("opacity", "1");
    })
    .on('mousemove', () => {
        let mouse = d3.mouse(d3.event.currentTarget);
        // MOVE THIS BEFORE THE RETURN
        // d3.select(".mouse-line")
        // .attr("d", () => {
        //     var d = "M" + mouse[0] + "," + height;
        //     d += " " + mouse[0] + "," + 0;
        //     return d;
        // });
        d3.selectAll(".mouse-per-line")
        .attr("transform", (d, i) => {
            var lines = document.getElementsByClassName('line')
            var xDate = this.x.invert(mouse[0])
            var bisect = d3.bisector(function(d) { return d.date; }).right;
            var idx = bisect(this.dataObj, xDate);

            // GET RID OF THIS
            // var beginning = 0,
            // end = lines[i].getTotalLength()
            // var target = null;

            // while (true){
            //     var target = Math.floor((beginning + end) / 2);
            //     var pos = lines[i].getPointAtLength(target);
            //     if ((target === end || target === beginning) && pos.x !== mouse[0]) {
            //         break;
            //     }
            //     if (pos.x > mouse[0])      end = target;
            //     else if (pos.x < mouse[0]) beginning = target;
            //     else break; //position found
            // }

            // REPLACE pos.y WITH y(d.values[idx].temperature)
            // AND mouse[0] WITH x(d.values[idx].date)
            d3.select('text')
            .text(this.y.invert(pos.y));

            return "translate(" + mouse[0] + "," + pos.y +")";
        });
    });

Below is the fully working code with the changes applied. 以下是已应用更改的完整工作代码。 For this snippet I used interpolate('linear') to show the values correctly; 对于此代码段,我使用了interpolate('linear')来正确显示值。 if you use interpolate('basis') , the tooltips and lines will not match correctly: 如果您使用interpolate('basis') ,则工具提示和线条将无法正确匹配:

 <!DOCTYPE html> <html> <head> <script data-require="d3@3.5.3" data-semver="3.5.3" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script> <style> body { font: 10px sans-serif; } .axis path, .axis line { fill: none; stroke: #000; shape-rendering: crispEdges; } .x.axis path { display: none; } .line { fill: none; stroke: steelblue; stroke-width: 1.5px; } </style> </head> <body> <script> var myData = "date New York San Francisco Austin\\n\\ 20111001 63.4 62.7 72.2\\n\\ 20111002 58.0 59.9 67.7\\n\\ 20111003 53.3 59.1 69.4\\n\\ 20111004 55.7 58.8 68.0\\n\\ 20111005 64.2 58.7 72.4\\n\\ 20111006 58.8 57.0 77.0\\n\\ 20111007 57.9 56.7 82.3\\n\\ 20111008 61.8 56.8 78.9\\n\\ 20111009 69.3 56.7 68.8\\n\\ 20111010 71.2 60.1 68.7\\n\\ 20111011 68.7 61.1 70.3\\n\\ 20111012 61.8 61.5 75.3\\n\\ 20111013 63.0 64.3 76.6\\n\\ 20111014 66.9 67.1 66.6\\n\\ 20111015 61.7 64.6 68.0\\n\\ 20111016 61.8 61.6 70.6\\n\\ 20111017 62.8 61.1 71.1\\n\\ 20111018 60.8 59.2 70.0\\n\\ 20111019 62.1 58.9 61.6\\n\\ 20111020 65.1 57.2 57.4\\n\\ 20111021 55.6 56.4 64.3\\n\\ 20111022 54.4 60.7 72.4\\n"; var margin = { top: 20, right: 80, bottom: 30, left: 50 }, width = 400 - margin.left - margin.right, height = 250 - margin.top - margin.bottom; var parseDate = d3.time.format("%Y%m%d").parse; var x = d3.time.scale() .range([0, width]); var y = d3.scale.linear() .range([height, 0]); var color = d3.scale.category10(); var xAxis = d3.svg.axis() .scale(x) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .orient("left"); var line = d3.svg.line() .interpolate("linear") .x(function (d) { return x(d.date); }) .y(function (d) { return y(d.temperature); }); var svg = d3.select("body").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var data = d3.tsv.parse(myData); color.domain(d3.keys(data[0]).filter(function (key) { return key !== "date"; })); data.forEach(function (d) { d.date = parseDate(d.date); }); var cities = color.domain().map(function (name) { return { name: name, values: data.map(function (d) { return { date: d.date, temperature: +d[name] }; }) }; }); x.domain(d3.extent(data, function (d) { return d.date; })); y.domain([ d3.min(cities, function (c) { return d3.min(c.values, function (v) { return v.temperature; }); }), d3.max(cities, function (c) { return d3.max(c.values, function (v) { return v.temperature; }); }) ]); var legend = svg.selectAll('g') .data(cities) .enter() .append('g') .attr('class', 'legend'); legend.append('rect') .attr('x', width - 20) .attr('y', function (d, i) { return i * 20; }) .attr('width', 10) .attr('height', 10) .style('fill', function (d) { return color(d.name); }); legend.append('text') .attr('x', width - 8) .attr('y', function (d, i) { return (i * 20) + 9; }) .text(function (d) { return d.name; }); svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); svg.append("g") .attr("class", "y axis") .call(yAxis) .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", ".71em") .style("text-anchor", "end") .text("Temperature (ºF)"); var city = svg.selectAll(".city") .data(cities) .enter().append("g") .attr("class", "city"); city.append("path") .attr("class", "line") .attr("d", function (d) { return line(d.values); }) .style("stroke", function (d) { return color(d.name); }); city.append("text") .datum(function (d) { return { name: d.name, value: d.values[d.values.length - 1] }; }) .attr("transform", function (d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; }) .attr("x", 3) .attr("dy", ".35em") .text(function (d) { return d.name; }); var mouseG = svg.append("g") .attr("class", "mouse-over-effects"); mouseG.append("path") // this is the black vertical line to follow mouse .attr("class", "mouse-line") .style("stroke", "black") .style("stroke-width", "1px") .style("opacity", "0"); var lines = document.getElementsByClassName('line'); var mousePerLine = mouseG.selectAll('.mouse-per-line') .data(cities) .enter() .append("g") .attr("class", "mouse-per-line"); mousePerLine.append("circle") .attr("r", 7) .style("stroke", function (d) { return color(d.name); }) .style("fill", "none") .style("stroke-width", "1px") .style("opacity", "0"); mousePerLine.append("text") .attr("transform", "translate(10,3)"); mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas .attr('width', width) // can't catch mouse events on ag element .attr('height', height) .attr('fill', 'none') .attr('pointer-events', 'all') .on('mouseout', function () { // on mouse out hide line, circles and text d3.select(".mouse-line") .style("opacity", "0"); d3.selectAll(".mouse-per-line circle") .style("opacity", "0"); d3.selectAll(".mouse-per-line text") .style("opacity", "0"); }) .on('mouseover', function () { // on mouse in show line, circles and text d3.select(".mouse-line") .style("opacity", "1"); d3.selectAll(".mouse-per-line circle") .style("opacity", "1"); d3.selectAll(".mouse-per-line text") .style("opacity", "1"); }) .on('mousemove', function () { // mouse moving over canvas var mouse = d3.mouse(this); d3.selectAll(".mouse-per-line") .attr("transform", function (d, i) { var xDate = x.invert(mouse[0]), bisect = d3.bisector(function (d) { return d.date; }).left; idx = bisect(d.values, xDate); d3.select(this).select('text') .text(y.invert(y(d.values[idx].temperature)).toFixed(2)); d3.select(".mouse-line") .attr("d", function () { var data = "M" + x(d.values[idx].date) + "," + height; data += " " + x(d.values[idx].date) + "," + 0; return data; }); return "translate(" + x(d.values[idx].date) + "," + y(d.values[idx].temperature) + ")"; }); }); </script> </body> </html> 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM