1

I'm having trouble trying to embed a Highchart's chart image into an Excel file. I am basically trying to use an existing "Export Excel" button on a website to now export a static image of the Highchart displayed on the website into an excel file. I'll add the relevant code below:

async getChartPng(chartInstance): Promise<string> {
  console.log('getChartPng called');
  return new Promise(async (resolve, reject) => {
    console.log('Inside the promise');

    try {
      chartInstance.exportChart({}, (dataUrl) => {
        console.log('Inside exportChart callback');
        
        // Log the value of dataUrl
        console.log('dataUrl value:', dataUrl);
        
        if (dataUrl) {
          console.log('DataUrl:', dataUrl);
          resolve(dataUrl);
        } else {
          console.log('DataUrl is empty or undefined');
          reject('Failed to get dataUrl');
        }
      });
          console.log('After exportChart function call');
    } catch (error) {
      console.error('Error in exportChart function:', error);
      reject(error);
    }
  });
}

async addChartImage(workbook: ExcelJS.Workbook, chartInstance {
  console.log('addChartImage called');
  const chartWorksheet = workbook.addWorksheet('HighChart Image');
  console.log('chartWorksheet created:', chartWorksheet);

  try {
    const chartImagePng = await this.getChartPng(chartInstance);
    console.log('chartImagePng:', chartImagePng);

    const base64Image = chartImagePng.split(',')[1];
    const imageId = workbook.addImage({
      base64: base64Image,
      extension: 'png',
    });

    chartWorksheet.addImage(imageId, {
      tl: { col: 0, row: 0 },
      ext: { width: 600, height: 400 },
    });

  } catch (error) {
    console.error('Error adding chart image to workbook:', error);
  }
}

My code never enters into the exportChart callback function and I'm not sure why. That is to say the console.log message ('Inside exportChart callback') never prints but the message ('After exportChart function call') does print to the console browser.

I should add that a worksheet named HighChart image does show up in an excel file when I click an existing custom button on the website as intended. However, the worksheet shows up as empty and a snapshot of the Highchart image downloads as a separate png file along with the Excel file. However, what I really need is the Highchart image to show up in the excel file, in the proper worksheet. How can I do that?

Rabiya
  • 23
  • 5
  • Please edit your post to correct the syntax error around `console.log('After exportChart ....` so we won't assume the error is due to a trivial syntax error. – kikon May 01 '23 at 06:02
  • @kikon Hi, sorry about the missing quotation mark. Fixed it now. – Rabiya May 01 '23 at 13:13
  • I'm looking through the [documentation](https://api.highcharts.com/class-reference/Highcharts.Chart#exportChart) for the `exporting` module and I can't find any reference to a callback function for `exportChart()`. In the [demo](https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/members/chart-exportchart/), the method's intended function seems to be directly downloading a PNG in the browser, as you described. Perhaps you were mixing up the documentation with another library? – RNanoware May 01 '23 at 15:45
  • @RNanoware Hi, sorry if this is a dumb question. Are you stating that there is no documentation confirming that I can take a callback function as a parameter for the exportChart method? The dataURI is used as parameter for the callback fxn but if Im not allowed to have callback fxn it would explain why none of the console.log messages inside chartInstance.exportChart({}, (dataUrl) => {} get printed to console – Rabiya May 01 '23 at 18:37
  • Yes, that's correct that I cannot find any documentation referencing a callback function as a parameter in `exportChart`. @kikon has a great answer below that _I think_ should satisfy your use case: they use Highchart's `getSVG` utility and then load that graphic into the DOM (asynchronously) to eventually produce a PNG. – RNanoware May 01 '23 at 19:00

1 Answers1

2

The recommended solution to get the image programmatically and not to a file is to get the SVG through getSVG and then convert the SVG to an image. Neither exportChart nor exportChartLocal the latter despite the name (local means no external server is used) don't allow that, I ask myself as @RNanoware where did you get that whole asynchronous handler as second argument. Maybe I'm missing something?

Since people seem to still ask similar questions and no simple solution exists (let me know if there is, so I'll delete this post), I'm giving here a basic solution based on this post; it can be extended and adapted to other formats. Adapting yourgetChartPng function, I'd have (pure js):

async function getChartPng(chartInstance, options = {}){
    const svg = chartInstance.getSVG(options);

    const svgData = `data:image/svg+xml,${encodeURIComponent(svg)}`
    const loadImage = async url => {
        const img = document.createElement('img');
        img.src = url
        return new Promise((resolve, reject) => {
            img.onload = () => resolve(img);
            img.onerror = reject;
            img.src = url;
        });
    }
    const img = await loadImage(svgData);
    const canvas = document.createElement('canvas')
    canvas.width = options.width || chartInstance.chartWidth;
    canvas.height = options.height || chartInstance.chartHeight;
    canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);

    return  canvas.toDataURL(`image/png`, 1.0);
}

Here's a snippet with its use to add an image to a div element, which also contains an adaptation of your exceljs code (unused):

const myChart = new Highcharts.Chart({
    series: [
        {
            name: "Purchase",
            data: [44, 55, 57, 56, 61, 58, 63, 60, 66],
            color: "#4FC0FF",
        },
        {
            name: "Sales",
            data: [76, 85, 101, 98, 87, 105, 91, 114, 94],
            color: "#DF5638",
        },
        {
            name: "Expense",
            data: Array.from({length: 9}, () => Math.floor(200 + Math.random() * 100)),
            color: "#00A825",
        }
    ],
    chart: {
        renderTo: 'chart1',
        type: "bar",
    },

    plotOptions: {
        bar: {
            horizontal: false,
            columnWidth: "55%",
            endingShape: "rounded",
        },
    },
    dataLabels: {
        enabled: false,
    },
    xAxis: {
        categories: [
            "Jan",
            "Feb",
            "Mar",
            "Apr",
            "May",
            "Jun",
            "Jul",
            "Aug",
            "Sep",
            "Oct",
            "Nov",
            "Dec"
        ],
    },
    fill: {
        opacity: 1,
        colors: ["#4FC0FF", "#DF5638", "#00A825"],
    }
});


async function getChartPng(chartInstance, options = {}){
    const svg = chartInstance.getSVG(options);

    const svgData = `data:image/svg+xml,${encodeURIComponent(svg)}`
    const loadImage = async url => {
        const img = document.createElement('img');
        img.src = url
        return new Promise((resolve, reject) => {
            img.onload = () => resolve(img);
            img.onerror = reject;
            img.src = url;
        });
    }
    const img = await loadImage(svgData);
    const canvas = document.createElement('canvas')
    canvas.width = options.width || chartInstance.chartWidth;
    canvas.height = options.height || chartInstance.chartHeight;
    canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);

    return  canvas.toDataURL(`image/png`, 1.0);
}

async function addChartImageToElement(element, chartInstance, options){
    try {
        const base64Image = await this.getChartPng(chartInstance, options);
        const img = document.createElement('img');
        img.src = base64Image;
        element.appendChild(img);
    } catch (error) {
        console.error('Error adding chart image to dom:', error);
    }
}

async function addChartImageToWorksheet(chartWorksheet, chartInstance, options, col = 0, row = 0) {
    try {
        const base64Image = await this.getChartPng(chartInstance, options);

        const imageId = workbook.addImage({
            base64: base64Image,
            extension: 'png',
        });
        chartWorksheet.addImage(imageId, {
            tl: { col, row},
            ext: {
                width: options.width || chartInstance.chartWidth,
                height: options.height || chartInstance.chartHeight
            },
        });

    } catch (error) {
        console.error('Error adding chart image to workbook:', error);
    }
}

function addToWorkbook(workbook){
    const chartWorksheet = workbook.addWorksheet('HighChart Image');
    addChartImageToWorksheet(
        document.querySelector('#out_png'), myChart, {width: 600, height: 400}
    );
}

function displayPNG(){
    addChartImageToElement(
        document.querySelector('#out_png'), myChart, {width: 300, height: 200}
    );
}


document.querySelector('#export').onclick = displayPNG;
<div id="chart1" style="height:250px; width:80vw">
</div>

<button id="export">Export</button>
<div id="out_png" style=""></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/highcharts/10.3.3/highcharts.js" integrity="sha512-8cJ3Lf1cN3ld0jUEZy26UOg+A5YGLguP6Xi6bKLyYurrxht+xkLJ9oH9rc7pvNiYsmYuTvpe3wwS6LriK/oWDg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highcharts/10.3.3/modules/exporting.min.js" integrity="sha512-azuQazgd4m6TavtXB0Zgm1Pb7RBk7Bq0G9JV8nmec7/toRckUZ6L/fG8D3xcoXk0Le9DS+5YmAi421fry057Fg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
kikon
  • 3,670
  • 3
  • 5
  • 20