Victory-native: [ Reports ] How convert VictoryChart to image or HTML to create a PDF report page?

Created on 3 Jan 2017  路  19Comments  路  Source: FormidableLabs/victory-native

Can anyone suggest possible approaches to generate a PDF image from a VictoryChart in react native?

react-native-html-to-pdf is a cross-platform solution to convert HTML including images to a PDF using React Native.

Are there any methods to save a VictoryChart view to an image?

Thanks in advance,

Most helpful comment

Oh, I wasn't aware of RNSVGSvgViewManager.toDataURL!

VictoryChart has access to the SVG already, and it's technically accessible as chart.containerRef.svgRef.

        <VictoryChart
          ref={(chart) => {
            if (chart && chart.containerRef && chart.containerRef.svgRef) {
              setTimeout(() => {
                chart.containerRef.svgRef.toDataURL((base64) => {
                  console.log(base64);
                });
              }, 0);
            }
          }}
        >
          <VictoryLine/>
        </VictoryChart>

This seems to work, but has a couple of problems

  1. The svgRef is an internal implementation detail, and should not be accessed from outside as we do above.
  2. It doesn't seem to be safe to call toDataURL on the SVG ref directly in the chart's ref callback - thus the setTimeout hack above. Calling the method on the same call stack results in a native exception, at least on iOS. This won't be a problem if the chart is already mounted by the time the snapshot is taken, of course.

@esutton does this solution work for your use case? If it does, I think we can expose a method to grab the SVG reference.

All 19 comments

@esutton I wonder if you could use a generic solution like https://github.com/gre/react-native-view-shot - it currently supports jpg/png output.

Thanks @jevakallio

When user sends report, using the screenshot method I would need to:

  1. Temporarily pop-up a view matching the desired page width-to-height ratio
  2. Use react-native-view-shot to grab image
  3. Close view and render the HTML to PDF using the grabbed image

I fear the resulting user experience would not be good. I had hoped there were alternatives to react-native-view-shot.

Does not VictoryChart use D3 underneath?

Could something like this An example of creating a PNG from an SVG in D3. be possible?

Export VictoryChart to SVG, then render as image?

I expect this is not easy to accomplish in React Native. Especially for someone who is no D3 expert.

Or this perhaps?

How to convert/save d3.js graph to pdf/jpeg

@esutton we use d3 internally, but the example you posted also uses the browser canvas element to render the graphic into an image. This API is not available on React Native, so that mechanism won't work.

Internally, we also use react-native-svg, which could be extended to support saving the drawing into an image (in fact, the Android implementation already draws the SVG to a bitmap and renders that bitmap on-screen). That's outside the scope of Victory Native, though.

I wonder if you would be able to achieve this by rendering a temporary dummy view as you suggest, but rendering it off-screen using relative positioning so the user would not see the view? I have not tried this, just an idea.

@jevakallio Thanks for the ideas.

I think reporting / printing of charts is a very important feature to users.

It sounds like the options are to:

  1. Get react-native-view-shot working with Victory Native.
  2. Redraw my charts in react-native-svg not using Victory Native and use the new toDataURL method.
svgRef.toDataURL(base64 => {
    console.log(base64);
});

It looks like toDataURL method is supported by both Android and iOS. Is there a way to get the SVG reference of the Victory Native chart?

https://github.com/react-native-community/react-native-svg/search?utf8=%E2%9C%93&q=toDataURL

Oh, I wasn't aware of RNSVGSvgViewManager.toDataURL!

VictoryChart has access to the SVG already, and it's technically accessible as chart.containerRef.svgRef.

        <VictoryChart
          ref={(chart) => {
            if (chart && chart.containerRef && chart.containerRef.svgRef) {
              setTimeout(() => {
                chart.containerRef.svgRef.toDataURL((base64) => {
                  console.log(base64);
                });
              }, 0);
            }
          }}
        >
          <VictoryLine/>
        </VictoryChart>

This seems to work, but has a couple of problems

  1. The svgRef is an internal implementation detail, and should not be accessed from outside as we do above.
  2. It doesn't seem to be safe to call toDataURL on the SVG ref directly in the chart's ref callback - thus the setTimeout hack above. Calling the method on the same call stack results in a native exception, at least on iOS. This won't be a problem if the chart is already mounted by the time the snapshot is taken, of course.

@esutton does this solution work for your use case? If it does, I think we can expose a method to grab the SVG reference.

@jevakallio I think exposing a method to grab the SVG reference would be very helpful in printing and report generation.

@jevakallio Will you have time to expose method to grab SVG reference?

I tried using your example and the console log is dumping base64 data. To avoid crashes I had to increase the timeout from 0 to 500.

ExceptionsManager.js:71 Exception thrown while executing UI block: *** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[0]

I need to save to file using RNFS and see what the image result looks like and learn how to scale image to fill a full landscape page in a PDF. For rendering an image for PDF use, I hope I can understand how to render off-screen using relative positioning so the user would not see the view, as you mentioned.

Thanks in advance,

Can any react native experts share suggestions on how to invisibly render a VictoryChart for image generation purpose?

I have VictorChart writing image to a file using RNFS, then adding image to HTML content, then rendering HTML to PDF.

The hard part, is understanding how to create "dummy view" or a technique to render image offscreen ( with width set to match desired PDF page width ).

A second issue is I have not figured out how to make chart X:Y scales match so that distance meters Y matches distance meters Y scaling.

See example below. The X:Y scale ratio is * NOT * 1:1. How can I do 1:1 ?
xxx-profile

