react-pdf icon indicating copy to clipboard operation
react-pdf copied to clipboard

Bug with React PDF and Highcharts SVG data Labels

Open aimeetacchi opened this issue 3 years ago • 1 comments

Hi, I am using Highcharts to get an SVG of my chart with getSVG() method and I've got a bunch of logic I found online which creates me a SVG that React-PDF accepts. I have it rendering all good React-PDF, only issue I'm having is, I want to resize the dataLabels. I can resize the Pie Chart with width and height fine, heres some code below.


let chartSVG = chart.getSVG({
        chart: {
            width: 400,
            height: 162,
        }, ..... 

This code is doing the work to let React-PDF accept the Highcharts SVG

import { useMemo, createElement } from 'react';
import { parse, TextNode, ElementNode } from 'svg-parser';
import { styles } from './results-pdf.styles';
import { View } from '@react-pdf/renderer';

const supportedStyleProps = [
    'color',
    'dominantBaseline',
    'fill',
    'fillOpacity',
    'fillRule',
    'opacity',
    'stroke',
    'strokeWidth',
    'strokeOpacity',
    'strokeLinecap',
    'strokeDasharray',
    'transform',
    'textAnchor',
    'visibility',
];

const isElementNode = (node: TextNode | ElementNode): boolean => node.type === 'element';

const removeLineBreaks = (text?: string | number | boolean) => {
    if (typeof text === 'string') {
        return text.replace(/(\r\n|\n|\r)/gm, '');
    }

    return text;
};

// https://dev.to/qausim/convert-html-inline-styles-to-a-style-object-for-react-components-2cbi
const formatStringToCamelCase = (str: string) => {
    const splitted = str.split('-');
    if (splitted.length === 1) return splitted[0];
    return (
        splitted[0] +
        splitted
            .slice(1)
            .map(word => word[0].toUpperCase() + word.slice(1))
            .join('')
    );
};

const getStyleObjectFromString = (str: string | null) => {
    const style: any = {};
    if (!str) return {};

    str.split(';').forEach(el => {
        let [property, value] = el.split(':');
        if (!property) return;
        if (property === 'cursor') return;

        // calling formatStringToCamelCase function ====
        const formattedProperty = formatStringToCamelCase(property.trim());

        if (supportedStyleProps.includes(formattedProperty)) {
            if (formattedProperty === 'strokeDasharray') {
                console.log('value', value);
                value = value.replace(/pt/g, ''); //dasharray has now px
            }
            style[formattedProperty] = value.trim();
        }
    });

    return style;
};

const handleRelativePositioning = (node: ElementNode, parentX?: number, parentY?: number) => {
    return {
        x: Number(node.properties?.x ?? parentX ?? 0) + Number(node.properties?.dx ?? 0),
        y: Number(node.properties?.y ?? parentY ?? 0) + Number(node.properties?.dy ?? 0),
    };
};

const getParentPosition = (pos: number | string | undefined) => {
    if (!pos) return 0;
    if (typeof pos === 'string') return Number(pos);
    return pos;
};

const svgToJSXWithRelPositioning = (
    node: TextNode | ElementNode | string | any,
    key?: string,
    parentX?: number,
    parentY?: number
): any => {
    if (typeof node === 'string') {
        return removeLineBreaks(node);
    }
    if (!isElementNode(node)) {
        return removeLineBreaks(node.value);
    }
    const elementName = node.tagName;
    if (!elementName) {
        // console.log('NO TAG NAME: ', node);
        return null;
    }

    let componentProps;

    if (node.tagName === 'desc' || node.tagName === 'defs') return null;

    if (node.properties !== undefined) {
        if (node.tagName === 'text' || node.tagName === 'tspan' || node.tagName === 'rect') {
            // calling handleRelativePositioning function ===
            componentProps = handleRelativePositioning(node, parentX, parentY);

            if (node.tagName !== 'rect') {
                componentProps = {
                    ...componentProps,
                    textAnchor: node.properties['text-anchor'],
                };
            } else {
                componentProps = {
                    ...node.properties,
                    ...componentProps,
                };
            }
        } else {
            componentProps = node.properties;
        }
        // console.log(node, componentProps);

        if (node.properties.style) {
            console.log(node.properties.style);
            componentProps = {
                ...componentProps,
                // Calling getStyleObjectFromString function ----
                style: getStyleObjectFromString(node.properties.style as string),
            };
        }
    }
    let children = [];
    if (node.children && node.children.length > 0) {
        children = node.children.map((childNode: TextNode | ElementNode | string, i: number) =>
            svgToJSXWithRelPositioning(
                childNode,
                key + '-' + i,
                // calling getParentPosition function ====
                getParentPosition(node.properties.x),
                getParentPosition(node.properties.y)
            )
        );
    } else {
        children = [''];
    }
    componentProps = { ...componentProps, key: key ?? 'root' };

    return createElement(elementName.toUpperCase(), componentProps, children);
};

export const SvgComponent = ({ svgXml }: { svgXml: string }) => {
    const MyView: any = View;
    const svgElement = useMemo(() => {
        if (!svgXml || svgXml === '') return <></>;
        // const svg = svgXml.replace(/px/g, 'pt'); //replace all px with pt
        const parsed: TextNode | ElementNode | string | any = parse(svgXml);

        // calling svgToJSXWithRelPositioning function ===
        return svgToJSXWithRelPositioning(parsed.children[0]);
    }, [svgXml]);

    return <MyView style={styles.resultsChart}>{svgElement}</MyView>;
};

the SvgComponent above is called in the my PDF component I've built <SvgComponent svgXml={chartSVG} />

Where I am getting the chartSVG and passes it to SvgComponent as a prop, which it receives as svgXML as seen above, it does all the functions up above it and gives me a SVG I can use in my PDF component, all works fine. just when I go to resize the dataLabels as they are too big, they will not Resize in highcharts you can style the datalabels with

plotOptions: {
           variablepie: {
               borderWidth: 3,
               dataLabels: {
                   style: {
                       fontSize: '5px',
                       textOutline: 'none',
                       fontFamily: 'PhoenixSansBoldWeb',
                   },
               },
           },
           series: {
               dataLabels: {
                   // connectorWidth: 0,
                   style: {
                       fontSize: '5px',
                       textOutline: 'none',
                       fontFamily: 'PhoenixSansBoldWeb',
                   },
               },
           },
       },

But React PDF is ignoring it... so question is how can I resize these data labels is there something wrong with the logic in the functions above where it's creating an SVG for React PDF to accept?

I was using the logic from here - https://gist.github.com/dennemark/5f0f3d7452d9334f9349172db6c40f74

if someone knows the answer to this that would be great. Thanks

aimeetacchi avatar Sep 09 '22 10:09 aimeetacchi

Maybe related to https://github.com/diegomura/react-pdf/issues/1271

ghost avatar Sep 16 '22 05:09 ghost