D3:在Canvas中缩放/平移SVG折线图无法工作

5
我使用SVG和d3创建了一个缩放/平移图表。我正在尝试使用Canvas创建完全相同的图表。我的问题是,在对Canvas图表进行缩放和平移时,图表会消失,我无法找出原因。我创建了两个JSBin来显示两者的代码。有人可以帮助我吗?

SVG - JSBin

Canvas - JSBin

我的SVG缩放代码如下:

// Zoom Components
zoom = d3.zoom()
        .scaleExtent([1, dayDiff*12])
        .translateExtent([[0, 0], [width, height]])
        .extent([[0, 0], [width, height]])
        .on("zoom", zoomed);

function zoomed(){
    t = d3.event.transform;
    xScale.domain(t.rescaleX(x2).domain());
    xAxis = d3.axisBottom(xScale).tickSize(0).tickFormat(d3.timeFormat('%b'));
    focus.select(".axis--x").call(xAxis); //xAxis changes
    usageLinePath.attr('d',line); //line path reference, regenerate
}

我的 Canvas 缩放代码如下:

// Zoom Components
zoom = d3.zoom()
        .scaleExtent([1, dayDiff*12])
        .translateExtent([[0, 0], [width, height]])
        .extent([[0, 0], [width, height]])
        .on("zoom", zoomed);

function zoomed() {
    t = d3.event.transform;
    x.domain(t.rescaleX(x2).domain());
    context.save();
    context.clearRect(0, 0, width, height);
    draw();
    context.restore();
}

function draw() {
    xAxis();
    yAxis();

    context.beginPath();
    line(data);
    context.lineWidth = 1.5;
    context.strokeStyle = "steelblue";
    context.stroke();
}

你是否缺少x2域? x2.domain(x.domain()); 如果您可以在每次缩放时清除线条、x轴和y轴,它将正常工作。 - Shane G
2个回答

5

有一个主要的悲伤来源会导致您的线条消失,它只会在缩放时触发:

function zoomed() {
    t = d3.event.transform;
    x.domain(t.rescaleX(x2).domain());  // here
    ...
}

重新缩放无法作用于 x2,因为你没有定义它的域。 x2 是你的参考比例尺,用于在每次缩放时设置 x,它应该与 x 相同以开始。然而,d3.timeScale() 的默认域是从2000年1月1日到2000年1月2日(请参见API文档),这对于你的数据不起作用,因为你的数据不重叠于此时间段。
你需要设置 x2x 的域。如果你在使用 x.domain() 设置初始域之后这样做:x2.domain(x.domain()),你应该会得到一个更新的图表(jsbin),因为现在你有了一个与你的数据重叠的域。
然而,现在的问题是你需要裁剪你的线条,在 svg 示例中你已经这样做了,但是在画布上没有。为了这样做,你可以使用类似以下的内容:
function draw() {
    xAxis();
    yAxis();

  // save context without clip apth
  context.save();

  // create a clip path:
  context.beginPath()
  context.rect(0, 0, width, height);
  context.clip();

  // draw line in clip path
  context.beginPath()
  line(data);

  context.lineWidth = 1.5;
  context.strokeStyle = "steelblue";
  context.stroke();

  // restore context without clip path
  context.restore();
}

看这个jsbin

由于我们不应该让轴互相覆盖:这是一个jsbin,它会删除先前的轴(其中有一段被注释掉的代码块,根据所选x域中包含的值重新定义y域)。

为了保险起见,这是最后一个jsbin的代码片段(为了片段视图而缩小):

