Добавление всплывающих подсказок к свече при помощи d3. js - PullRequest
1 голос
/ 29 мая 2020

Я использую d3. js для масштабируемого свечного графика. Он работает нормально, но мне нужно написать подсказку для отображения значений OHL C. Я сделал ссылку, чтобы попробовать, но мне это не удалось. Может кто-нибудь помочь мне отладить? Большое спасибо!

Вот мой код, он очень длинный, но для части всплывающих подсказок он находится в средней части.

function drawChart() {

    d3.csv("amazon.csv").then(function(prices) {

        const months = {0 : 'Jan', 1 : 'Feb', 2 : 'Mar', 3 : 'Apr', 4 : 'May', 5 : 'Jun', 6 : 'Jul', 7 : 'Aug', 8 : 'Sep', 9 : 'Oct', 10 : 'Nov', 11 : 'Dec'}

        var dateFormat = d3.timeParse("%Y-%m-%d");
        for (var i = 0; i < prices.length; i++) {

            prices[i]['Date'] = dateFormat(prices[i]['Date'])
        }

        const margin = {top: 15, right: 65, bottom: 205, left: 50},
            w = 1000 - margin.left - margin.right,
            h = 625 - margin.top - margin.bottom;

        var svg = d3.select("#container")
            .attr("width", w + margin.left + margin.right)
            .attr("height", h + margin.top + margin.bottom)
            .append("g")
            .attr("transform", "translate(" +margin.left+ "," +margin.top+ ")");

        let dates = _.map(prices, 'Date');

        var xmin = d3.min(prices.map(r => r.Date.getTime()));
        var xmax = d3.max(prices.map(r => r.Date.getTime()));
        var xScale = d3.scaleLinear().domain([-1, dates.length])
            .range([0, w])
        var xDateScale = d3.scaleQuantize().domain([0, dates.length]).range(dates)
        let xBand = d3.scaleBand().domain(d3.range(-1, dates.length)).range([0, w]).padding(0.3)
        var xAxis = d3.axisBottom()
            .scale(xScale)
            .tickFormat(function(d) {
                d = dates[d]
                // hours = d.getHours()
                // minutes = (d.getMinutes()<10?'0':'') + d.getMinutes()
                // amPM = hours < 13 ? 'am' : 'pm'
                // return hours + ':' + minutes + amPM + ' ' + d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
                return d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
            });

        svg.append("rect")
            .attr("id","rect")
            .attr("width", w)
            .attr("height", h)
            .style("fill", "none")
            .style("pointer-events", "all")
            .attr("clip-path", "url(#clip)")

        var gX = svg.append("g")
            .attr("class", "axis x-axis") //Assign "axis" class
            .attr("transform", "translate(0," + h + ")")
            .call(xAxis)

        gX.selectAll(".tick text")
            .call(wrap, xBand.bandwidth())

        var ymin = d3.min(prices.map(r => r.Low));
        var ymax = d3.max(prices.map(r => r.High));
        var yScale = d3.scaleLinear().domain([ymin, ymax]).range([h, 0]).nice();
        var yAxis = d3.axisLeft()
            .scale(yScale)

        var gY = svg.append("g")
            .attr("class", "axis y-axis")
            .call(yAxis);

        var chartBody = svg.append("g")
            .attr("class", "chartBody")
            .attr("clip-path", "url(#clip)");

        // draw rectangles
        let candles = chartBody.selectAll(".candle")
            .data(prices)
            .enter()
            .append("rect")
            .attr('x', (d, i) => xScale(i) - xBand.bandwidth())
            .attr("class", "candle")
            .attr('y', d => yScale(Math.max(d.Open, d.Close)))
            .attr('width', xBand.bandwidth())
            .attr('height', d => (d.Open === d.Close) ? 1 : yScale(Math.min(d.Open, d.Close))-yScale(Math.max(d.Open, d.Close)))
            .attr("fill", d => (d.Open === d.Close) ? "silver" : (d.Open > d.Close) ? "red" : "green")

        // draw high and low
        let stems = chartBody.selectAll("g.line")
            .data(prices)
            .enter()
            .append("line")
            .attr("class", "stem")
            .attr("x1", (d, i) => xScale(i) - xBand.bandwidth()/2)
            .attr("x2", (d, i) => xScale(i) - xBand.bandwidth()/2)
            .attr("y1", d => yScale(d.High))
            .attr("y2", d => yScale(d.Low))
            .attr("stroke", d => (d.Open === d.Close) ? "white" : (d.Open > d.Close) ? "red" : "green");

        // -1- Create a tooltip div that is hidden by default:
        let tooltip = d3.select("#container")
            .append("div")
            .style("opacity", 0)
            .attr("class", "tooltip")
            .style("background-color", "black")
            .style("border-radius", "5px")
            .style("padding", "10px")
            .style("color", "white");

        // -2- Create 3 functions to show / update (when mouse move but stay on same circle) / hide the tooltip
        let showTooltip = function(d) {
            tooltip
              .transition()
              .duration(200);
            tooltip
              .style("opacity", 1)
                .html("Open: " + d.Open)
                .html("High: " + d.High)
                .html("Low: " + d.Low)
                .html("Close: " + d.Close)
              .style("left", (d3.mouse(this)[0]+30) + "px")
              .style("top", (d3.mouse(this)[1]+30) + "px")
        };
        let moveTooltip = function() {
            tooltip
              .style("left", (d3.mouse(this)[0]+30) + "px")
              .style("top", (d3.mouse(this)[1]+30) + "px")
        };
        let hideTooltip = function() {
              tooltip
                  .transition()
                  .duration(200)
                  .style("opacity", 0)
        };

        svg.append("defs")
            .append("clipPath")
            .attr("id", "clip")
            .append("rect")
            .attr("width", w)
            .attr("height", h)
            .on("mouseover", showTooltip )// -3- Trigger the functions
            .on("mousemove", moveTooltip )
            .on("mouseleave", hideTooltip);

        const extent = [[0, 0], [w, h]];

        var resizeTimer;
        var zoom = d3.zoom()
            .scaleExtent([1, 100])
            .translateExtent(extent)
            .extent(extent)
            .on("zoom", zoomed)
            .on('zoom.end', zoomend);

        svg.call(zoom);

        function zoomed() {

            var t = d3.event.transform;
            let xScaleZ = t.rescaleX(xScale);

            let hideTicksWithoutLabel = function() {
                d3.selectAll('.xAxis .tick text').each(function(d){
                    if(this.innerHTML === '') {
                        this.parentNode.style.display = 'none'
                    }
                })
            };

            gX.call(
                d3.axisBottom(xScaleZ).tickFormat((d, e, target) => {
                    if (d >= 0 && d <= dates.length-1) {
                        d = dates[d];
                        // hours = d.getHours()
                        // minutes = (d.getMinutes()<10?'0':'') + d.getMinutes()
                        // amPM = hours < 13 ? 'am' : 'pm'
                        // return hours + ':' + minutes + amPM + ' ' + d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
                        return d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
                    }
                })
            );

            candles.attr("x", (d, i) => xScaleZ(i) - (xBand.bandwidth()*t.k)/2)
                .attr("width", xBand.bandwidth()*t.k);
            stems.attr("x1", (d, i) => xScaleZ(i) - xBand.bandwidth()/2 + xBand.bandwidth()*0.5);
            stems.attr("x2", (d, i) => xScaleZ(i) - xBand.bandwidth()/2 + xBand.bandwidth()*0.5);

            hideTicksWithoutLabel();

            gX.selectAll(".tick text")
                .call(wrap, xBand.bandwidth())

        }

        function zoomend() {
            var t = d3.event.transform;
            let xScaleZ = t.rescaleX(xScale);
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function() {

                var xmin = new Date(xDateScale(Math.floor(xScaleZ.domain()[0])));
                xmax = new Date(xDateScale(Math.floor(xScaleZ.domain()[1])));
                filtered = _.filter(prices, d => ((d.Date >= xmin) && (d.Date <= xmax)));
                minP = +d3.min(filtered, d => d.Low);
                maxP = +d3.max(filtered, d => d.High);
                buffer = Math.floor((maxP - minP) * 0.1);

                yScale.domain([minP - buffer, maxP + buffer]);
                candles.transition()
                    .duration(800)
                    .attr("y", (d) => yScale(Math.max(d.Open, d.Close)))
                    .attr("height",  d => (d.Open === d.Close) ? 1 : yScale(Math.min(d.Open, d.Close))-yScale(Math.max(d.Open, d.Close)));

                stems.transition().duration(800)
                    .attr("y1", (d) => yScale(d.High))
                    .attr("y2", (d) => yScale(d.Low));

                gY.transition().duration(800).call(d3.axisLeft().scale(yScale));

            }, 500)

        }
    });
}

function wrap(text, width) {
    text.each(function() {
        var text = d3.select(this),
            words = text.text().split(/\s+/).reverse(),
            word,
            line = [],
            lineNumber = 0,
            lineHeight = 1.1, // ems
            y = text.attr("y"),
            dy = parseFloat(text.attr("dy")),
            tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
        while (word = words.pop()) {
            line.push(word);
            tspan.text(line.join(" "));
            if (tspan.node().getComputedTextLength() > width) {
                line.pop();
                tspan.text(line.join(" "));
                line = [word];
                tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
            }
        }
    });
}

drawChart();
...