svgsvg-transforms

Switch X and Y axis for everything in SVG?


I'm creating a bar chart with SVG, by default bars are vertical. But in some cases it looks better if bars are horizontal, like when there are only 2 bars.

enter image description here

How can I reuse same SVG code and just switch X and Y axis to achieve that? It's actually more than just X and Y and also things like height and width for the rect.

Is that possible? I would like to avoid writing very similar code twice.

Example: I built two charts separately, ideally the second chart should be produced by reusing the code from the first chart.

<style>
  svg { height: 20px; width: 100px;  border: 1px solid #ccc;}
</style>

<svg>
  <rect x="5%" y="60%" width="40%" height="40%" fill="black"/>
  <rect x="55%" y="40%" width="40%" height="60%" fill="black"/>
</svg>

<svg>
  <rect y="5%" x="0%" height="40%" width="40%" fill="black"/>
  <rect y="55%" x="0%" height="40%" width="60%" fill="black"/>
</svg>


Solution

  • Maybe something like this:

    function cloneWithTransformedAttributes(obj1, mapping) {
      // returns copy of obj1 with child node attributes transformed according to mapping.
      const obj2 = obj1.cloneNode(true);
      [...obj1.children].forEach((child, idx)=>{
        Object.keys(mapping).forEach((attribute) => {
          const replacementVal = mapping[attribute].default ?
                mapping[attribute].default :
                child.getAttribute(mapping[attribute]);
            obj2.children[idx].setAttribute(attribute, replacementVal);
        })
      })
      return obj2;
    }
    
    mapping = {
    x: {
      default: 0
    },
    y: "x",
    width: "height",
    height: "width"
    }
    
    const verticalSvg = document.getElementsByTagName("svg")[0];
    const horizontalSvg = cloneWithTransformedAttributes(verticalSvg, mapping);
    
    const graphs = document.getElementById("graphs");
    graphs.appendChild(horizontalSvg);
    <style>
      svg { height: 20px; width: 100px;  border: 1px solid #ccc;}
    </style>
    
    <div id="graphs">
      <svg>
        <rect x="5%" y="60%" width="40%" height="40%" fill="black"/>
        <rect x="55%" y="40%" width="40%" height="60%" fill="black"/>
      </svg>
    </div>

    Edit: You could also do it with svg transforms but you need to put the inside of the SVG inside a group <g></g>. Here's an example with both a manually calculated transform and a JS solution that calculates the transform by itself by getting the original svg width and height:

    const verticalSvg = document.getElementsByTagName("svg")[0];
    const svgStyle = window.getComputedStyle(verticalSvg, null);
    
    const width = parseInt(svgStyle.getPropertyValue("width"));
    const height = parseInt(svgStyle.getPropertyValue("height"));
    const whRatio = width / height;
    const transform = `rotate(90) scale(${1 / whRatio} ${whRatio}) translate(0 -${height})`
    
    
    const horizontalSvg = verticalSvg.cloneNode(true);
    horizontalSvg.children[0].setAttribute("transform", transform);
    
    const graphs = document.getElementById("graphs");
    graphs.appendChild(horizontalSvg);
    <style>
      svg { height: 20px; width: 100px;  border: 1px solid #ccc; overflow: visible}
    </style>
    
    <div id="graphs">
      <svg>
        <g>
        <rect x="5%" y="60%" width="40%" height="40%" fill="black"/>
        <rect x="55%" y="40%" width="40%" height="60%" fill="black"/>
        </g>
      </svg>
      <svg>
        <!-- Manually calculated and applied transform    -->
        <g transform="rotate(90)
                    scale(0.20 5)
                      translate(0 -20)
                      ">
        <rect x="5%" y="60%" width="40%" height="40%" fill="black"/>
        <rect x="55%" y="40%" width="40%" height="60%" fill="black"/>
        </g>
      </svg>
    <!-- JS generated SVG will get inserted here   -->
    </div>

    In your case, you need to apply the transform to a group holding the SVG contents. If you apply the transform to the SVG itself it will also scale the border according to the scale transform.

    vector-effect="non-scaling-stroke" can not be applied to the border of the SVG which is not part of the SVG itself. But if you are using elements with a "stroke" property inside the SVG then you would probably want to also apply the vector-effect="non-scaling-stroke" attribute to them.