var data = getData().map(function (d) {
        return d;
    });

    var canvas = document.querySelector("canvas"),
        context = canvas.getContext("2d");

    var margin = { top: 20, right: 20, bottom: 30, left: 50 },
        width = canvas.width - margin.left - margin.right,
        height = canvas.height - margin.top - margin.bottom;

    var parseTime = d3.timeParse("%d-%b-%y");

    // setup scales
    var x = d3.scaleTime()
        .range([0, width]);
    var x2 = d3.scaleTime().range([0, width]);
    var y = d3.scaleLinear()
        .range([height, 0]);

    // setup domain
    x.domain(d3.extent(data, function (d) { return moment(d.Ind, 'YYYYMM'); }));
    y.domain(d3.extent(data, function (d) { return d.KSum; }));
    
    x2.domain(x.domain());
 


    // get day range
    var dayDiff = daydiff(x.domain()[0],x.domain()[1]);

    // line generator
    var line = d3.line()
        .x(function (d) { return x(moment(d.Ind, 'YYYYMM')); })
        .y(function (d) { return y(d.KSum); })
        .curve(d3.curveMonotoneX)
        .context(context);

    // zoom
    var zoom = d3.zoom()
        .scaleExtent([1, dayDiff])
        .translateExtent([[0, 0], [width, height]])
        .extent([[0, 0], [width, height]])
        .on("zoom", zoomed);
    
    d3.select("canvas").call(zoom)

    context.translate(margin.left, margin.top);

    draw();
//

    function draw() {
      // remove everything:
      context.clearRect(-margin.left, -margin.top, canvas.width, canvas.height);
      
      /*
      // Calculate the y axis domain across the selected x domain:
      newYDomain = d3.extent(data, function(d) {
         if (  (x(moment(d.Ind, 'YYYYMM')) > 0) && (x(moment(d.Ind, 'YYYYMM')) < width) ) {
           return d.KSum;           
         }
      });
      // Don't update the y axis if there are no points to set a new domain, just keep the old domain.
      if ((newYDomain[0] !== undefined) && (newYDomain[0] != newYDomain[1])) {
        y.domain(newYDomain);        
      }
     //*/

      // draw axes:
      xAxis();
      yAxis();
      
      // save context without clip apth
      context.save();
      
      // create a clip path:
      context.beginPath()
      context.rect(0, 0, width, height);
      context.clip();

      // draw line in clip path
      context.beginPath()
      line(data);
      
      context.lineWidth = 1.5;
      context.strokeStyle = "steelblue";
      context.stroke();
      
      // restore context without clip path
      context.restore();

 
    }

    function zoomed() {
        t = d3.event.transform;
        x.domain(t.rescaleX(x2).domain());
       
      
        draw();
    }

    function xAxis() {
        var tickCount = 10,
            tickSize = 6,
            ticks = x.ticks(tickCount),
            tickFormat = x.tickFormat();

        context.beginPath();
        ticks.forEach(function (d) {
            context.moveTo(x(d), height);
            context.lineTo(x(d), height + tickSize);
        });
        context.strokeStyle = "black";
        context.stroke();

        context.textAlign = "center";
        context.textBaseline = "top";
        ticks.forEach(function (d) {
            context.fillText(tickFormat(d), x(d), height + tickSize);
        });
    }

    function yAxis() {
        var tickCount = 10,
            tickSize = 6,
            tickPadding = 3,
            ticks = y.ticks(tickCount),
            tickFormat = y.tickFormat(tickCount);

        context.beginPath();
        ticks.forEach(function (d) {
            context.moveTo(0, y(d));
            context.lineTo(-6, y(d));
        });
        context.strokeStyle = "black";
        context.stroke();

        context.beginPath();
        context.moveTo(-tickSize, 0);
        context.lineTo(0.5, 0);
        context.lineTo(0.5, height);
        context.lineTo(-tickSize, height);
        context.strokeStyle = "black";
        context.stroke();

        context.textAlign = "right";
        context.textBaseline = "middle";
        ticks.forEach(function (d) {
            context.fillText(tickFormat(d), -tickSize - tickPadding, y(d));
        });

        context.save();
        context.rotate(-Math.PI / 2);
        context.textAlign = "right";
        context.textBaseline = "top";
        context.font = "bold 10px sans-serif";
        context.fillText("Price (US$)", -10, 10);
        context.restore();
    }

    function getDate(d) {
        return new Date(d.Ind);
    }

    function daydiff(first, second) {
        return Math.round((second - first) / (1000 * 60 * 60 * 24));
    }

    function getData() {
        return [
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201501,
                "TMin": 30.43,
                "TMax": 77.4,
                "KMin": 0.041,
                "KMax": 1.364,
                "KSum": 625.08
            },
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201502,
                "TMin": 35.3,
                "TMax": 81.34,
                "KMin": 0.036,
                "KMax": 1.401,
                "KSum": 542.57
            },
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201503,
                "TMin": 32.58,
                "TMax": 81.32,
                "KMin": 0.036,
                "KMax": 1.325,
                "KSum": 577.83
            },
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201504,
                "TMin": 54.54,
                "TMax": 86.55,
                "KMin": 0.036,
                "KMax": 1.587,
                "KSum": 814.62
            },
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201505,
                "TMin": 61.35,
                "TMax": 88.61,
                "KMin": 0.036,
                "KMax": 1.988,
                "KSum": 2429.56
            },
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201506,
                "TMin": 69.5,
                "TMax": 92.42,
                "KMin": 0.037,
                "KMax": 1.995,
                "KSum": 2484.93
            },
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201507,
                "TMin": 71.95,
                "TMax": 98.62,
                "KMin": 0.037,
                "KMax": 1.864,
                "KSum": 2062.05
            },
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201508,
                "TMin": 76.13,
                "TMax": 99.59,
                "KMin": 0.045,
                "KMax": 1.977,
                "KSum": 900.05
            },
            {
                "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                "Ind": 201509,
                "TMin": 70,
                "TMax": 91.8,
                "KMin": 0.034,
                "KMax": 1.458,
                "KSum": 401.39
            }];
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<canvas width="500" height="200"></canvas>


