Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to measure SVG elements in React?

Tags:

reactjs

svg

To render certain SVG elements in my app, I need to first measure some other SVG elements.

For example, imagine a <text> element that is randomly positioned in a square (0,0) - (100,100), and can have various font sizes, font families, etc.

If the text is positioned at (10,20), and have a width of 30 and a height of 40, I'd like to set the containing <svg> width to 40 (= 10 + 30) and height to 60 (= 20 + 40).

The main point is: <text> needs to be measured before rendering the <svg>.

To help with <text> measurement, I created the following component:

class MeasureSvgElements extends React.Component {
  storeSvgReference = svg => {
    if (svg !== null) {
      this.svg = svg;
    }
  };

  measure() {
    const childElements = Array.from(this.svg.children);
    const dimensions = childElements
      .map(element => element.getBoundingClientRect())
      .map(({ width, height }) => ({ width, height }));

    this.props.onChange(dimensions);
  }

  componentDidMount() {
    this.measure();
  }

  componentDidUpdate() {
    this.measure();
  }

  render() {
    return (
      <svg width="0" height="0" style={{ display: 'block' }} ref={this.storeSvgReference}>
        {this.props.children}
      </svg>
    );
  }
}

which can be used to measure multiple elements at once:

<MeasureSvgElements onChange={onChange}>
  {['Hello', 'Stack', 'Overflow'].map(str => <text>{str}</text>)}
</MeasureSvgElements>

onChange will be called once the dimensions are ready.

Now, I'm not sure what's the best way to use <MeasureSvgElements> to render the containing <svg> using the dimensions provided by onChange.

Or, is there a better approach?

like image 962
Misha Moroshko Avatar asked May 09 '17 21:05

Misha Moroshko


People also ask

How do you determine the size of an element in React?

To get the width of an Element in React: Set the ref prop on the element. In the useLayoutEffect hook, update the state variable for the width. Use the offsetWidth property to get the width of the element.

How do I change the size of SVG in React?

You can change the size using CSS transform: scale(2) in <ComponentName /> , which in React can be achieved using className or a global CSS file. Note: To change the color you can use . componentClass path { fill: "color" } , but if you change the scale on .

How do I show SVG icons in React?

To do this, open up the SVG file in a text editor, and copy-paste the code into a new component: export const ArrowUndo = () => { return ( <svg xmlns="http://www.w3.org/2000/svg" className="ionicon" viewBox="0 0 512 512" > <path d="M245.


1 Answers

One solution might be to mock-up each text element prior to render, gather the heights and widths (in addition to positions), and then iterate through the dimensions to determine what sizes should be applied to the parent.

Working CodeSandbox: https://codesandbox.io/s/stack-43880276-dynamic-svg-textbox-id3rt

Data Structure:

import React, { useState, useEffect } from "react";
import styled from "styled-components";

const yourTextData = [
  {
    id: 0,
    posX: 0,
    posY: 0,
    str: "~~Hello~~",
    fontFamily: "URW Chancery L, cursive",
    fontSize: "30"
  },
  {
    id: 1,
    posX: 20,
    posY: 20,
    str: "~~Stack~~",
    fontFamily: "URW Chancery L, cursive",
    fontSize: "30"
  },
  {
    id: 2,
    posX: 40,
    posY: 40,
    str: "~~Overflow~~",
    fontFamily: "URW Chancery L, cursive",
    fontSize: "30"
  },
  {
    id: 3,
    posX: 0,
    posY: 80,
    str: "This SVG with text sets its own",
    fontFamily: "Arial, sans-serif",
    fontSize: "30"
  },
  {
    id: 4,
    posX: 40,
    posY: 120,
    str: "d i m e n s i o n s",
    fontFamily: "FreeMono, monospace",
    fontSize: "30"
  }
];

Define your component and hooks:

const DynamicSVGText = (props) => {
  const [svgWidth, setSvgWidth] = useState(0);
  const [svgHeight, setSvgHeight] = useState(0);
  const [textDims, setTextDims] = useState([]); // array of objects, defining dims/pos for each texteach text

Measure function

This will be called when creating each text element and saved to the textDims hook.

  const measure = (data) => {
    // create a mock element
    let newText = document.createElement("text");
    newText.setAttribute("id", data.id);
    document.body.appendChild(newText);

    // append text data
    let theTextEle = document.getElementById(`${data.id}`);
    theTextEle.innerHTML += data.str;

    // append text font / size, bc might as well be fancy
    theTextEle.style.fontFamily = data.fontFamily;
    theTextEle.style.fontSize = `${data.fontSize}px`;

    // measure element
    let width = theTextEle.getBoundingClientRect().width;
    let height = theTextEle.getBoundingClientRect().height;

    // delete element
    theTextEle.parentNode.removeChild(theTextEle);

    // set data
    let dimData = [width, height];
    //console.log(dimData);

    // return dimension data
    return dimData;
  };

Function for creating text elements:

  const SvgText = ({ text }) =>
    text.map((data, i) => {
      let dimensions = measure(data);

      let updatedTextDims = textDims;

      updatedTextDims[data.id] = {
        x: data.posX,
        y: data.posY,
        w: dimensions[0],
        h: dimensions[1]
      };

      return (
        <StyledText
          fontFamily={data.fontFamily}
          fontSize={data.fontSize}
          key={data.id}
          x={data.posX.toString()}
          y={(data.posY + dimensions[1]).toString()}
        >
          {data.str}
        </StyledText>
      );
    });

useEffect to detect when the textDims hook changes

  useEffect(() => {
    let tw = 0; // longest width
    let th = 0; // tallest height

    // loop through text elements and their dimensions,
    // set total width/height to the greatest dimensions across objects
    for (let d = 0; d < textDims.length; d++) {
      let thisWidth = textDims[d].x + textDims[d].w;
      let thisHeight = textDims[d].y + textDims[d].h;

      if (thisWidth > tw) {
        tw = thisWidth;
      }

      if (thisHeight > th) {
        th = thisHeight;
      }
    }

    setSvgWidth(tw);
    setSvgHeight(th);
  }, [textDims]);

Finalize the component, and a bit of style

  return (
    <svg
      width={svgWidth.toString()}
      height={svgHeight.toString()}
      style={{ display: "block" }}
    >
      <SvgText text={props.text} />
    </svg>
  );
};

export default function App() {
  return (
    <Container>
      <DynamicSVGText text={yourTextData} />
    </Container>
  );
}

const Container = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;
`;

const StyledText = styled.text``;
like image 126
Harley Lang Avatar answered Oct 02 '22 19:10

Harley Lang