diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
new file mode 100644
index 000000000..965f8201c
--- /dev/null
+++ b/.storybook/preview-head.html
@@ -0,0 +1,6 @@
+
+
+
diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js
index 2b382123f..f0925b88c 100644
--- a/src/__demo__/SingleValue.stories.js
+++ b/src/__demo__/SingleValue.stories.js
@@ -1,4 +1,4 @@
-import React, { useState, useMemo, useRef, useEffect } from 'react'
+import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'
import { createVisualization } from '../index.js'
const constainerStyleBase = {
width: 800,
@@ -636,6 +636,7 @@ export const Default = () => {
const [dashboard, setDashboard] = useState(false)
const [showIcon, setShowIcon] = useState(true)
const [indicatorType, setIndicatorType] = useState('plain')
+ const [exportAsPdf, setExportAsPdf] = useState(true)
const [width, setWidth] = useState(constainerStyleBase.width)
const [height, setHeight] = useState(constainerStyleBase.height)
const containerStyle = useMemo(
@@ -646,6 +647,39 @@ export const Default = () => {
}),
[width, height]
)
+ const downloadOffline = useCallback(() => {
+ if (newChartRef.current) {
+ const currentBackgroundColor =
+ newChartRef.current.userOptions.chart.backgroundColor
+
+ newChartRef.current.update({
+ exporting: {
+ chartOptions: {
+ isPdfExport: exportAsPdf,
+ },
+ },
+ })
+ newChartRef.current.exportChartLocal(
+ {
+ sourceHeight: 768,
+ sourceWidth: 1024,
+ scale: 1,
+ fallbackToExportServer: false,
+ filename: 'testOfflineDownload',
+ showExportInProgress: true,
+ type: exportAsPdf ? 'application/pdf' : 'image/png',
+ },
+ {
+ chart: {
+ backgroundColor:
+ currentBackgroundColor === 'transparent'
+ ? '#ffffff'
+ : currentBackgroundColor,
+ },
+ }
+ )
+ }
+ }, [exportAsPdf])
useEffect(() => {
if (newContainerRef.current) {
requestAnimationFrame(() => {
@@ -748,6 +782,15 @@ export const Default = () => {
})}
+
+
diff --git a/src/visualizations/config/adapters/dhis_highcharts/events/loadCustomSVG/singleValue/index.js b/src/visualizations/config/adapters/dhis_highcharts/events/loadCustomSVG/singleValue/index.js
index 268d2c547..84cc83e7d 100644
--- a/src/visualizations/config/adapters/dhis_highcharts/events/loadCustomSVG/singleValue/index.js
+++ b/src/visualizations/config/adapters/dhis_highcharts/events/loadCustomSVG/singleValue/index.js
@@ -7,7 +7,7 @@ import { DynamicStyles } from './styles.js'
export default function loadSingleValueSVG() {
const { formattedValue, icon, subText, fontColor } =
this.userOptions.customSVGOptions
- const dynamicStyles = new DynamicStyles()
+ const dynamicStyles = new DynamicStyles(this.userOptions?.isPdfExport)
const valueElement = this.renderer
.text(formattedValue)
.attr('data-test', 'visualization-primary-value')
diff --git a/src/visualizations/config/adapters/dhis_highcharts/events/loadCustomSVG/singleValue/styles.js b/src/visualizations/config/adapters/dhis_highcharts/events/loadCustomSVG/singleValue/styles.js
index e7ec189fd..f1b944ee2 100644
--- a/src/visualizations/config/adapters/dhis_highcharts/events/loadCustomSVG/singleValue/styles.js
+++ b/src/visualizations/config/adapters/dhis_highcharts/events/loadCustomSVG/singleValue/styles.js
@@ -28,14 +28,15 @@ const spacings = [
export const MIN_SIDE_WHITESPACE = 4
export class DynamicStyles {
- constructor() {
+ constructor(isPdfExport) {
this.currentIndex = 0
+ this.isPdfExport = isPdfExport
}
getStyle() {
return {
value: {
...valueStyles[this.currentIndex],
- 'font-weight': '300',
+ 'font-weight': this.isPdfExport ? 'normal' : '300',
},
subText: subTextStyles[this.currentIndex],
spacing: spacings[this.currentIndex],
diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js
index f087f06bd..3620e81f5 100644
--- a/src/visualizations/config/generators/highcharts/index.js
+++ b/src/visualizations/config/generators/highcharts/index.js
@@ -3,16 +3,20 @@ import HM from 'highcharts/highcharts-more'
import HB from 'highcharts/modules/boost'
import HE from 'highcharts/modules/exporting'
import HNDTD from 'highcharts/modules/no-data-to-display'
+import HOE from 'highcharts/modules/offline-exporting'
import HPF from 'highcharts/modules/pattern-fill'
import HSG from 'highcharts/modules/solid-gauge'
+import PEBFP from './pdfExportBugFixPlugin/index.js'
// apply
HM(H)
HSG(H)
HNDTD(H)
HE(H)
+HOE(H)
HPF(H)
HB(H)
+PEBFP(H)
/* Whitelist some additional SVG attributes here. Without this,
* the PDF export for the SingleValue visualization breaks. */
diff --git a/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/index.js b/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/index.js
new file mode 100644
index 000000000..7b4899cde
--- /dev/null
+++ b/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/index.js
@@ -0,0 +1,7 @@
+import nonASCIIFontBugfix from './nonASCIIFont.js'
+import textShadowBugFix from './textShadow.js'
+
+export default function (H) {
+ textShadowBugFix(H)
+ nonASCIIFontBugfix(H)
+}
diff --git a/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/nonASCIIFont.js b/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/nonASCIIFont.js
new file mode 100644
index 000000000..d2c8d9835
--- /dev/null
+++ b/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/nonASCIIFont.js
@@ -0,0 +1,9 @@
+/* This is a workaround for https://github.com/highcharts/highcharts/issues/22008
+ * We add some transparent text in a non-ASCII script to the chart to prevent
+ * the chart from being exported in a serif font */
+
+export default function (H) {
+ H.addEvent(H.Chart, 'load', function () {
+ this.renderer.text('ыки', 20, 20).attr({ opacity: 0 }).add()
+ })
+}
diff --git a/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/textShadow.js b/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/textShadow.js
new file mode 100644
index 000000000..21a96e1a5
--- /dev/null
+++ b/src/visualizations/config/generators/highcharts/pdfExportBugFixPlugin/textShadow.js
@@ -0,0 +1,308 @@
+/* This plugin was provided by HighCharts support and resolves an issue with label
+ * text that has a white outline, such as the one we use for stacked bar charts.
+ * For example: "ANC: 1-4 visits by districts this year (stacked)"
+ * This issue has actually been resolved in HighCharts v11, so once we have upgraded
+ * to that version, this plugin can be removed. */
+
+export default function (H) {
+ const { AST, defaultOptions, downloadURL } = H,
+ { ajax } = H.HttpUtilities,
+ doc = document,
+ win = window,
+ OfflineExporting =
+ H._modules['Extensions/OfflineExporting/OfflineExporting.js'],
+ { getScript, svgToPdf, imageToDataUrl, svgToDataUrl } = OfflineExporting
+
+ H.wrap(
+ OfflineExporting,
+ 'downloadSVGLocal',
+ function (proceed, svg, options, failCallback, successCallback) {
+ var dummySVGContainer = doc.createElement('div'),
+ imageType = options.type || 'image/png',
+ filename =
+ (options.filename || 'chart') +
+ '.' +
+ (imageType === 'image/svg+xml'
+ ? 'svg'
+ : imageType.split('/')[1]),
+ scale = options.scale || 1
+ var svgurl,
+ blob,
+ finallyHandler,
+ libURL = options.libURL || defaultOptions.exporting.libURL,
+ objectURLRevoke = true,
+ pdfFont = options.pdfFont
+ // Allow libURL to end with or without fordward slash
+ libURL = libURL.slice(-1) !== '/' ? libURL + '/' : libURL
+ /*
+ * Detect if we need to load TTF fonts for the PDF, then load them and
+ * proceed.
+ *
+ * @private
+ */
+ var loadPdfFonts = function (svgElement, callback) {
+ var hasNonASCII = function (s) {
+ return (
+ // eslint-disable-next-line no-control-regex
+ /[^\u0000-\u007F\u200B]+/.test(s)
+ )
+ }
+ // Register an event in order to add the font once jsPDF is
+ // initialized
+ var addFont = function (variant, base64) {
+ win.jspdf.jsPDF.API.events.push([
+ 'initialized',
+ function () {
+ this.addFileToVFS(variant, base64)
+ this.addFont(variant, 'HighchartsFont', variant)
+ if (!this.getFontList().HighchartsFont) {
+ this.setFont('HighchartsFont')
+ }
+ },
+ ])
+ }
+ // If there are no non-ASCII characters in the SVG, do not use
+ // bother downloading the font files
+ if (pdfFont && !hasNonASCII(svgElement.textContent || '')) {
+ pdfFont = void 0
+ }
+ // Add new font if the URL is declared, #6417.
+ var variants = ['normal', 'italic', 'bold', 'bolditalic']
+ // Shift the first element off the variants and add as a font.
+ // Then asynchronously trigger the next variant until calling the
+ // callback when the variants are empty.
+ var normalBase64
+ var shiftAndLoadVariant = function () {
+ var variant = variants.shift()
+ // All variants shifted and possibly loaded, proceed
+ if (!variant) {
+ return callback()
+ }
+ var url = pdfFont && pdfFont[variant]
+ if (url) {
+ ajax({
+ url: url,
+ responseType: 'blob',
+ success: function (data, xhr) {
+ var reader = new FileReader()
+ reader.onloadend = function () {
+ if (typeof this.result === 'string') {
+ var base64 = this.result.split(',')[1]
+ addFont(variant, base64)
+ if (variant === 'normal') {
+ normalBase64 = base64
+ }
+ }
+ shiftAndLoadVariant()
+ }
+ reader.readAsDataURL(xhr.response)
+ },
+ error: shiftAndLoadVariant,
+ })
+ } else {
+ // For other variants, fall back to normal text weight/style
+ if (normalBase64) {
+ addFont(variant, normalBase64)
+ }
+ shiftAndLoadVariant()
+ }
+ }
+ shiftAndLoadVariant()
+ }
+ /*
+ * @private
+ */
+ var downloadPDF = function () {
+ AST.setElementHTML(dummySVGContainer, svg)
+ var textElements =
+ dummySVGContainer.getElementsByTagName('text'),
+ // Copy style property to element from parents if it's not
+ // there. Searches up hierarchy until it finds prop, or hits the
+ // chart container.
+ setStylePropertyFromParents = function (el, propName) {
+ var curParent = el
+ while (curParent && curParent !== dummySVGContainer) {
+ if (curParent.style[propName]) {
+ el.style[propName] = curParent.style[propName]
+ break
+ }
+ curParent = curParent.parentNode
+ }
+ }
+ var titleElements,
+ outlineElements
+ // Workaround for the text styling. Making sure it does pick up
+ // settings for parent elements.
+ ;[].forEach.call(textElements, function (el) {
+ // Workaround for the text styling. making sure it does pick up
+ // the root element
+ ;['font-family', 'font-size'].forEach(function (property) {
+ setStylePropertyFromParents(el, property)
+ })
+ el.style.fontFamily =
+ pdfFont && pdfFont.normal
+ ? // Custom PDF font
+ 'HighchartsFont'
+ : // Generic font (serif, sans-serif etc)
+ String(
+ el.style.fontFamily &&
+ el.style.fontFamily.split(' ').splice(-1)
+ )
+ // Workaround for plotband with width, removing title from text
+ // nodes
+ titleElements = el.getElementsByTagName('title')
+ ;[].forEach.call(titleElements, function (titleElement) {
+ el.removeChild(titleElement)
+ })
+
+ // Remove all .highcharts-text-outline elements, #17170
+ outlineElements = el.getElementsByClassName(
+ 'highcharts-text-outline'
+ )
+ while (outlineElements.length > 0) {
+ const outline = outlineElements[0]
+ if (outline.parentNode) {
+ outline.parentNode.removeChild(outline)
+ }
+ }
+ })
+ var svgNode = dummySVGContainer.querySelector('svg')
+ if (svgNode) {
+ loadPdfFonts(svgNode, function () {
+ svgToPdf(svgNode, 0, function (pdfData) {
+ try {
+ downloadURL(pdfData, filename)
+ if (successCallback) {
+ successCallback()
+ }
+ } catch (e) {
+ failCallback(e)
+ }
+ })
+ })
+ }
+ }
+ // Initiate download depending on file type
+ if (imageType === 'image/svg+xml') {
+ // SVG download. In this case, we want to use Microsoft specific
+ // Blob if available
+ try {
+ if (typeof win.navigator.msSaveOrOpenBlob !== 'undefined') {
+ // eslint-disable-next-line no-undef
+ blob = new MSBlobBuilder()
+ blob.append(svg)
+ svgurl = blob.getBlob('image/svg+xml')
+ } else {
+ svgurl = svgToDataUrl(svg)
+ }
+ downloadURL(svgurl, filename)
+ if (successCallback) {
+ successCallback()
+ }
+ } catch (e) {
+ failCallback(e)
+ }
+ } else if (imageType === 'application/pdf') {
+ if (win.jspdf && win.jspdf.jsPDF) {
+ downloadPDF()
+ } else {
+ // Must load pdf libraries first. // Don't destroy the object
+ // URL yet since we are doing things asynchronously. A cleaner
+ // solution would be nice, but this will do for now.
+ objectURLRevoke = true
+ getScript(libURL + 'jspdf.js', function () {
+ getScript(libURL + 'svg2pdf.js', downloadPDF)
+ })
+ }
+ } else {
+ // PNG/JPEG download - create bitmap from SVG
+ svgurl = svgToDataUrl(svg)
+ finallyHandler = function () {
+ try {
+ OfflineExporting.domurl.revokeObjectURL(svgurl)
+ } catch (e) {
+ // Ignore
+ }
+ }
+ // First, try to get PNG by rendering on canvas
+ imageToDataUrl(
+ svgurl,
+ imageType,
+ {},
+ scale,
+ function (imageURL) {
+ // Success
+ try {
+ downloadURL(imageURL, filename)
+ if (successCallback) {
+ successCallback()
+ }
+ } catch (e) {
+ failCallback(e)
+ }
+ },
+ function () {
+ // Failed due to tainted canvas
+ // Create new and untainted canvas
+ var canvas = doc.createElement('canvas'),
+ ctx = canvas.getContext('2d'),
+ imageWidth =
+ svg.match(
+ // eslint-disable-next-line no-useless-escape
+ /^