【问题标题】:D3: Zooming/Panning Line Graph in SVG is not working in CanvasD3:SVG 中的缩放/平移线图在 Canvas 中不起作用
【发布时间】:2017-08-23 15:28:50
【问题描述】:

我使用SVG 用d3 创建了zooming/panning 图形。我正在尝试使用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 轴,则可以正常工作。

标签: javascript d3.js canvas svg


【解决方案1】:

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

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 docs),这不适用于您的数据,因为您的数据不与此时间段重叠。

您需要设置x2x 的域。如果您在将 x 的初始域设置为: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 的 sn-p(缩小为 sn-p 视图):

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>

【讨论】:

  • 我为最初的 3rd js bin 链接道歉,虽然它按预期工作,但它不是最终代码(它丢失了注释掉和可选的 y 缩放并颠倒了 scales 的角色xx2)。幸运的是,我在回答时将其复制到了 sn-p 中,并相应地更新了 jsbin。
  • 我正在尝试在内部沿同一条线添加区域路径,但该区域覆盖了整个图形。你可以帮帮我吗? jsbin
  • 您需要为面积图定义上下文,并为每个要素开始一条新路径(这对线来说不是问题,而是与面积合二为一)。我在 bin 中将这些更改标记为 #1-#3,我还删除了剪辑区域的样式。
  • 谢谢。你认为你能帮我解决这个问题吗:stackoverflow.com/questions/46062295/…
  • 我很可能必须为我链接的其他问题再提供 500 分,因为我正在开发一个关键任务应用程序,我急需帮助
【解决方案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
                }];
        }

【讨论】:

    猜你喜欢
    • 2015-01-23
    • 2013-10-05
    • 1970-01-01
    • 1970-01-01
    • 2015-07-08
    • 2013-01-17
    • 2016-07-07
    • 2017-01-29
    • 1970-01-01
    相关资源
    最近更新 更多