UNPKG

apexcharts

Version:

A JavaScript Chart Library

466 lines (406 loc) 14 kB
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(/&nbsp;/g, '&#160;') } // 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) { if (scale == undefined) { scale = 1 // if no scale is specified, don't 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, fileName, columnDelimiter = ',', lineDelimiter = '\n', }) { const w = this.w if (!series) series = w.config.series let columns = [] let rows = [] let result = '' let universalBOM = '\uFEFF' let gSeries = w.globals.series.map((s, i) => { return w.globals.collapsedSeriesIndices.indexOf(i) === -1 ? s : [] }) const isTimeStamp = (num) => { return w.config.xaxis.type === 'datetime' && String(num).length >= 10 } const seriesMaxDataLength = Math.max( ...series.map((s) => { return s.data ? s.data.length : 0 }) ) 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('') } // Fix https://github.com/apexcharts/apexcharts.js/issues/3365 const getEmptyDataForCsvColumn = () => { return [...Array(seriesMaxDataLength)].map(() => '') } const handleAxisRowsColumns = (s, sI) => { if (columns.length && sI === 0) { // It's the first series. Go ahead and create the first row with header information. rows.push(columns.join(columnDelimiter)) } if (s.data) { // Use the data we have, or generate a properly sized empty array with empty data if some data is missing. s.data = (s.data.length && s.data) || getEmptyDataForCsvColumn() for (let i = 0; i < s.data.length; i++) { // Reset the columns array so that we can start building columns for this row. 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) { // It's the first series. Also handle the category. 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++) { if (dataFormat.isFormatXY()) { columns.push(series[ci].data[i]?.y) } else { columns.push(gSeries[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)) } } } } const handleUnequalXValues = () => { const categories = new Set() const data = {} series.forEach((s, sI) => { s?.data.forEach((dataItem) => { let cat, value if (dataFormat.isFormatXY()) { cat = dataItem.x value = dataItem.y } else if (dataFormat.isFormat2DArray()) { cat = dataItem[0] value = dataItem[1] } else { return } if (!data[cat]) { data[cat] = Array(series.length).fill('') } data[cat][sI] = value categories.add(cat) }) }) if (columns.length) { rows.push(columns.join(columnDelimiter)) } Array.from(categories) .sort() .forEach((cat) => { rows.push([ isTimeStamp(cat) && w.config.xaxis.type === 'datetime' ? w.config.chart.toolbar.export.csv.dateFormatter(cat) : Utils.isNumber(cat) ? cat : cat.split(columnDelimiter).join(''), data[cat].join(columnDelimiter), ]) }) } columns.push(w.config.chart.toolbar.export.csv.headerCategory) if (w.config.chart.type === 'boxPlot') { columns.push('minimum') columns.push('q1') columns.push('median') columns.push('q3') columns.push('maximum') } else if (w.config.chart.type === 'candlestick') { columns.push('open') columns.push('high') columns.push('low') columns.push('close') } else if (w.config.chart.type === 'rangeBar') { columns.push('minimum') columns.push('maximum') } else { 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)) } if ( !w.globals.allSeriesHasEqualX && w.globals.axisCharts && !w.config.xaxis.categories.length && !w.config.labels.length ) { handleUnequalXValues() } else { series.map((s, sI) => { if (w.globals.axisCharts) { handleAxisRowsColumns(s, sI) } else { columns = [] columns.push(w.globals.labels[sI].split(columnDelimiter).join('')) columns.push(gSeries[sI]) rows.push(columns.join(columnDelimiter)) } }) } result += rows.join(lineDelimiter) this.triggerDownload( 'data:text/csv; charset=utf-8,' + encodeURIComponent(universalBOM + result), fileName ? fileName : 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