1

I have been tasked to implement zooming in custom charts based on SVGs. Before i had one <svg> element. I looked into either using the transform or the viewbox approach and decided for the viewbox approach. Since i have to support zooming on just the x-axis, the charts contents will be squashed depending on the zoomFactor and need preserveAspectRatio="none". This does not look pretty for the chart labels that i used foreignObjects for. They get disorted as well and are not readable anymore. I did not find any solutions on how to apply the viewbox to just the actual chart contents, not the scales / labels.

I came up with the solution to split the chart into 3 nested svgs. The structure looks like this:

<svg> // Container SVG
   <svg>...</svg> // XAxis
   <svg>...</svg> // YAxis
   <svg>...</svg> // ChartContent
</svg>

The viewbox will only be applied to the ChartContent svg and the svgs with the actual scales stay untouched and are just simply rerendered if needed with different labels at different positions. The desired outcome is similar to this example: https://jsfiddle.net/19h83ker/1/

Given that i have a chart to display that is as an example 4000 pixels wide and 200 pixels high, the y-axis is 40 pixels wide and 200 pixels high, the x-axis is 40 pixels high and 4000 pixels wide, how should i generally setup the viewports? If i set width="100%" and height="100%" on the ContainerSVG and ChartContent SVG, i have no scrollbar available. If i set width="4040" on the ContainerSVG and width="4000" on the ChartContent SVG, i have a scrollbar but applying the viewbox while zooming out by 100% will simply halve my svg in width and the right 50% are left blank. I dont really understand what the combination of widths / heights is in my structure, that i should be going for. Or am i making a mistake in general? Are there better ways to implement the desired outcome? I have already spent 2 days on this and dont really see any other option than these 3 nested svgs.At the end of the day panning / zooming in the 4000 pixel wide example SVG chart should be possible.

Mav3N
  • 41
  • 2
  • Is the jsfiddle example yours? What are you using to process the zoom by the user (D3 / Vue / JS ...)? – MSC Jul 23 '21 at 00:29
  • @MSC: No, the jsfiddle is not mine, i found it on google. I am processing the zoom by my own. My own code is calculating the zoomFactor and creating the viewbox based on the calculated zoomFactor. I am using svg.js, not d3. – Mav3N Jul 23 '21 at 08:21

1 Answers1

0

The suggestions you mentioned involve setting width and height, then using the default browser behavior for scroll and zoom. Instead, you need to intercept the appropriate events and modify the viewBox attribute.

The code below demonstrates zooming on mouse wheel events. Panning should be simpler (only the first and second parts of the viewBox need to change) but the implementation depends on what UI controls you want to use.

svg = document.getElementById("s")
var vb = [0,0,300,200]
svg.setAttribute("viewBox",vb) // vb gets converted to the string "0,0,300,40"

function zoom(wheelEvent){
  let k=1.005**wheelEvent.deltaY
  let ctm = svg.children[0].getScreenCTM()//If the svg is empty, this won't work
  // position of the mouse in svg coords
  let mx = (wheelEvent.clientX-ctm.e)/ctm.a
  let my = (wheelEvent.clientY-ctm.f)/ctm.d
  // To center the zoom on the mouse, we need:
  // (mx - initialX)/initialWidth == (mx - finalX)/finalWidth
  // finalX = mx - (mx - initalX)*(finalWidth/initialWidth)
  vb[0] = mx-(mx-vb[0])*k
  vb[1] = my-(my-vb[1])*k
  vb[2] *= k
  vb[3] *= k
  svg.setAttribute("viewBox",vb)
  wheelEvent.preventDefault() // prevent the page from scrolling
}

svg.addEventListener("wheel",zoom)
html,body,svg{
  width: 100%;
  height: 100%;
  margin: 0px;
  padding: 0px;
}
#s{
  width: 100%;
  height: 100%;
  background-color: red;
}
<svg id="outer" viewBox="0 0 200 100" preserveAspectRatio="xMinYMin">
<rect x="0" y="0" width="20" height="200" />
<rect x="0" y="0" width="100%" height="20" />
<svg id="s" x="20" y="20" width="180" height="180" viewBox="0,0,300,200">
<rect x="-1000" y="-1000" width="2000" height="2000" fill="lightgray"/>
<text font-size="60" x="10" y="60" fill="blue"> hello </text>
</svg>
</svg>

If you only want scaling of the x-axis, you'll need a different value for the 'preserveAspectRatio' attribute (as well as removing the code which changes vb[1] and vb[3]).

Tesseract
  • 526
  • 3
  • 20