Html canvas element resets width and height to zero after drag/drop
I'm working on customisable dashboard where (amongst other features) users can drag dashboard tiles (div
elements) around and reposition those tiles anywhere in the dashboard.
HTML Structure
The html structure is similar to the snippet below
<div class="dashboard">
<div class="tile"><canvas/></div>
<div class="tile"><canvas/></div>
<div class="tile empty"></div>
</div>
Expected Behavior
The idea is that the .dashboard
can contain multiple .tiles
and each .tile
contains a report (a graph/chart drawn on a canvas
element). Some of these .tiles
can be .empty
, as in, not containing any report. Then .tile
can be dragged and dropped into the .empty
tiles.
So, div.tile.empty
serves as "dropzone" while div.tile
will be draggable elements. A fiddler snippet has been added below for a simplistic-but-fully-functional example.
Libraries used
- jQuery
- ChartJs. An open source js library to draw charts on a canvas
The problem
It all seems to work well, except that after dropping a .tile
the canvas resets its width/height to zero!!! And I haven't found a way to reset it back to its original dimensions before the drag/drop events. Even if I set the width/height manually, nothing is drawn on the canvas.
Question
Is there any way I can preserve (or recover) the width/height of the canvas while also getting it to re-drawn the graph after drag/dropping?
I tried using chartjs
's update
, render
and resize
functions to no avail.
The documentation of these functions can be found in the link below (version 3.5.0)...
https://www.chartjs.org/docs/3.5.0/developers/api.html
Code Example
Here's a sample code snippet where you can reproduce the issue mentioned above. The buttons are my attempt to update/resize/re-render the graphs after drag/dropping.
var $sourceTile = null;
var charts = [];
$(() => {
$(".buttons-container a").on("click", btnClickHandler);
renderChart("canvas1", 'doughnut');
renderChart("canvas2", "line");
attachDropHandlers();
});
attachDropHandlers = () => {
$(".tile").off("dragstart").off("dragover").off("drop");
$(".tile .report").on("dragstart", dragStartHandler);
$(".tile.empty").on("dragover", dragOverHandler);
$(".tile.empty").on("drop", dropHandler);
}
dragStartHandler = (e) => {
const $target = $(e.target);
const $report = $target.is(".report") ? $target : $target.parents(".report");
$sourceTile = $report.parents(".tile");
e.originalEvent.dataTransfer.setData("application/dashboard", $report[0].id);
e.originalEvent.dataTransfer.effectAllowed = "move";
e.originalEvent.dataTransfer.dropEffect = "move";
}
dragOverHandler = (e) => {
e.preventDefault();
e.originalEvent.dataTransfer.dropEffect = "move";
}
dropHandler = (e) => {
e.preventDefault();
const id = e.originalEvent.dataTransfer.getData("application/dashboard");
if (id) {
$("#" + id).appendTo(".tile.empty");
$(".tile.empty").removeClass("empty");
if ($sourceTile) {
$sourceTile.addClass("empty");
attachDropHandlers();
}
}
}
renderChart = (canvasId, type) => {
const labels = ["Red", "Green", "Blue"];
const data = [30, 25, 42];
const colors = ['rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)'];
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const chart = new Chart(ctx, {
type: type,
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors,
borderColor: colors,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 1,
plugins: {
legend: {
display: false
},
htmlLegend: {
tile: this.$tile,
maxItems: 8
}
}
}
});
chart.update();
charts.push(chart);
}
btnClickHandler = (e) => {
const button = e.target.id;
switch (button) {
case "btn1":
charts.forEach((chart) => chart.update());
break;
case "btn2":
charts.forEach((chart) => chart.update('resize'));
break;
case "btn3":
charts.forEach((chart) => chart.render());
break;
case "btn4":
charts.forEach((chart) => chart.resize());
break;
case "btn5":
charts.forEach((chart) => chart.resize(120, 120));
break;
}
}
html,
body {
background-color: #eee;
}
h3 {
margin: 0;
padding: 10px;
}
.dashboard {}
.dashboard .tile {
display: inline-block;
vertical-align: top;
margin: 5px;
height: 250px;
width: 250px;
}
.tile.empty {
border: 2px dashed #ccc;
}
.report {
height: 250px;
width: 250px;
background-color: #fff;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
.buttons-container {
display: flex;
justify-content: space-between;
margin: 20px 0;
}
.buttons-container a {
background-color: #673AB7;
color: #EDE7F6;
cursor: pointer;
padding: 10px 15px;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
.buttons-container a:hover {
background-color: #7E57C2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
<div class="dashboard">
<div class="tile">
<div id="report1" class="report" draggable="true">
<h3>
Report 1
</h3>
<div style="padding:10px;height:180px;width:180px">
<canvas id="canvas1"></canvas>
</div>
</div>
</div>
<div class="tile">
<div id="report2" class="report" draggable="true">
<h3>
Report 2
</h3>
<div style="padding:10px;height:180px;width:180px">
<canvas id="canvas2"></canvas>
</div>
</div>
</div>
<div class="tile empty">
</div>
</div>
<div class="buttons-container">
<a id="btn1">update()</a>
<a id="btn2">update('resize')</a>
<a id="btn3">render()</a>
<a id="btn4">resize()</a>
<a id="btn5">resize(120,120)</a>
</div>
Solution 1:
This is a Chart.js issue of version 3.6.0 and fixed in version 3.6.1. Example below:
var $sourceTile = null;
var charts = [];
$(() => {
$(".buttons-container a").on("click", btnClickHandler);
renderChart("canvas1", 'doughnut');
renderChart("canvas2", "line");
attachDropHandlers();
});
attachDropHandlers = () => {
$(".tile").off("dragstart").off("dragover").off("drop");
$(".tile .report").on("dragstart", dragStartHandler);
$(".tile.empty").on("dragover", dragOverHandler);
$(".tile.empty").on("drop", dropHandler);
}
dragStartHandler = (e) => {
const $target = $(e.target);
const $report = $target.is(".report") ? $target : $target.parents(".report");
$sourceTile = $report.parents(".tile");
e.originalEvent.dataTransfer.setData("application/dashboard", $report[0].id);
e.originalEvent.dataTransfer.effectAllowed = "move";
e.originalEvent.dataTransfer.dropEffect = "move";
}
dragOverHandler = (e) => {
e.preventDefault();
e.originalEvent.dataTransfer.dropEffect = "move";
}
dropHandler = (e) => {
e.preventDefault();
const id = e.originalEvent.dataTransfer.getData("application/dashboard");
if (id) {
$("#" + id).appendTo(".tile.empty");
$(".tile.empty").removeClass("empty");
if ($sourceTile) {
$sourceTile.addClass("empty");
attachDropHandlers();
}
}
}
renderChart = (canvasId, type) => {
const labels = ["Red", "Green", "Blue"];
const data = [30, 25, 42];
const colors = ['rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)'];
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const chart = new Chart(ctx, {
type: type,
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors,
borderColor: colors,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 1,
plugins: {
legend: {
display: false
},
htmlLegend: {
tile: this.$tile,
maxItems: 8
}
}
}
});
chart.update();
charts.push(chart);
}
btnClickHandler = (e) => {
const button = e.target.id;
switch (button) {
case "btn1":
charts.forEach((chart) => chart.update());
break;
case "btn2":
charts.forEach((chart) => chart.update('resize'));
break;
case "btn3":
charts.forEach((chart) => chart.render());
break;
case "btn4":
charts.forEach((chart) => chart.resize());
break;
case "btn5":
charts.forEach((chart) => chart.resize(120, 120));
break;
}
}
html,
body {
background-color: #eee;
}
h3 {
margin: 0;
padding: 10px;
}
.dashboard {}
.dashboard .tile {
display: inline-block;
vertical-align: top;
margin: 5px;
height: 250px;
width: 250px;
}
.tile.empty {
border: 2px dashed #ccc;
}
.report {
height: 250px;
width: 250px;
background-color: #fff;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
.buttons-container {
display: flex;
justify-content: space-between;
margin: 20px 0;
}
.buttons-container a {
background-color: #673AB7;
color: #EDE7F6;
cursor: pointer;
padding: 10px 15px;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
.buttons-container a:hover {
background-color: #7E57C2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
<div class="dashboard">
<div class="tile">
<div id="report1" class="report" draggable="true">
<h3>
Report 1
</h3>
<div style="padding:10px;height:180px;width:180px">
<canvas id="canvas1"></canvas>
</div>
</div>
</div>
<div class="tile">
<div id="report2" class="report" draggable="true">
<h3>
Report 2
</h3>
<div style="padding:10px;height:180px;width:180px">
<canvas id="canvas2"></canvas>
</div>
</div>
</div>
<div class="tile empty">
</div>
</div>
<div class="buttons-container">
<a id="btn1">update()</a>
<a id="btn2">update('resize')</a>
<a id="btn3">render()</a>
<a id="btn4">resize()</a>
<a id="btn5">resize(120,120)</a>
</div>