非常抱歉之前提供的第三个js bin链接,虽然它按照预期工作,但那不是最终代码(它丢失了注释和可选的y缩放,并颠倒了比例尺“x”和“x2”的角色)。幸运的是,我在回答时将其复制到了片段中,并已相应更新了jsbin。 - Andrew Reid
我正在尝试沿着同一条线路在内部添加一个区域路径,但该区域覆盖了整个图表。你能帮我吗?jsbin - Abdullah Rasheed
你需要为区域图定义上下文,并为每个特征启动一个新路径(这对于线条不是问题,但对于区域则成为一个问题)。我在这个bin中标记了这些更改#1-#3,我还删除了剪辑区域的样式。 - Andrew Reid
谢谢。你觉得你能帮我解决这个问题吗:https://dev59.com/FaTja4cB1Zd3GeqPJe1E - Abdullah Rasheed
我很可能需要再提供500个积分来解决我链接的另一个问题,因为我正在开发一款关键任务应用程序,急需帮助。 - Abdullah Rasheed
我已经看了一下,并正在考虑;但是,我对Ionic不是很熟悉,所以可能帮不上太多忙。 - Andrew Reid

2

每次绘制时,您需要通过在绘制函数内添加以下代码来清除画布:

context.clearRect(0-margin.left, 0, canvas.width, canvas.height);

