D3: Zoom / pan graph in SVG does not work in Canvas

I created a zooming/panning with d3 using SVG . I am trying to create the exact same graph with Canvas . My problem is, when it comes to scaling and panning the Canvas graph, the graph disappears, and I can't figure out why. I created two JSBin to show the code of both. Can someone help me.

SVG - JSBin

Canvas - JSBin

My SVG scaling code is as follows:

 // 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 } 

My Canvas scaling code is as follows:

 // 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(); } 
+5
source share
2 answers

There is one main source of grief that makes your line disappear, and it only works when scaling:

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

Re-scaling will not work on x2 because you have not defined its domain. x2 is a reference scale that is used to set x for each scaling, it should start with x . However, the default domain for d3.timeScale() is from January 1, 2000 to January 2, 2000 (see the API docs ) t for your data, since your data does not overlap this period.

You need to install the x2 domain as well as x . If you do this after you set the start domain for x with: x2.domain(x.domain()) , you should get a diagram that is updated ( jsbin ), since now you have a domain that overlaps your data.

However, now the problem is that you need to pin your line, which is what you are doing in your svg example, but not in the canvas. For this you can use something like:

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

See jsbin

And how should we not allow the axes to overwrite ourselves: Here is jsbin that erases the previous axes (with comments, a code that redefines the y domain based on what values ​​are contained in the selected x domain).

For a good measure, here is a snippet of the last jsbin (reduced to view the snippet):

 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> 
+5
source

You need to clear the canvas every time you draw, adding only inside the paint function,

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

but other -

 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 }]; } 
+2
source

Source: https://habr.com/ru/post/1271163/


All Articles