Create high-quality charts for React and PowerPoint

Jakub Olszyna
3 min readSep 7, 2021
@foxyburrow — stock.adobe.com

When you need some high-quality data visualisations for your React project you have multiple options. What if you also need the same charts to create a downloadable PowerPoint presentation and you don’t want to do the same work twice?

Simplest approach to the problem would be rendering the whole view on the server side, using tools like PhantomJS [1] or Puppeteer [2], and then saving the chart as an image. This can, however, turn out to be a little tricky and not so ‘lossless’ solution.

In this article I will show you how to create charts for your React application and then use these in a PowerPoint slide without quality loss.

Let’s assume we have already created a project containing the React client and the Express.js server. We want to add a Chart component that could be easily accessed in the backend. To achieve that, let’s use Scalable Vector Graphics (SVG) elements, that can be created and styled with D3.js library [3].

const svg = (<svgref={ref ?? localRef}xmlns='http://www.w3.org/2000/svg'className={classnames(styles.svg,className)}width={width}height={height}viewBox={[0, 0, width, height].join(' ')}{...otherProps}>{children}</svg>);

To use the chart on the server side, we need to export it as a module. Let’s create a script that will compile the whole client and turn it into a module.

#!/bin/shMODULE_BUILD_DIR=$(PWD)/../commonmkdir -p "$MODULE_BUILD_DIR"yarn exec babel ./src -- -d "$MODULE_BUILD_DIR"

Let’s not forget to copy the package.json and adjust the names accordingly

cp ./package.json "$MODULE_BUILD_DIR/"sed -i .bak '/"name": "client"/s/"client"/"common"/' $MODULE_BUILD_DIR/package.json

Once we have the module ready, we can render the Chart component on the server-side and extract the SVG contents.

const Chart = require('common/components/Chart.js').default;let svg = ReactDOMServer.renderToStaticMarkup(
React.createElement(Chart, {})
);

We want to make sure all the CSS variables are set to proper values

const CSS_VAR_MAP = {'var(--spectrum-global-color-static-white)': '#fff',};
const mapCssVars = (str) => { try { let _str = str.slice(); for (let prop in CSS_VAR_MAP) { let rx = new RegExp( `${prop.replace('(', '\\(').replace(')', '\\)')}`, 'g'); _str = _str.replace(rx, CSS_VAR_MAP[prop]); } _str = _str.replace(/adobe-clean/g, 'Adobe Clean'); return _str; } catch (err) { return str; }}svg = mapCssVars(svg);

The last step is optimising the result SVG and saving it to the filesystem.

const fs = require('fs');const os = require('os');const path = require('path');const SVGO = require('svgo');...const svgo = new SVGO({ plugins: [{ cleanupIDs: false }] });try {   const optimized = await svgo.optimize(svg);   if (optimized.data) {      svg = optimized.data;   }
} catch (err) { }
const filePath = path.join(os.tmpdir(), 'chart.svg');fs.writeFileSync(filePath, svg);

Enhanced metafile format (EMF) stores graphical images device-independently. An EMF is the native vector format for PowerPoint and acts in a similar manner to a SVG file. Shapes and text are crisp and we can even ungroup the visualisation and modify single components.

Since we already are able to generate and save the SVG files at this point, all we need to do is find a way to convert them to the EMF format. For this step we want to use Inkscape [4].

const spawn = require('spawn-promise');
const inkscape = process.env.INKSCAPE_PATH || 'inkscape';
...return spawn(inkscape, [
filePath,
'--without-gui',
`--export-filename=${filePath.replace(/\.svg$/, '.emf')}`
]);

The EMF file created in the previous step can then easily be appended to the PPT template, as an image (provided the template has a proper custom tag in place where the image should be appended), with the python-pptx library [5].

#!/usr/bin/python3prs = Presentation(path.join(templatePath, templateName))for slide in prs.slides: for shape in slide.shapes:  if re.search("^%.*%$", shape.name):   if shape.has_text_frame:    for paragraph in shape.text_frame.paragraphs:     add_text_content(paragraph, shape)   else:    if slideData.get(shape.name):     newImagePath = slideData.get(shape.name)     oldimage = shape._element     newimage = slide.shapes.add_picture(newImagePath, shape.left,   shape.top,  shape.width, shape.height)     oldimage.addnext(newimage._element)     oldimage.getparent().remove(oldimage)prs.save(resultPath)

References

  1. https://phantomjs.org/
  2. https://github.com/puppeteer/puppeteer
  3. https://d3js.org/
  4. https://inkscape.org/about/overview/
  5. https://python-pptx.readthedocs.io/en/latest/index.html

--

--