Example: Save VictoryChart as base64 SVG data as file image using RNFS

  onWriteImageToFile(fileAttachment) {
    RNFS.writeFile(fileAttachment.path, fileAttachment.fileData, fileAttachment.encoding)
      .then(() => {
        console.log('RNFS.writeFile ', fileAttachment.path);
        // this.onWriteFileComplete(fileAttachment, sendCallback);
      })
      .catch((err) => {
        console.log(err);
        Alert.alert(`Failed to write file: ${fileAttachment.name}`, err.message);
      });
  },

In render method, call SVG to DataURL((base64) method.

      <VictoryChart

        ref={(chart) => {
          if (chart && chart.containerRef && chart.containerRef.svgRef) {
            setTimeout(() => {
              chart.containerRef.svgRef.toDataURL((base64) => {
                const name = 'profile.png';
                const path = `${RNFS.DocumentDirectoryPath}/${name}`;
                const fileAttachment = {
                  path,
                  name,
                  fileData: base64,
                  mimeType: 'image/png',
                  encoding: 'base64',
                };
                this.onWriteImageToFile(fileAttachment);
              });
            }, 500);
          }
        }}
      >
      ... < removed code >

Thank in advance,

Render offscreen ( works on iOS and hopefully Android )

Now if I can figure out how to do this once:

  1. Make View width twice screen width
  2. Set justifyContent to flex-end
  3. Call SVG to DataURL((base64) method.
  4. Restore normal View

Or perhaps make a new transitional view that says "Creating PDF" onscreen,
and renders chart offscreen,
and creates image file by calling svgRef.toDataURL,

Ugly but I think it can work.

render()
   const { height, width } = Dimensions.get('window');
    return (
        <View
         style={[styles.containerOffscreen]}        
         width={2 * width}
        >
          {victoryChart}
        </View>
    );
  },
});
const styles = StyleSheet.create({
  containerOffscreen: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    justifyContent: 'flex-end',
    alignItems: 'center',
  },

See [email protected] exposes a containerRef:

https://github.com/FormidableLabs/victory/issues/781#event-1281057513
released in [email protected]. Look for this in the next victory native release as well. ~ next week

I have not tried but hope this will work with the new containerRef exposed in [email protected]:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.logContainerRef = this.logContainerRef.bind(this);
  }

  componentDidMount() {
    this.logContainerRef();
  }

  logContainerRef() {
    console.log(this.containerRef);
  }

  render() {
    return (
      <VictoryBar
        containerComponent={
          <VictoryContainer containerRef={(ref) => {
            this.containerRef = ref;

            setTimeout(() => {
              this.containerRef.svgRef.toDataURL((base64) => {
                console.log(base64);
              });
            }, 500);
          }}
          />
        }
      />
    );
  }
}


@esutton Your approach sounds great, did you manage to make it work? I'm in the same situation now, need to create a pdf with chart inside

@dzuncoi I have it working for victory-native 0.7.0

````
width={1056}

    ref={(chart) => {
      if (chart && chart.containerRef && chart.containerRef.svgRef) {
        setTimeout(() => {
          chart.containerRef.svgRef.toDataURL((base64) => {
            const name = 'profile.png';

            // iOS Cache Problem: New profile.png images is created but PDF still contains old, even if delete png and pdf first?
            // ToDo: Create a "tmp-<uuid>" folder each time?
            // const path = `${Utility.getFilePath()}/tmp-pdf/${name}`;
            const path = `${Utility.getFilePath()}/${name}`;

            const fileAttachment = {
              path,
              name,
              fileData: base64,
              mimeType: 'image/png',
              encoding: 'base64',
            };
            // console.log(base64);
            // console.log('fileAttachment:', fileAttachment);
            this.onSvgCreated(fileAttachment);
          });
        }, 500);
      }
    }}
  >

````

However the container ref is exposure has changed and the above can no longer work with the latest version of victory-native.

See: Add a containerRef prop for those who need a reference to the rendered chart #781

I will need to get it working with the latest victory-native but have not had time to try yet.

@esutton Thanks for your response. I didn't make it work in latest version so I'm trying the approach using react-native-view-shot, convert it to image and insert to HTML, performance with 2,3 small images (400x200) is quite good

@dzuncoi Thanks!.

I will give https://github.com/gre/react-native-view-shot a shot!

It sounds like less effort to get working.

@dzuncoi Do you know if react-native-view-shot works offscreen?

I need to make a landscape image of the victor-native chart that gets included in the PDF even if the device is being help in portrait mode. For this reason I need to draw offscreen.

I guess react-native-view-shot should be the same concept.

Yes, I tested this case, it works. Because I think react-native-view-shot will capture base on component's ref, no matter where it is.

My example:

// ...
capture = () => {
  captureRef(this.mRef, {
    format: "jpg",
    quality: 1,
  })
}

render() {
  return (
    <Button onPress={this.capture} />
    <View ref={ref => this.mRef = ref} style={{ position: absolute, left: 300, top: 200, background: 'red' }}>
      // Some element inside
    </View>
  )
}

The element above will be half onscreen, half offscreen, and the image is like a charm :)
@esutton

@dzuncoi Thank you for the code example.

The element above will be half onscreen, half offscreen, and the image is like a charm :)

That's what my victory-native image capture looks like as well.

It's rather ugly with left corner of the line chart displaying. I added a circular progress bar on top to make it better. It would be nice to hide it completely while being rendered and saved to storage.

Hello, I also have similar problem but with react js. Does anyone know how to generate an image for PDF in react js? Thanks in advance!

I'm closing this issue as a wont fix

Was this page helpful?
0 / 5 - 0 ratings