Добавить всплывающую подсказку на многострочном графике D3.js - PullRequest
0 голосов
/ 28 августа 2018

У меня есть многострочный график D3.js, основанный на этом http://bl.ocks.org/d3noob/e99a762017060ce81c76.

Мой график представляет собой накопленную сумму на ежедневной основе, но отметки по оси x ежеквартально. Я хочу добавить всплывающую подсказку в каждой строке на этом графике и на каждую дату, заданную данными (не Q). Я попробовал несколько предложений, как вы можете видеть в коде, из похожих вопросов SO, но ни один из них, похоже, не работает. Предпочтительной является вертикальная линия, показывающая круги со значениями наведения мыши на каждой строке. Мой код выглядит следующим образом:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>Statistics</title>
  <style>
        .row {
            display: -webkit-box;
            display: -webkit-flex;
            display: -ms-flexbox;
            display: flex;
            flex-wrap: wrap
        }
        .row>[class*=col-] {
            display: flex;
            flex-direction: column
        }
        .d3-tip:after,
        .mapael .zoomButton {
            position: absolute;
            text-align: center
        }
        path {
            stroke: #fff
        }
        path:hover {
            opacity: .9
        }
        rect:hover {
            fill: #00f
        }
        .axis {
            font: 10px sans-serif
        }
        .legend tr {
            border-bottom: 1px solid grey
        }
        .legend tr:first-child {
            border-top: 1px solid grey
        }
        .axis line,
        .axis path {
            fill: none;
            stroke: #000;
            shape-rendering: crispEdges
        }
        .d3-tip {
            line-height: 1;
            font-weight: 700;
            background: rgba(0, 0, 0, .8);
            color: #fff;
            border-radius: 2px
        }
        .d3-tip:after {
            box-sizing: border-box;
            display: inline;
            width: 100%;
            line-height: 1;
            color: rgba(0, 0, 0, .8);
            content: "\25BC"
        }
        .d3-tip.n:after {
            margin: -1px 0 0;
            top: 100%;
            left: 0
        }
        .chart .legend {
            fill: #000;
            font: 90% sans-serif;
            text-anchor: start;
            font-size: 60%
        }
        .chart text {
            fill: #fff;
            font: 10px sans-serif;
            text-anchor: end
        }
        .chart .label {
            fill: #000;
            font: 90% sans-serif;
            text-anchor: end
        }
        rect.background {
            fill: none;
            pointer-events: all
        }
        .chart-wrapper {
            max-width: 1100px;
            min-width: 304px;
            margin: 0 auto;
            background-color: #FAF7F7
        }
        .chart-wrapper .inner-wrapper {
            position: relative;
            padding-bottom: 20%;
            width: 100%;
            height: 80%
        }
        #info,
        .chart-wrapper .outer-box,
        .ol-popup,
        .ol-popup-closer,
        .toolTip {
            position: absolute
        }
        .chart-wrapper .outer-box {
            top: 0;
            bottom: 0;
            left: 0;
            right: 0;
            height: 100%
        }
        .chart-wrapper .inner-box {
            width: 100%;
            height: 100%
        }
        .chart-wrapper text {
            font-family: sans-serif;
            /*font-size: 90%*/
        }
        .chart-wrapper p {
            /*font-size: 100%;*/
            margin-top: 5px;
            margin-bottom: 40px
        }
        .chart-wrapper .axis line,
        .chart-wrapper .axis path {
            fill: none;
            stroke: #1F1F2E;
            stroke-opacity: .7;
            shape-rendering: crispEdges
        }
        .chart-wrapper .axis path {
            stroke-width: 2px
        }
        .chart-wrapper .line {
            fill: none;
            stroke: #4682b4;
            stroke-width: 5px
        }
        .chart-wrapper .legend {
            min-width: 100px;
            display: flex;
            justify-content: flex-start;
            flex-wrap: wrap;
            /*font-size: 100%*/
        }
        .chart-wrapper .legend>div {
            margin: 0 25px 10px 0;
            flex-grow: 0
        }
        .chart-wrapper .legend p {
            display: inline;
            /*font-size: 80%;*/
            font-family: sans-serif;
            font-weight: 600
        }
        .chart-wrapper .legend .series-marker {
            height: 1em;
            width: 1em;
            border-radius: 35%;
            background-color: #dc143c;
            display: inline-block;
            margin-right: 4px;
            margin-bottom: -.16rem
        }
        .toolTip,
        .x.axis path {
            display: none
        }
        .chart-wrapper .overlay {
            fill: none;
            pointer-events: all
        }
        .chart-wrapper .focus circle {
            fill: #dc143c;
            stroke: #dc143c;
            stroke-width: 2px;
            fill-opacity: 15%
        }

        .chart-wrapper .focus rect {
            fill: #add8e6;
            opacity: .4;
            border-radius: 2px
        }

        .chart-wrapper .focus.line {
            stroke: #4682b4;
            stroke-dasharray: 2, 5;
            stroke-width: 2;
            opacity: .5
        }
        .toolTip {
            width: auto;
            height: auto;
            background: #fff;
            border: 0;
            border-radius: 8px;
            box-shadow: -3px 3px 15px #888;
            color: #000;
            font: 12px sans-serif;
            text-align: center
        }
        .tooltip.in {
            opacity: 1;
            filter: alpha(opacity=100)
        }
        .tooltip.top .tooltip-arrow {
            border-top-color: #fff
        }
        .tooltip-inner {
            border: 2px solid #fff
        }
        .chart rect:first-of-type {
            color: #fff;
            stroke: #3994b6;
            fill: #fff
        }
        .chart rect:nth-of-type(2) {
            color: #fff;
            fill: #3994b6
        }
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.7/d3.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.6.3/d3-tip.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/reqwest/2.0.5/reqwest.min.js"></script>
  <script type="text/javascript">
    Number.prototype.formatMoney = function(c, d, t){
        var n = this, 
        c = isNaN(c = Math.abs(c)) ? 2 : c, 
        d = d == undefined ? "," : d, 
        t = t == undefined ? "." : t, 
        s = n < 0 ? "-" : "", 
        i = String(parseInt(n = Math.abs(Number(n) || 0).toFixed(c))), 
        j = (j = i.length) > 3 ? j % 3 : 0;
        return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
    };
    function responsivefy(svg) {
        // get container + svg aspect ratio
        var container = d3.select(svg.node().parentNode),
            width = parseInt(svg.style("width")),
            height = parseInt(svg.style("height")),
            aspect = width / height;
        // add viewBox and preserveAspectRatio properties,
        // and call resize so that svg resizes on inital page load
        svg.attr("viewBox", "0 0 " + width + " " + height)
            .attr("perserveAspectRatio", "xMinYMid")
            .call(resize);
        // to register multiple listeners for same event type, 
        // you need to add namespace, i.e., 'click.foo'
        // necessary if you call invoke this function for multiple svgs
        // api docs: https://github.com/mbostock/d3/wiki/Selections#on
        d3.select(window).on("resize." + container.attr("id"), resize);
        // get width of container and resize svg to fit it
        function resize() {
            var targetWidth = parseInt(container.style("width"));
            svg.attr("width", targetWidth);
            svg.attr("height", Math.round(targetWidth / aspect));
        }
    }
  </script>
