import Data from '../modules/Data' import AxesUtils from '../modules/axes/AxesUtils' import Series from '../modules/Series' import Utils from '../utils/Utils' class Exports { constructor(ctx) { this.ctx = ctx this.w = ctx.w } scaleSvgNode(svg, scale) { // get current both width and height of the svg let svgWidth = parseFloat(svg.getAttributeNS(null, 'width')) let svgHeight = parseFloat(svg.getAttributeNS(null, 'height')) // set new width and height based on the scale svg.setAttributeNS(null, 'width', svgWidth * scale) svg.setAttributeNS(null, 'height', svgHeight * scale) svg.setAttributeNS(null, 'viewBox', '0 0 ' + svgWidth + ' ' + svgHeight) } fixSvgStringForIe11(svgData) { // IE11 generates broken SVG that we have to fix by using regex if (!Utils.isIE11()) { // not IE11 - noop return svgData.replace(/ /g, ' ') } // replace second occurrence of "xmlns" attribute with "xmlns:xlink" with correct url + add xmlns:svgjs let nXmlnsSeen = 0 let result = svgData.replace( /xmlns="http:\/\/www.w3.org\/2000\/svg"/g, (match) => { nXmlnsSeen++ return nXmlnsSeen === 2 ? 'xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev"' : match } ) // remove the invalid empty namespace declarations result = result.replace(/xmlns:NS\d+=""/g, '') // remove these broken namespaces from attributes result = result.replace(/NS\d+:(\w+:\w+=")/g, '$1') return result } getSvgString(scale) { let svgString = this.w.globals.dom.Paper.svg() // in case the scale is different than 1, the svg needs to be rescaled if (scale !== 1) { // clone the svg node so it remains intact in the UI const svgNode = this.w.globals.dom.Paper.node.cloneNode(true) // scale the image this.scaleSvgNode(svgNode, scale) // get the string representation of the svgNode svgString = new XMLSerializer().serializeToString(svgNode) } return this.fixSvgStringForIe11(svgString) } cleanup() { const w = this.w // hide some elements to avoid printing them on exported svg const xcrosshairs = w.globals.dom.baseEl.getElementsByClassName( 'apexcharts-xcrosshairs' ) const ycrosshairs = w.globals.dom.baseEl.getElementsByClassName( 'apexcharts-ycrosshairs' ) const zoomSelectionRects = w.globals.dom.baseEl.querySelectorAll( '.apexcharts-zoom-rect, .apexcharts-selection-rect' ) Array.prototype.forEach.call(zoomSelectionRects, (z) => { z.setAttribute('width', 0) }) if (xcrosshairs && xcrosshairs[0]) { xcrosshairs[0].setAttribute('x', -500) xcrosshairs[0].setAttribute('x1', -500) xcrosshairs[0].setAttribute('x2', -500) } if (ycrosshairs && ycrosshairs[0]) { ycrosshairs[0].setAttribute('y', -100) ycrosshairs[0].setAttribute('y1', -100) ycrosshairs[0].setAttribute('y2', -100) } } svgUrl() { this.cleanup() const svgData = this.getSvgString() const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }) return URL.createObjectURL(svgBlob) } dataURI(options) { return new Promise((resolve) => { const w = this.w const scale = options ? options.scale || options.width / w.globals.svgWidth : 1 this.cleanup() const canvas = document.createElement('canvas') canvas.width = w.globals.svgWidth * scale canvas.height = parseInt(w.globals.dom.elWrap.style.height, 10) * scale // because of resizeNonAxisCharts const canvasBg = w.config.chart.background === 'transparent' ? '#fff' : w.config.chart.background let ctx = canvas.getContext('2d') ctx.fillStyle = canvasBg ctx.fillRect(0, 0, canvas.width * scale, canvas.height * scale) const svgData = this.getSvgString(scale) if (window.canvg && Utils.isIE11()) { // use canvg as a polyfill to workaround ie11 considering a canvas with loaded svg 'unsafe' // without ignoreClear we lose our background color; without ignoreDimensions some grid lines become invisible let v = window.canvg.Canvg.fromString(ctx, svgData, { ignoreClear: true, ignoreDimensions: true }) // render the svg to canvas v.start() let blob = canvas.msToBlob() // dispose - missing this will cause a memory leak v.stop() resolve({ blob }) } else { const svgUrl = 'data:image/svg+xml,' + encodeURIComponent(svgData) let img = new Image() img.crossOrigin = 'anonymous' img.onload = () => { ctx.drawImage(img, 0, 0) if (canvas.msToBlob) { // IE and Edge can't navigate to data urls, so we return the blob instead let blob = canvas.msToBlob() resolve({ blob }) } else { let imgURI = canvas.toDataURL('image/png') resolve({ imgURI }) } } img.src = svgUrl } }) } exportToSVG() { this.triggerDownload( this.svgUrl(), this.w.config.chart.toolbar.export.svg.filename, '.svg' ) } exportToPng() { this.dataURI().then(({ imgURI, blob }) => { if (blob) { navigator.msSaveOrOpenBlob(blob, this.w.globals.chartID + '.png') } else { this.triggerDownload( imgURI, this.w.config.chart.toolbar.export.png.filename, '.png' ) } }) } exportToCSV({ series, columnDelimiter, lineDelimiter = '\n' }) { const w = this.w let columns = [] let rows = [] let result = '' let universalBOM = '\uFEFF' const isTimeStamp = (num) => { return w.config.xaxis.type === 'datetime' && String(num).length >= 10 } const dataFormat = new Data(this.ctx) const axesUtils = new AxesUtils(this.ctx) const getCat = (i) => { let cat = '' // pie / donut/ radial if (!w.globals.axisCharts) { cat = w.config.labels[i] } else { // xy charts // non datetime if ( w.config.xaxis.type === 'category' || w.config.xaxis.convertedCatToNumeric ) { if (w.globals.isBarHorizontal) { let lbFormatter = w.globals.yLabelFormatters[0] let sr = new Series(this.ctx) let activeSeries = sr.getActiveConfigSeriesIndex() cat = lbFormatter(w.globals.labels[i], { seriesIndex: activeSeries, dataPointIndex: i, w }) } else { cat = axesUtils.getLabel( w.globals.labels, w.globals.timescaleLabels, 0, i ).text } } // datetime, but labels specified in categories or labels if (w.config.xaxis.type === 'datetime') { if (w.config.xaxis.categories.length) { cat = w.config.xaxis.categories[i] } else if (w.config.labels.length) { cat = w.config.labels[i] } } } if (Array.isArray(cat)) { cat = cat.join(' ') } return Utils.isNumber(cat) ? cat : cat.split(columnDelimiter).join('') } const handleAxisRowsColumns = (s, sI) => { if (columns.length && sI === 0) { rows.push(columns.join(columnDelimiter)) } if (s.data && s.data.length) { for (let i = 0; i < s.data.length; i++) { columns = [] let cat = getCat(i) if (!cat) { if (dataFormat.isFormatXY()) { cat = series[sI].data[i].x } else if (dataFormat.isFormat2DArray()) { cat = series[sI].data[i] ? series[sI].data[i][0] : '' } } if (sI === 0) { columns.push( isTimeStamp(cat) ? w.config.chart.toolbar.export.csv.dateFormatter(cat) : Utils.isNumber(cat) ? cat : cat.split(columnDelimiter).join('') ) for (let ci = 0; ci < w.globals.series.length; ci++) { columns.push(w.globals.series[ci][i]) } } if ( w.config.chart.type === 'candlestick' || (s.type && s.type === 'candlestick') ) { columns.pop() columns.push(w.globals.seriesCandleO[sI][i]) columns.push(w.globals.seriesCandleH[sI][i]) columns.push(w.globals.seriesCandleL[sI][i]) columns.push(w.globals.seriesCandleC[sI][i]) } if ( w.config.chart.type === 'boxPlot' || (s.type && s.type === 'boxPlot') ) { columns.pop() columns.push(w.globals.seriesCandleO[sI][i]) columns.push(w.globals.seriesCandleH[sI][i]) columns.push(w.globals.seriesCandleM[sI][i]) columns.push(w.globals.seriesCandleL[sI][i]) columns.push(w.globals.seriesCandleC[sI][i]) } if (w.config.chart.type === 'rangeBar') { columns.pop() columns.push(w.globals.seriesRangeStart[sI][i]) columns.push(w.globals.seriesRangeEnd[sI][i]) } if (columns.length) { rows.push(columns.join(columnDelimiter)) } } } } columns.push(w.config.chart.toolbar.export.csv.headerCategory) series.map((s, sI) => { const sname = s.name ? s.name : `series-${sI}` if (w.globals.axisCharts) { columns.push( sname.split(columnDelimiter).join('') ? sname.split(columnDelimiter).join('') : `series-${sI}` ) } }) if (!w.globals.axisCharts) { columns.push(w.config.chart.toolbar.export.csv.headerValue) rows.push(columns.join(columnDelimiter)) } series.map((s, sI) => { if (w.globals.axisCharts) { handleAxisRowsColumns(s, sI) } else { columns = [] columns.push(w.globals.labels[sI].split(columnDelimiter).join('')) columns.push(w.globals.series[sI]) rows.push(columns.join(columnDelimiter)) } }) result += rows.join(lineDelimiter) this.triggerDownload( 'data:text/csv; charset=utf-8,' + encodeURIComponent(universalBOM + result), w.config.chart.toolbar.export.csv.filename, '.csv' ) } triggerDownload(href, filename, ext) { const downloadLink = document.createElement('a') downloadLink.href = href downloadLink.download = (filename ? filename : this.w.globals.chartID) + ext document.body.appendChild(downloadLink) downloadLink.click() document.body.removeChild(downloadLink) } } export default Exports