Search code examples
svgpath

pathfit to scale to bound


I am creating svg path and scaling it to a specific dimension. Let's suppose 100x100. I am using d3 to shape the data as per my req.

However, I was wondering, how can I achieve the exact same output as pathStr with pathfit. This is what I tried below.

function degreeToRadian(d) {
    return (d * Math.PI) / 180;
};

function fnData() {
    const data = d3.range(0, 11, 1).map((d) => {
        const angleRadian = degreeToRadian(d);
        return {
            index: d,
            angleDeg: d,
            angleRadian: angleRadian,
            x: Math.cos(angleRadian),
            y: Math.sin(angleRadian)
        }
    });

    return data;
};

const width = 1280;
const height = 720;
const data = fnData();

//------------------------WITH D3------------------------//
const scaleX = d3
    .scaleLinear()
    .range([0, 100])
    .domain(d3.extent(data, (d) => d.x));

const scaleY = d3
    .scaleLinear()
    .range([100, 0])
    .domain(d3.extent(data, (d) => d.y));

const pathStr = d3
    .line()
    .x((d) => scaleX(d.x))
    .y((d) => scaleY(d.y))(data);
  
const pathStr2 = d3.line().x(d=>d.x).y(d=>d.y)(data);  

//------------------------WITH pathfit------------------------//

const base = {
    viewBox: `0 0 ${width} ${height}`,
    preserveAspectRatio: "xMidYMid meet" // the default
};
const pathFitter = new Pathfit(base, undefined, pathStr2).scale_with_aspect_ratio(100, 100);
console.log({d3:pathStr, pathfit:pathFitter});
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pathfit@1.0.4/pathfit.js"></script>


Solution

  • If you want to use the extent of your data as the box that should be expanded into the 100*100 dimensions, you have to set these data in the viewBox. d3 can provide you with the coordinates of the four sides:

    const [x1, x2] = d3.extent(data, (d) => d.x);
    const [y1, y2] = d3.extent(data, (d) => d.y);
    
    viewBox: `${x1} ${y1} ${x2 - x1} ${y2 - y1}`
    

    If you want to not preserve the aspect ratio, but stretch the data to the maximum dimensions in both directions, you have to set

    preserveAspectRatio: "none"
    

    If you want to flip the data upside-down, you have to provide a pretransform value. This is certainly a bit more awkward as with d3. Since SVG syntax is used, you can not specify a center of transformation directly, but need to find the correct formula for the transform matrix yourself. Fortunately, an upside-down flip that leaves the bounds of of your data exactly in place is relatively easy:

    `matrix(1 0 0 -1 0 ${y1 + y2})`
    

    Unfortunately, the result will still look different from what d3 delivers, because the non-scaled path string from d3 fed to pathfit has a too small number of digits after the decimal separator. This leads to rounding errors that appear huge in the end result. You will need to increase them from the default:

    const pathStr2 = d3.line().digits(6).x(d=>d.x).y(d=>d.y)(data);
    

    function degreeToRadian(d) {
        return (d * Math.PI) / 180;
    };
    
    function fnData() {
        const data = d3.range(0, 11, 1).map((d) => {
            const angleRadian = degreeToRadian(d);
            return {
                index: d,
                angleDeg: d,
                angleRadian: angleRadian,
                x: Math.cos(angleRadian),
                y: Math.sin(angleRadian)
            }
        });
    
        return data;
    };
    
    const width = 1280;
    const height = 720;
    const data = fnData();
    
    //------------------------WITH D3------------------------//
    const scaleX = d3
        .scaleLinear()
        .range([0, 100])
        .domain(d3.extent(data, (d) => d.x));
    
    const scaleY = d3
        .scaleLinear()
        .range([100, 0])
        .domain(d3.extent(data, (d) => d.y));
    
    const pathStr = d3
        .line()
        .x((d) => scaleX(d.x))
        .y((d) => scaleY(d.y))(data);
      
    const pathStr2 = d3.line().digits(5).x(d=>d.x).y(d=>d.y)(data);  
    
    //------------------------WITH pathfit------------------------//
    
    const [x1, x2] = d3.extent(data, (d) => d.x);
    const [y1, y2] = d3.extent(data, (d) => d.y);
    
    const base = {
        viewBox: `${x1} ${y1} ${x2 - x1} ${y2 - y1}`,
        preserveAspectRatio: "none" // do stretch the result
    };
    const pathFitter = new Pathfit(
       base,
       undefined,
       pathStr2,
       `matrix(1 0 0 -1 0 ${y1 + y2})`,
       { precision: 4 }
    ).scale_with_aspect_ratio(100, 100);
    
    console.log({d3:pathStr, pathfit:pathFitter});
    <script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/pathfit@1.0.4/pathfit.js"></script>