</head>

<body id="page-top">
  <!-- Navigation-->
  <div class="content-wrapper">
    <div class="container-fluid">
      <div class="card mb-3">
        <div class="card-body">
            <div class="box" style="border: 5px #cbcbcb double">
                <div class="chart-wrapper col-12 d-block" id="chart-line1" style="height:50%;">
                <script type="text/javascript">
                    // Set the dimensions of the canvas / graph
                    var marginlines = {top: 40, right: 60, bottom: 70, left: 80},
                        widthlines = 1100 - marginlines.left - marginlines.right,
                        heightlines = 400 - marginlines.top - marginlines.bottom;
                    // Parse the date / time
                    var parseDate = d3.time.format("%Y-%m-%d").parse;
                    // Set the ranges
                    var xlines = d3.time.scale().range([0, widthlines]);
                    var ylines = d3.scale.linear().range([heightlines, 0]);
                    // Define the axes
                    var xAxislines = d3.svg.axis().scale(xlines)
                        .ticks( d3.time.months, 3 )
                        .tickFormat( function ( x ) {
                            // get the milliseconds since Epoch for the date
                            var milli = (x.getTime() - 10000);
                            // calculate new date 10 seconds earlier. Could be one second, 
                            // but I like a little buffer for my neuroses
                            var vanilli = new Date(milli);
                            // calculate the month (0-11) based on the new date
                            var mon = vanilli.getMonth();
                            var yr = vanilli.getFullYear();
                            // return appropriate quarter for that month
                            if ( mon <= 2 ) {return  "Q1 " + yr;
                            } else if ( mon <= 5 ) {return  "Q2 " + yr;
                            } else if ( mon <= 8 ) {return  "Q3 " + yr;
                            } else {return "Q4 " + yr;
                            }
                        })
                        .orient( "bottom" );
                    var yAxislines = d3.svg.axis().scale(ylines)
                        .orient("left").ticks(5);
                    // Define the line
                    var priceline = d3.svg.line()   
                        .x(function(d) { return xlines(d.date); })
                        .y(function(d) { return ylines(d.value); });
                    // Adds the svg canvas
                    var svglines = d3.select("#chart-line1")
                        .append("svg")
                            .attr("width", widthlines + marginlines.left + marginlines.right)
                            .attr("height", heightlines + marginlines.top + marginlines.bottom)
                            .call(responsivefy)
                        .append("g")
                            .attr("transform", "translate(" + marginlines.left + "," + marginlines.top + ")");
                    // Get the data
                    d3.json("http://88.99.13.199:3000/poreiapaa", function(error, data) {
                        data.forEach(function(d) {
                            d.date = parseDate(d.date);
                            d.value = +Number(d.value);
                        });
                        // Scale the range of the data
                        xlines.domain(d3.extent(data, function(d) { return d.date; }));
                        ylines.domain([0, d3.max(data, function(d) {return d.value; })]);
                        // Nest the entries by symbol
                        var dataNest = d3.nest()
                            .key(function(d) {return d.variable;})
                            .entries(data);
                        var colorlines = d3.scale.category10();   // set the colour scale
                        legendSpace = widthlines/dataNest.length; // spacing for legend
                        // Loop through each symbol / key
                        let tipBox;
                        dataNest.forEach(function(d,i) { 
                            svglines.append("path")
                                .attr("class", "line")
                                .style("stroke", function() { // Add the colours dynamically
                                    return d.color = colorlines(d.key); })
                                .attr("d", priceline(d.values));
                            // Add the Legend
                            svglines.append("text")
                                .attr("x", (legendSpace/2)+i*legendSpace) // spacing
                                .attr("y", heightlines + (marginlines.bottom/2)+ 5)
                                .attr("class", "legend")    // style the legend
                                .style("fill", function() { // dynamic colours
                                    return d.color = colorlines(d.key); })
                                .text('Test');
                        });
                        // Add the X Axis
                        svglines.append("g")
                            .attr("class", "x axis")
                            .attr("transform", "translate(0," + heightlines + ")")
                            .call(xAxislines);
                        // Add the Y Axis
                        svglines.append("g")
                            .attr("class", "y axis")
                            .call(yAxislines);
                        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(dataNest)
                            .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', widthlines) // can't catch mouse events on a g element
                            .attr('height', heightlines)
                            .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.select(".mouse-line")
                                    .attr("d", function() {
                                        var d = "M" + mouse[0] + "," + heightlines;
                                        d += " " + mouse[0] + "," + 0;
                                        return d;
                                    });
                                d3.selectAll(".mouse-per-line")
                                    .attr("transform", function(d, i) {
                                    console.log(width/mouse[0])
                                var xDate = x.invert(mouse[0]),
                                    bisect = d3.bisector(function(d) { return d.date; }).right;
                                    idx = bisect(d.values, xDate);
                                var beginning = 0,
                                    end = lines[i].getTotalLength(),
                                    target = null;
                                while (true){
                                  target = Math.floor((beginning + end) / 2);
                                  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(this).select('text')
                                  .text(y.invert(pos.y).toFixed(2));
                                return "translate(" + mouse[0] + "," + pos.y +")";
                            });
                        });
                    });
                </script>
            </div>
        </div>
      </div>
    </div>
  </div>
</body>
</html> 

Я был бы очень признателен за любую помощь.

...