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

时间:2017-02-07 01:14:17

标签: javascript d3.js svg charts

我是D3的新手,只是将下面的工具提示合并到我的应用程序中。我既有单线图也有多线图。

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

多行:Multiseries line chart with mouseover tooltip

如您所见,两个工具提示的功能不同。单线工具提示从每个数据点跳转,而多线连续跟随图表路径。我想更改多行功能以模仿单行工具提示的工作方式。

非常感谢任何帮助。如果我需要提供更多信息,请告诉我。另请注意,我正在使用的数据是一个数组数组

以下是我的代码:

单线图:

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

多线图:

    //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 +")";
        });
    });

1 个答案:

答案 0 :(得分:4)

我将Mark的答案作为您提供的Multiseries line chart with mouseover tooltip的参考。

基本上,您需要做的是将工具提示设置为在x轴数据的每个刻度上显示,因此不要使用鼠标[0]抓住鼠标的位置并移动工具提示,而应将其移动到x轴数据的位置。

以下是我所做的更改的详细信息:

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

以下是应用了更改的完整工作代码。对于此代码段,我使用interpolate('linear')正确显示了值;如果您使用interpolate('basis'),工具提示和行将无法正确匹配:

&#13;
&#13;
<!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 a g 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>
&#13;
&#13;
&#13;

相关问题