2

I am trying to create an array of patterns to use for my Chart.js doughnut chart so I do this:

var surfacePatterns
var surfaceNames = ['Unknown', 'Paved', 'Unpaved', 'Concrete', 'Cobblestone', 'Metal', 'Wood', 'Ground', 'Sand', 'Grass']

$(document).ready(() => {    
    surfacePatterns = []
    console.log(surfacePatterns)
    for (i = 0; i < surfaceNames.length; i++) {
        var temp = new Image()
        temp.onload = () => surfacePatterns.push($("#canvas1")[0].getContext('2d').createPattern(temp, 'repeat'))
        temp.src = '../img/surfaces/'+ surfaceNames[i] + '.jpg'
    }
});

function chart(){
   console.log(surfacePatterns)
}

The chart() function is called by a button press. I deliberately wait for a few seconds so it can load the images and create the patterns but when I call the function it gives me this:

console

Out of the nine images, only 8 and 9 got converted into a pattern and the rest is null. I have no idea what's going on because I get no errors and all images are found. If I change a String in surfaceNames I, as expected, get an error that an image couldn't be found and if I use patterns 8 and 9 they work perfectly fine.

Another weird thing: If I change

temp.onload = () => surfacePatterns.push($("#canvas1")[0].getContext('2d').createPattern(temp, 'repeat'))

to

temp.onload = () => surfacePatterns[i] = canvas1[0].getContext('2d').createPattern(temp, 'repeat')

The entire array is entirely empty (=/= null). Also, the first log after initializing the array as an empty array gives me an empty array with a length of 9 and I have no idea why since nothing else has happened yet.

leonheess
  • 16,068
  • 14
  • 77
  • 112

2 Answers2

1

You need to wait for all of the patterns to load before you can log the array. In fact, the array should be the result of awaiting all the patterns, rather than a variable in the outer scope that you populate asynchronously. See Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference

As for how to solve this, you can use Promise.all() after converting each callback into a pending promise:

const surfaceNames = ['stackoverflow.com', 'codegolf.stackexchange.com', 'superuser.com', 'codereview.stackexchange.com'];
const context = document.querySelector('#canvas1').getContext('2d');
const promises = surfaceNames.map(surfaceName =>
  new Promise((resolve, reject) => {
    const image = new Image();

    image.addEventListener('load', () => {
      resolve(context.createPattern(image, 'repeat'));
    });
    image.addEventListener('error', () => {
      reject(new Error(`${surfaceName} failed to load ${image.src}`));
    });
    image.src = `https://${surfaceName}/favicon.ico`;
  }).catch(error => {
    // populating array with null entry and logging error
    // instead of ignoring all the other pending promises
    console.log(error.message);
    return null;
  })
);

Promise.all(promises).then(surfacePatterns => {
  console.log(surfacePatterns.map(pattern => pattern.constructor.name));
});
<canvas id="canvas1"></canvas>

As you can see, each of these successfully resolves with a CanvasPattern.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • That is such a smart way to do this! I also learned about the \`String${variable}String\`-thing which is awesome as well. Thanks! – leonheess Feb 02 '19 at 00:39
  • 1
    @MiXT4PE that syntax is called a [template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) available since ECMAScript 2015 – Patrick Roberts Feb 02 '19 at 01:21
0

When calling an async function several times in this case inside a "for", you have to wait for all the calls. If you don't wait they you lost some async calls and you got an empty array.

If you can put a snipped I'll try to fix that problem.

window.surfacePatterns = [];
window.c = 0;
//window.surfaceNames = ['Unknown', 'Paved', 'Unpaved', 'Concrete', 'Cobblestone', 'Metal', 'Wood', 'Ground', 'Sand', 'Grass'];
window.surfaceNames = ["https://www.worldatlas.com/r/w1200-h630-c1200x630/upload/37/99/85/northern-pygmy-owl.jpg",
  "https://www.friendsofwehr.org/wp-content/uploads/2013/06/Great-horned_Owl_RWD_at_CRC1transparent.jpg"
];

$(document).ready(() => {
  console.log('Step 1: ', window.surfacePatterns);
  for (i = 0; i < window.surfaceNames.length; i++) {
    var temp = new Image();
    temp.onload = function() {
      var my = $(".canvas1")[0].getContext('2d').createPattern(temp, 'repeat');
      console.log('Step inside async: ', my);
      window.surfacePatterns.push(my);
      window.c++;
      if (window.surfaceNames.length === c) {
        console.log('Complete async calls: ', window.surfacePatterns);
      }
    }
    //temp.src = '../img/surfaces/'+ window.surfaceNames[i] + '.jpg';
    temp.src = "https://www.friendsofwehr.org/wp-content/uploads/2013/06/Great-horned_Owl_RWD_at_CRC1transparent.jpg"

  }
  console.log('Outside For: ', window.surfacePatterns);
});

function chart() {
  console.log(window.surfacePatterns);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas class="canvas1"></canvas>
<canvas class="canvas1"></canvas>
leonheess
  • 16,068
  • 14
  • 77
  • 112
Κωλζαρ
  • 803
  • 1
  • 10
  • 22
  • As said above even if I wait for a minute or so before I call chart it is the same result. Or do you mean if I start the next async-call it will drop the one before? – leonheess Jan 27 '19 at 12:25
  • The for run faster than the async call (method onload). Which framework do you use? Have you ever use the promise? – Κωλζαρ Jan 27 '19 at 18:48
  • No framework. I don't use Promises - how would they be used in this scenario? – leonheess Jan 28 '19 at 20:23