Accepted answer

The necessary changes are few:

  1. Instead of .attr('patternUnits', 'userSpaceOnUse'), do .attr('patternContentUnits', 'objectBoundingBox').
  2. Set both width and height of both the <pattern> and <image> to 1 (or 100%).

Here is the resulting demo (hover over the circle):

const mainSVG ='svg');

const defs = mainSVG.append('defs')
  .attr('id', 'foo')
  .attr('width', 1)
  .attr('height', 1)
  .attr('patternContentUnits', 'objectBoundingBox')
  .attr('xlink:href', '')
  .attr('preserveAspectRatio', 'none')
  .attr('width', 1)
  .attr('height', 1);

const circle = mainSVG.append('circle')
  .attr('r', 30)
  .attr('cx', 150)
  .attr('cy', 75)
  .attr('stroke', 'gray')
  .attr('stroke-width', '2')
  .attr('fill', 'url(#foo)')
  .on('mouseover', function() {
      .attr('r', 70);
  .on('mouseout', function() {
      .attr('r', 30);
<script src=""></script>

You can read more about it in this excellent answer (not a duplicate, though).

Related Query