Doing a .then after an await FileAttachment.

I’m still learning JS and Observable currently confused why this this bit of code I wrote doesn’t resize the svg once it’s loaded.

<div id="pic0"> ${await FileAttachment('composition-15.svg').text()
  .then ( () => {
    d3.select('#pic0 > svg')
      .attr("width", 400)
      .attr("height", 300)
  })
 }
</div>

I also tried to have another cell with this code

{    
      d3.select('#pic1  > svg')
        .attr("width", 400)
        .attr("height", 300);
}

But it only worked if the cell was manually run after the notebook had loaded.

Any help welcome or thought on doing this a better way.

Would have also liked to add an ID to the SVG but I don;t see a way to do the using the FileAttachment.

A nasty way to resize them is by doing this:

The cell with the div containing the svg images is called fakeVis and so I have another cell that replaces the innerHTML with a copy that has had a string replace all the text of the svg width and heights.

fakeVis.innerHTML = fakeVis.innerHTML.replaceAll('width="3871px" height="2367px"','width="387px" height="236px"')

1 Like

Maybe:

<div id="pic0"> ${await FileAttachment('composition-15.svg').text()
  .then( t => html`${t}`)
  .then( h => d3.select(h).select('#pic0 > svg')
      .attr("width", 400)
      .attr("height", 300)
  )
 }
</div>
1 Like

The await waits for, and returns the value of, the last Promise in the chain. If we change your code a bit, it should become clearer where the problems originate:

const mySvgPromise = FileAttachment('composition-15.svg').text()
  // The next promise in the chain receives the value returned by the
  // previous promise. In this case, the text content of the file.
  .then(text => {
    // What are you selecting? How would your svg magically appear?
    d3.select('#pic0 > svg').attr("width", 400).attr("height", 300);
    // You're not returning anything. The final value of
    // mySvgPromise ends up being undefined.
    return;
  });

return html`<div id="pic0"> ${await mySvgPromise}</div>`;

You’re already using d3, so adding the ID is just another call to .attr():

myCell = FileAttachment('composition-15.svg').text()
  .then(text => d3.select(html`${text}`)
    .attr("id", "pic0")
    .attr("width", 400)
    .attr("height", 300)
   // Return the <svg> DOM element.
    .node()
  )

Attention: If your SVG file starts with a comment (as is the case for files created with Inkscape), then html`${text}` will return a <span> element. In this case use the following code:

d3.select(html`${text}`.children[0])
  .remove()
  .attr("id", "pic0")
  // etc ...

.children[0] gives us the actual <svg> element, while .remove() detaches it from its parent <span> element. The latter is important, because otherwise Observable will display the element as myCell = ▸ SVGSVGElement {}.


Note that you can also render the SVG as an image, either directly:

myCell = FileAttachment('composition-15.svg').image()

or via its URL:

myCell = html`<img
  width="400"
  height="300"
  src="${await FileAttachment("composition-15.svg").url()}"
>`
4 Likes

I would try something along the following lines:

pic = {
  let div = d3
    .create('div')
    .html(await FileAttachment("temp.svg").text())
    .style('height', '100px');
  div
    .select('svg')
    .attr('width', w)
    .attr('height', 100);
  return div.node();
}

Note that the width is set by the variable w; here it is in action with w specified by a slider:

Two comments:

  • d3.selections of dom elements by ID or class are generally discouraged in Observable. The preferred technique (as used here) is to select from within a named d3 object. See, for example, this notebook by Tom Macwright.
  • As much as I like the template literals, I tend to create objects and manipulate theme directly. This leads to more portable code
2 Likes

Thanks all for the help, I liked mcmcclur solution but went with the simple img suggestion from mootari.

Using the html img seem to make a raster image and I need it SVG as I need to access the colour palette, I’m lucky you posted so many solutions. :slight_smile:

I guess looking at a problem from one angle hides other possible solutions.
Really appreciate the help, thanks.

update2: in the end I did it like this:

dis = {
  d3.shuffle(imagefiles)
  let ht = html`<div id='group' style="display:grid;grid-template-columns: ${d3.range(5).map(s=>`auto`).join(' ')};"></div>`
  
  for(let i=0;i<15;i++){
    const text = await imagefiles[i].text();
    const document = (new DOMParser).parseFromString(text, "image/svg+xml");
    const svg = d3.select(document.documentElement).remove();
      
    d3.select(ht)
      .append("div")
        .append('svg')
          .attr('width', sw)
          .attr('height', sh)
          .attr("id", `pic_${i}`)
          .attr('viewBox',"0 0 1000 1000")
        .append('g')
        .html(svg.html())
 }
  return ht
}
1 Like