-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
tosvg.js
160 lines (133 loc) · 6.02 KB
/
tosvg.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
'use strict';
var d3 = require('@plotly/d3');
var Lib = require('../lib');
var Drawing = require('../components/drawing');
var Color = require('../components/color');
var xmlnsNamespaces = require('../constants/xmlns_namespaces');
var DOUBLEQUOTE_REGEX = /"/g;
var DUMMY_SUB = 'TOBESTRIPPED';
var DUMMY_REGEX = new RegExp('("' + DUMMY_SUB + ')|(' + DUMMY_SUB + '")', 'g');
function htmlEntityDecode(s) {
var hiddenDiv = d3.select('body').append('div').style({display: 'none'}).html('');
var replaced = s.replace(/(&[^;]*;)/gi, function(d) {
if(d === '<') { return '<'; } // special handling for brackets
if(d === '&rt;') { return '>'; }
if(d.indexOf('<') !== -1 || d.indexOf('>') !== -1) { return ''; }
return hiddenDiv.html(d).text(); // everything else, let the browser decode it to unicode
});
hiddenDiv.remove();
return replaced;
}
function xmlEntityEncode(str) {
return str.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g, '&');
}
module.exports = function toSVG(gd, format, scale) {
var fullLayout = gd._fullLayout;
var svg = fullLayout._paper;
var toppaper = fullLayout._toppaper;
var width = fullLayout.width;
var height = fullLayout.height;
var i;
// make background color a rect in the svg, then revert after scraping
// all other alterations have been dealt with by properly preparing the svg
// in the first place... like setting cursors with css classes so we don't
// have to remove them, and providing the right namespaces in the svg to
// begin with
svg.insert('rect', ':first-child')
.call(Drawing.setRect, 0, 0, width, height)
.call(Color.fill, fullLayout.paper_bgcolor);
// subplot-specific to-SVG methods
// which notably add the contents of the gl-container
// into the main svg node
var basePlotModules = fullLayout._basePlotModules || [];
for(i = 0; i < basePlotModules.length; i++) {
var _module = basePlotModules[i];
if(_module.toSVG) _module.toSVG(gd);
}
// add top items above them assumes everything in toppaper is either
// a group or a defs, and if it's empty (like hoverlayer) we can ignore it.
if(toppaper) {
var nodes = toppaper.node().childNodes;
// make copy of nodes as childNodes prop gets mutated in loop below
var topGroups = Array.prototype.slice.call(nodes);
for(i = 0; i < topGroups.length; i++) {
var topGroup = topGroups[i];
if(topGroup.childNodes.length) svg.node().appendChild(topGroup);
}
}
// remove draglayer for Adobe Illustrator compatibility
if(fullLayout._draggers) {
fullLayout._draggers.remove();
}
// in case the svg element had an explicit background color, remove this
// we want the rect to get the color so it's the right size; svg bg will
// fill whatever container it's displayed in regardless of plot size.
svg.node().style.background = '';
svg.selectAll('text')
.attr({'data-unformatted': null, 'data-math': null})
.each(function() {
var txt = d3.select(this);
// hidden text is pre-formatting mathjax, the browser ignores it
// but in a static plot it's useless and it can confuse batik
// we've tried to standardize on display:none but make sure we still
// catch visibility:hidden if it ever arises
if(this.style.visibility === 'hidden' || this.style.display === 'none') {
txt.remove();
return;
} else {
// clear other visibility/display values to default
// to not potentially confuse non-browser SVG implementations
txt.style({visibility: null, display: null});
}
// Font family styles break things because of quotation marks,
// so we must remove them *after* the SVG DOM has been serialized
// to a string (browsers convert singles back)
var ff = this.style.fontFamily;
if(ff && ff.indexOf('"') !== -1) {
txt.style('font-family', ff.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
}
// Drop normal font-weight, font-style and font-variant to reduce the size
var fw = this.style.fontWeight;
if(fw && (fw === 'normal' || fw === '400')) { // font-weight 400 is similar to normal
txt.style('font-weight', undefined);
}
var fs = this.style.fontStyle;
if(fs && fs === 'normal') {
txt.style('font-style', undefined);
}
var fv = this.style.fontVariant;
if(fv && fv === 'normal') {
txt.style('font-variant', undefined);
}
});
svg.selectAll('.gradient_filled,.pattern_filled').each(function() {
var pt = d3.select(this);
// similar to font family styles above,
// we must remove " after the SVG DOM has been serialized
var fill = this.style.fill;
if(fill && fill.indexOf('url(') !== -1) {
pt.style('fill', fill.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
}
var stroke = this.style.stroke;
if(stroke && stroke.indexOf('url(') !== -1) {
pt.style('stroke', stroke.replace(DOUBLEQUOTE_REGEX, DUMMY_SUB));
}
});
if(format === 'pdf' || format === 'eps') {
// these formats make the extra line MathJax adds around symbols look super thick in some cases
// it looks better if this is removed entirely.
svg.selectAll('#MathJax_SVG_glyphs path')
.attr('stroke-width', 0);
}
if(format === 'svg' && scale) {
svg.attr('width', scale * width);
svg.attr('height', scale * height);
svg.attr('viewBox', '0 0 ' + width + ' ' + height);
}
var s = new window.XMLSerializer().serializeToString(svg.node());
s = htmlEntityDecode(s);
s = xmlEntityEncode(s);
// Fix quotations around font strings and gradient URLs
s = s.replace(DUMMY_REGEX, '\'');
return s;
};