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>