而剩下的部分是,
var data = getData().map(function (d) {
            return d;
        });

        var canvas = document.querySelector("canvas"),
            context = canvas.getContext("2d");

        var margin = { top: 20, right: 20, bottom: 30, left: 50 },
            width = canvas.width - margin.left - margin.right,
            height = canvas.height - margin.top - margin.bottom;

        var parseTime = d3.timeParse("%d-%b-%y");

        // setup scales
        var x = d3.scaleTime()
            .range([0, width]);
        var x2 = d3.scaleTime().range([0, width]);
        var y = d3.scaleLinear()
            .range([height, 0]);

        // setup domain
        x.domain(d3.extent(data, function (d) { return moment(d.Ind, 'YYYYMM'); }));
        y.domain(d3.extent(data, function (d) { return d.KSum; }));
        x2.domain(x.domain());

        // get day range
        var dayDiff = daydiff(x.domain()[0],x.domain()[1]);

        // line generator
        var line = d3.line()
            .x(function (d) { return x(moment(d.Ind, 'YYYYMM')); })
            .y(function (d) { return y(d.KSum); })
            .curve(d3.curveMonotoneX)
            .context(context);

        // zoom
        var zoom = d3.zoom()
            .scaleExtent([1, dayDiff * 12])
            .translateExtent([[0, 0], [width, height]])
            .extent([[0, 0], [width, height]])
            .on("zoom", zoomed);

        d3.select("canvas").call(zoom)

        context.translate(margin.left, margin.top);

        draw();


        function draw() {
            context.clearRect(0-margin.left, 0, canvas.width, canvas.height);
            xAxis();
            yAxis();

            context.beginPath();
            line(data);
            context.lineWidth = 1.5;
            context.strokeStyle = "steelblue";
            context.stroke();
        }

        function zoomed() {
            console.log(d3.event);
            t = d3.event.transform;
            x.domain(t.rescaleX(x2).domain());
            context.save();
            context.clearRect(0, 0, width, height);
            // context.translate(d3.event.translate[0], d3.event.translate[1]);
            // context.scale(d3.event.scale, d3.event.scale);
            draw();
            context.restore();
        }

        function xAxis() {
            var tickCount = 10,
                tickSize = 6,
                ticks = x.ticks(tickCount),
                tickFormat = x.tickFormat();

            context.beginPath();
            ticks.forEach(function (d) {
                context.moveTo(x(d), height);
                context.lineTo(x(d), height + tickSize);
            });
            context.strokeStyle = "black";
            context.stroke();

            context.textAlign = "center";
            context.textBaseline = "top";
            ticks.forEach(function (d) {
                context.fillText(tickFormat(d), x(d), height + tickSize);
            });
        }

        function yAxis() {
            var tickCount = 10,
                tickSize = 6,
                tickPadding = 3,
                ticks = y.ticks(tickCount),
                tickFormat = y.tickFormat(tickCount);

            context.beginPath();
            ticks.forEach(function (d) {
                context.moveTo(0, y(d));
                context.lineTo(-6, y(d));
            });
            context.strokeStyle = "black";
            context.stroke();

            context.beginPath();
            context.moveTo(-tickSize, 0);
            context.lineTo(0.5, 0);
            context.lineTo(0.5, height);
            context.lineTo(-tickSize, height);
            context.strokeStyle = "black";
            context.stroke();

            context.textAlign = "right";
            context.textBaseline = "middle";
            ticks.forEach(function (d) {
                context.fillText(tickFormat(d), -tickSize - tickPadding, y(d));
            });

            context.save();
            context.rotate(-Math.PI / 2);
            context.textAlign = "right";
            context.textBaseline = "top";
            context.font = "bold 10px sans-serif";
            context.fillText("Price (US$)", -10, 10);
            context.restore();
        }

        function getDate(d) {
            return new Date(d.Ind);
        }

        function daydiff(first, second) {
            return Math.round((second - first) / (1000 * 60 * 60 * 24));
        }

        function getData() {
            return [
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201501,
                    "TMin": 30.43,
                    "TMax": 77.4,
                    "KMin": 0.041,
                    "KMax": 1.364,
                    "KSum": 625.08
                },
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201502,
                    "TMin": 35.3,
                    "TMax": 81.34,
                    "KMin": 0.036,
                    "KMax": 1.401,
                    "KSum": 542.57
                },
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201503,
                    "TMin": 32.58,
                    "TMax": 81.32,
                    "KMin": 0.036,
                    "KMax": 1.325,
                    "KSum": 577.83
                },
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201504,
                    "TMin": 54.54,
                    "TMax": 86.55,
                    "KMin": 0.036,
                    "KMax": 1.587,
                    "KSum": 814.62
                },
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201505,
                    "TMin": 61.35,
                    "TMax": 88.61,
                    "KMin": 0.036,
                    "KMax": 1.988,
                    "KSum": 2429.56
                },
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201506,
                    "TMin": 69.5,
                    "TMax": 92.42,
                    "KMin": 0.037,
                    "KMax": 1.995,
                    "KSum": 2484.93
                },
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201507,
                    "TMin": 71.95,
                    "TMax": 98.62,
                    "KMin": 0.037,
                    "KMax": 1.864,
                    "KSum": 2062.05
                },
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201508,
                    "TMin": 76.13,
                    "TMax": 99.59,
                    "KMin": 0.045,
                    "KMax": 1.977,
                    "KSum": 900.05
                },
                {
                    "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542",
                    "Ind": 201509,
                    "TMin": 70,
                    "TMax": 91.8,
                    "KMin": 0.034,
                    "KMax": 1.458,
                    "KSum": 401.39
                }];
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接