| | ';
7 | /*
8 | * returns a function that evaluates template strings
9 | * using `expr-eval`.
10 | */
11 | export default function htmlTemplate(template) {
12 | const expressions = {};
13 | const parser = new Parser();
14 | template.replace(TPL_REG, (s, formula) => {
15 | formula = formula.trim();
16 | if (formula && !expressions[formula]) {
17 | expressions[formula] = parser.parse(formula);
18 | }
19 | });
20 | return context =>
21 | purifyHtml(
22 | template.replace(TPL_REG, (s, formula) => {
23 | const result = formula.trim() ? expressions[formula.trim()].evaluate(context) : '';
24 | return result === null ? '' : result;
25 | }),
26 | ALLOWED_TAGS
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/lib/dw/utils/htmlTemplate.test.mjs:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import htmlTemplate from './htmlTemplate.mjs';
3 |
4 | test('hello world', async t => {
5 | const tpl = htmlTemplate(`hello {{ world }}`);
6 | t.is(tpl({ world: 'foo' }), 'hello foo');
7 | t.is(tpl({ world: 'cat' }), 'hello cat');
8 | t.is(tpl({ world: 42 }), 'hello 42');
9 | });
10 |
11 | test('more html', async t => {
12 | const tpl = htmlTemplate(`{{ title }}
`);
13 | t.is(tpl({ title: 'foo', value: 3 }), 'foo
');
14 | t.is(tpl({ title: 'bar', value: 9 }), 'bar
');
15 | });
16 |
17 | test('ternary operator', async t => {
18 | const tpl = htmlTemplate(`{{ value > 10 ? "bigger" : "smaller" }}`);
19 | t.is(tpl({ value: 9 }), 'smaller');
20 | t.is(tpl({ value: 11 }), 'bigger');
21 | });
22 |
23 | test('round', async t => {
24 | const tpl = htmlTemplate(`{{ ROUND(value) }}`);
25 | t.is(tpl({ value: 1.3 }), '1');
26 | t.is(tpl({ value: 1.7 }), '2');
27 | });
28 |
29 | test('evil expression', async t => {
30 | t.throws(() => {
31 | const tpl = htmlTemplate(`{{ window.document.cookie }}`);
32 | tpl({ title: 'foo' });
33 | });
34 | });
35 |
36 | test('evil expression 2', async t => {
37 | t.throws(() => {
38 | const tpl = htmlTemplate(`{{ this.alert(422) }}`);
39 | tpl({ title: 'foo' });
40 | });
41 | });
42 |
43 | test('evil html', async t => {
44 | const tpl = htmlTemplate(`{{ title }} `);
45 | t.is(tpl({ title: 'foo' }), "foo alert('you are hacked')");
46 | });
47 |
48 | test('evil expr + html', async t => {
49 | const tpl = htmlTemplate(`{{ col1 }} alert('you are hacked') {{col2}}`);
50 | t.is(tpl({ col1: '' }), " alert('you are hacked') ");
51 | });
52 |
53 | test('expressions in style attributes', async t => {
54 | const tpl = htmlTemplate(`foo`);
55 | t.is(tpl({ col1: 123 }), 'foo');
56 | });
57 |
58 | test('cluster tooltips', async t => {
59 | const tpl = (tpl, ctx) => {
60 | tpl = htmlTemplate(tpl);
61 | return tpl(ctx);
62 | };
63 | const ctx = {
64 | symbols: [
65 | {
66 | ID: 'USA',
67 | value: 1234
68 | },
69 | {
70 | ID: 'Canada',
71 | value: 1876
72 | },
73 | {
74 | ID: 'Mexico',
75 | value: 234
76 | }
77 | ]
78 | };
79 | ctx.first = ctx.symbols[0];
80 | t.is(tpl(`{{ first.ID }}`, ctx), 'USA');
81 | t.is(tpl(`{{ JOIN(PLUCK(symbols, 'ID'), ' and ') }}`, ctx), 'USA and Canada and Mexico');
82 | t.is(tpl(`{{ JOIN(PLUCK(symbols, 'ID'), ', ', ' and ') }}`, ctx), 'USA, Canada and Mexico');
83 | t.is(
84 | tpl(
85 | `The countries are {{ JOIN(MAP(f(a) = CONCAT('',a,''), PLUCK(symbols, 'ID')), ', ', ', and ') }}!`,
86 | ctx
87 | ),
88 | 'The countries are USA, Canada, and Mexico!'
89 | );
90 | t.is(
91 | tpl(
92 | `There are {{ LENGTH(symbols) }} countries and their names are {{ JOIN(MAP(f(a) = CONCAT('',a,''), PLUCK(symbols, 'ID')), ', ', ' and ') }}!`,
93 | ctx
94 | ),
95 | 'There are 3 countries and their names are USA, Canada and Mexico!'
96 | );
97 | t.is(
98 | tpl(
99 | `There are {{ LENGTH(symbols) }} countries and their names are {{ JOIN(MAP(f(a) = CONCAT('',a.ID,''), symbols), ', ', ' and ') }}!`,
100 | ctx
101 | ),
102 | 'There are 3 countries and their names are USA, Canada and Mexico!'
103 | );
104 | t.is(
105 | tpl(`The biggest value is in {{ (SORT(symbols, FALSE, 'value'))[0].ID }}!`, ctx),
106 | 'The biggest value is in Canada!'
107 | );
108 | });
109 |
110 | test('null expressions converted to empty strings', async t => {
111 | const tpl = htmlTemplate(`hello {{ value }}`);
112 | t.is(tpl({ value: null }), 'hello ');
113 | t.is(tpl({ value: false }), 'hello false');
114 | });
115 |
116 | test('style tags are removed', async t => {
117 | const tpl = htmlTemplate(`hello {{ value }}`);
118 | t.is(tpl({ value: 'world' }), 'hello div { background: yellow } world');
119 | });
120 |
--------------------------------------------------------------------------------
/lib/dw/utils/index.mjs:
--------------------------------------------------------------------------------
1 | import purifyHtml from '@datawrapper/shared/purifyHtml.js';
2 | import column from '../dataset/column.mjs';
3 | import significantDimension from '@datawrapper/shared/significantDimension.js';
4 | import tailLength from '@datawrapper/shared/tailLength.js';
5 | import round from '@datawrapper/shared/round.js';
6 | import smartRound from '@datawrapper/shared/smartRound.js';
7 | import equalish from '@datawrapper/shared/equalish.js';
8 | import clone from '@datawrapper/shared/clone.js';
9 | import { outerHeight, getNonChartHeight } from './getNonChartHeight.mjs';
10 | import htmlTemplate from './htmlTemplate.mjs';
11 | import { isFunction, isString, range } from 'underscore';
12 |
13 | export {
14 | purifyHtml,
15 | significantDimension,
16 | tailLength,
17 | round,
18 | smartRound,
19 | equalish,
20 | clone,
21 | getNonChartHeight,
22 | outerHeight,
23 | htmlTemplate
24 | };
25 |
26 | /*
27 | * returns the min/max range of a set of columns
28 | */
29 | export function minMax(columns) {
30 | const minmax = [Number.MAX_VALUE, -Number.MAX_VALUE];
31 | columns.forEach(column => {
32 | minmax[0] = Math.min(minmax[0], column.range()[0]);
33 | minmax[1] = Math.max(minmax[1], column.range()[1]);
34 | });
35 | return minmax;
36 | }
37 |
38 | /*
39 | * returns a new column with all column names as values
40 | */
41 | export function columnNameColumn(columns) {
42 | const names = columns.map(col => col.title());
43 | return column('', names);
44 | }
45 |
46 | export function name(obj) {
47 | return isFunction(obj.name) ? obj.name() : isString(obj.name) ? obj.name : obj;
48 | }
49 |
50 | export function getMaxChartHeight() {
51 | if (window.innerHeight === 0) return 0;
52 | var maxH = window.innerHeight - getNonChartHeight();
53 | return Math.max(maxH, 0);
54 | }
55 |
56 | export function nearest(array, value) {
57 | let minDiff = Number.MAX_VALUE;
58 | let minDiffVal;
59 | array.forEach(v => {
60 | var d = Math.abs(v - value);
61 | if (d < minDiff) {
62 | minDiff = d;
63 | minDiffVal = v;
64 | }
65 | });
66 | return minDiffVal;
67 | }
68 |
69 | export function metricSuffix(locale) {
70 | switch (locale.substr(0, 2).toLowerCase()) {
71 | case 'de':
72 | return { 3: ' Tsd.', 6: ' Mio.', 9: ' Mrd.', 12: ' Bio.' };
73 | case 'fr':
74 | return { 3: ' mil', 6: ' Mio', 9: ' Mrd' };
75 | case 'es':
76 | return { 3: ' Mil', 6: ' millón' };
77 | default:
78 | return { 3: 'k', 6: 'M', 9: ' bil' };
79 | }
80 | }
81 |
82 | export function magnitudeRange(minmax) {
83 | return (
84 | Math.round(Math.log(minmax[1]) / Math.LN10) - Math.round(Math.log(minmax[0]) / Math.LN10)
85 | );
86 | }
87 |
88 | export function logTicks(min, max) {
89 | const e0 = Math.round(Math.log(min) / Math.LN10);
90 | const e1 = Math.round(Math.log(max) / Math.LN10);
91 | return range(e0, e1).map(exp => Math.pow(10, exp));
92 | }
93 |
94 | export function height(element) {
95 | const h = parseFloat(getComputedStyle(element, null).height.replace('px', ''));
96 | return isNaN(h) ? 0 : h;
97 | }
98 |
99 | export function width(element) {
100 | const w = parseFloat(getComputedStyle(element, null).width.replace('px', ''));
101 | return isNaN(w) ? 0 : w;
102 | }
103 |
104 | export function addClass(element, className) {
105 | if (element) element.classList.add(className);
106 | }
107 |
108 | export function removeClass(element, className) {
109 | if (element) element.classList.remove(className);
110 | }
111 |
112 | export function remove(elementOrSelector) {
113 | const element =
114 | typeof elementOrSelector === 'string'
115 | ? document.querySelector(elementOrSelector)
116 | : elementOrSelector;
117 | if (element) element.parentElement.removeChild(element);
118 | }
119 |
120 | export function domReady(callback) {
121 | if (/complete|interactive|loaded/.test(document.readyState)) {
122 | // dom is already loaded
123 | callback();
124 | } else {
125 | // wait for dom to load
126 | window.addEventListener('DOMContentLoaded', () => {
127 | callback();
128 | });
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/lib/dw/utils/parser.test.mjs:
--------------------------------------------------------------------------------
1 | /*
2 | * test our own custom methods and operators for `expr-eval`
3 | */
4 | import test from 'ava';
5 | import { Parser } from './parser.mjs';
6 |
7 | const parser = new Parser();
8 |
9 | test('in operator', t => {
10 | const expr = parser.parse('needle in haystack ? "y" : "n"');
11 | t.is(expr.evaluate({ needle: 'foo', haystack: 'lot of foo here' }), 'y');
12 | t.is(expr.evaluate({ needle: 'bar', haystack: 'lot of foo here' }), 'n');
13 | });
14 |
15 | test('!operator', t => {
16 | const expr = parser.parse('!(needle in haystack) ? "y" : "n"');
17 | t.is(expr.evaluate({ needle: 'foo', haystack: 'lot of foo here' }), 'n');
18 | t.is(expr.evaluate({ needle: 'bar', haystack: 'lot of foo here' }), 'y');
19 | });
20 |
21 | test('not function', t => {
22 | const expr = parser.parse('NOT(needle in haystack) ? "y" : "n"');
23 | t.is(expr.evaluate({ needle: 'foo', haystack: 'lot of foo here' }), 'n');
24 | t.is(expr.evaluate({ needle: 'bar', haystack: 'lot of foo here' }), 'y');
25 | });
26 |
27 | test('substr function', t => {
28 | const expr = parser.parse('SUBSTR(name, 0, 5)');
29 | t.is(expr.evaluate({ name: 'Datawrapper' }), 'Dataw');
30 | });
31 |
32 | test('variable substr', t => {
33 | const expr = parser.parse('SUBSTR(name, start, len)');
34 | t.is(expr.evaluate({ name: 'Datawrapper', start: 0, len: 6 }), 'Datawr');
35 | t.is(expr.evaluate({ name: 'Datawrapper', start: 3, len: 6 }), 'awrapp');
36 | });
37 |
38 | test('concat function', t => {
39 | const expr = parser.parse('CONCAT(first, " ", last)');
40 | t.is(expr.evaluate({ first: 'Lorem', last: 'Ipsum' }), 'Lorem Ipsum');
41 | });
42 |
43 | test('trim function', t => {
44 | const expr = parser.parse('TRIM(name)');
45 | t.is(expr.evaluate({ name: ' spaces\t\t' }), 'spaces');
46 | t.is(parser.evaluate('TRIM " hello "'), 'hello');
47 | });
48 |
49 | test('trim function without brackets', t => {
50 | const expr = parser.parse('TRIM name');
51 | t.is(expr.evaluate({ name: ' spaces\t\t' }), 'spaces');
52 | });
53 |
54 | test('TRUE', t => {
55 | const expr = parser.parse('TRUE');
56 | t.is(expr.evaluate({ name: '' }), true);
57 | });
58 |
59 | test('NOT TRUE', t => {
60 | const expr = parser.parse('NOT TRUE');
61 | t.is(expr.evaluate({ name: '' }), false);
62 | });
63 |
64 | test('IF(TRUE', t => {
65 | const expr = parser.parse('IF(TRUE, "yes", "no")');
66 | t.is(expr.evaluate({ name: '' }), 'yes');
67 | });
68 |
69 | test('FALSE', t => {
70 | const expr = parser.parse('FALSE');
71 | t.is(expr.evaluate({ name: '' }), false);
72 | });
73 |
74 | test('TRUE and FALSE', t => {
75 | const expr = parser.parse('TRUE and FALSE');
76 | t.is(expr.evaluate({ name: '' }), false);
77 | });
78 |
79 | test('true', t => {
80 | const expr = parser.parse('true');
81 | t.is(expr.evaluate({ true: 12 }), 12);
82 | t.throws(() => expr.evaluate({ foo: 12 }));
83 | });
84 |
85 | test('ABS()', t => {
86 | const expr = parser.parse('ABS(a)');
87 | t.is(expr.evaluate({ a: -12 }), 12);
88 | t.is(expr.evaluate({ a: 10 }), 10);
89 | });
90 |
91 | test('ABS', t => {
92 | const expr = parser.parse('ABS a');
93 | t.is(expr.evaluate({ a: -12 }), 12);
94 | t.is(expr.evaluate({ a: 10 }), 10);
95 | });
96 |
97 | test('ROUND()', t => {
98 | const expr = parser.parse('ROUND(a)');
99 | t.is(expr.evaluate({ a: -12.345 }), -12);
100 | t.is(expr.evaluate({ a: 10.56 }), 11);
101 | });
102 |
103 | test('ROUND(a,1)', t => {
104 | const expr = parser.parse('ROUND(a, 1)');
105 | t.is(expr.evaluate({ a: -12.345 }), -12.3);
106 | t.is(expr.evaluate({ a: 10.56 }), 10.6);
107 | });
108 |
109 | test('year() function', t => {
110 | const expr = parser.parse('YEAR(date)');
111 | t.is(expr.evaluate({ date: new Date(2020, 1, 1) }), 2020);
112 | t.is(expr.evaluate({ date: new Date(2017, 4, 14) }), 2017);
113 | t.is(expr.evaluate({ date: '2017-06-23' }), 2017);
114 | });
115 |
116 | test('month() function', t => {
117 | const expr = parser.parse('MONTH(date)');
118 | t.is(expr.evaluate({ date: new Date(2020, 1, 1) }), 2);
119 | t.is(expr.evaluate({ date: new Date(2017, 4, 14) }), 5);
120 | });
121 |
122 | test('day() function', t => {
123 | const expr = parser.parse('DAY(date)');
124 | t.is(expr.evaluate({ date: new Date(2020, 1, 1) }), 1);
125 | t.is(expr.evaluate({ date: new Date(2017, 4, 14) }), 14);
126 | });
127 |
128 | test('hours() function', t => {
129 | const expr = parser.parse('HOURS(date)');
130 | t.is(expr.evaluate({ date: new Date(2020, 1, 1) }), 0);
131 | t.is(expr.evaluate({ date: new Date(2017, 4, 14, 18, 30, 5) }), 18);
132 | });
133 |
134 | test('sin a^b', t => {
135 | const expr = parser.parse('SIN a^b');
136 | t.is(expr.evaluate({ a: 4, b: 2 }), Math.sin(Math.pow(4, 2)));
137 | });
138 |
139 | test('(sin a)^b', t => {
140 | const expr = parser.parse('(SIN a)^b');
141 | t.is(expr.evaluate({ a: 4, b: 2 }), Math.pow(Math.sin(4), 2));
142 | });
143 |
144 | test('sin(a)', t => {
145 | const expr = parser.parse('SIN(a)');
146 | t.is(expr.evaluate({ a: 4 }), Math.sin(4));
147 | });
148 |
149 | test('v1+v2+v3', t => {
150 | const expr = parser.parse('v1+v2+v3');
151 | t.is(expr.evaluate({ v1: 1, v2: 2, v3: 3 }), 6);
152 | });
153 |
154 | test('REPLACE()', t => {
155 | const expr = parser.parse('REPLACE(REPLACE(country, "Yemen", ":ye:"), "Zambia", ":zm:")');
156 | t.is(expr.evaluate({ country: 'Yemen' }), ':ye:');
157 | t.is(expr.evaluate({ country: 'Zambia' }), ':zm:');
158 | t.is(parser.evaluate('REPLACE("Hello world", "o", "a")'), 'Hella warld');
159 | t.is(parser.evaluate('REPLACE("Hello world", "o", f(d) = UPPER(d))'), 'HellO wOrld');
160 | t.is(parser.evaluate('REPLACE("Hello world", "o", UPPER)'), 'HellO wOrld');
161 | t.is(parser.evaluate('REPLACE("12.4", ".", ",")'), '12,4');
162 | t.is(parser.evaluate('REPLACE("12.4", "[0-9]", "x")'), '12.4');
163 | t.is(parser.evaluate('REPLACE_REGEX("12.4", "[0-9]", "x")'), 'xx.x');
164 | t.is(parser.evaluate('REPLACE_REGEX("12.4", "[0-9]+", "x")'), 'x.x');
165 | t.is(parser.evaluate('REPLACE_REGEX("12.4", "[0-9]", f(d) = d*2)'), '24.8');
166 | });
167 |
168 | test('ISNULL', t => {
169 | const expr = parser.parse('ISNULL value');
170 | t.is(expr.evaluate({ value: 12 }), false);
171 | t.is(expr.evaluate({ value: 0 }), false);
172 | t.throws(() => expr.evaluate({ value: undefined }));
173 | t.is(expr.evaluate({ value: null }), true);
174 | });
175 |
176 | test('FORMAT', t => {
177 | const expr = parser.parse('FORMAT(value, "0.00")');
178 | const FORMAT = (val, format) => {
179 | if (format === '0.00') return val.toFixed(2);
180 | return val.toFixed();
181 | };
182 | t.is(expr.evaluate({ value: 12, FORMAT }), '12.00');
183 | });
184 |
185 | test('ARRAY IN', t => {
186 | const expr = parser.parse('"are" in SPLIT(str, " ")');
187 | t.is(expr.evaluate({ str: 'charts are cool' }), true);
188 | t.is(expr.evaluate({ str: "charts aren't cool" }), false);
189 | });
190 |
191 | test('SPLIT', t => {
192 | const expr = parser.parse('(SPLIT(region, ", "))[0]');
193 | t.is(expr.evaluate({ region: 'Berlin, Germany' }), 'Berlin');
194 | });
195 |
196 | test('SPLIT + JOIN', t => {
197 | const expr = parser.parse('JOIN(SPLIT(region, ", "), " = ")');
198 | t.is(expr.evaluate({ region: 'Berlin, Germany' }), 'Berlin = Germany');
199 | });
200 |
201 | test('INDEXOF', t => {
202 | t.is(parser.evaluate('INDEXOF(text, "two")', { text: 'one,two,three' }), 4);
203 | t.is(parser.evaluate('INDEXOF(SPLIT(text, ","), "two")', { text: 'one,two,three' }), 1);
204 | });
205 |
206 | test('TEXT', t => {
207 | t.is(parser.evaluate('TEXT num', { num: 123456 }), '123456');
208 | t.is(parser.evaluate('TEXT(num)', { num: 123456 }), '123456');
209 | });
210 |
211 | test('NUMBER', t => {
212 | t.is(parser.evaluate('NUMBER str', { str: '123456' }), 123456);
213 | t.is(parser.evaluate('NUMBER(str)', { str: '123456' }), 123456);
214 | });
215 |
216 | test('NA', t => {
217 | t.is(parser.evaluate('NA'), Number.NaN);
218 | t.is(parser.evaluate('NULL'), Number.NaN);
219 | });
220 |
221 | test('DATE', t => {
222 | t.deepEqual(parser.evaluate('DATE()'), new Date());
223 | t.deepEqual(parser.evaluate('DATE(0)'), new Date(0));
224 | t.deepEqual(parser.evaluate('DATE(str)', { str: '2018/01/20' }), new Date('2018/01/20'));
225 | t.deepEqual(parser.evaluate('DATE(2020,5,6)'), new Date(2020, 4, 6));
226 | });
227 |
228 | test('WEEKDAY', t => {
229 | t.is(parser.evaluate('WEEKDAY(DATE(2020,5,10))'), 0);
230 | t.is(parser.evaluate('WEEKDAY(DATE(2020,5,11))'), 1);
231 | t.is(parser.evaluate('WEEKDAY(DATE(2020,5,12))'), 2);
232 | });
233 |
234 | test('DATEDIFF', t => {
235 | t.is(parser.evaluate('DATEDIFF("2020-05-01", "2020-05-02")'), 1);
236 | t.is(parser.evaluate('DATEDIFF("2020-05-05", "2020-05-02")'), -3);
237 | });
238 |
239 | test('TIMEDIFF', t => {
240 | t.is(parser.evaluate('TIMEDIFF("2020-05-01 10:00:00", "2020-05-01 10:00:05")'), 5);
241 | t.is(parser.evaluate('TIMEDIFF("2020-05-01 10:00:00", "2020-05-01 10:01:00")'), 60);
242 | t.is(parser.evaluate('TIMEDIFF("2020-05-01 10:00:00", "2020-05-01 12:00:00")'), 7200);
243 | });
244 |
245 | test('custom functions', t => {
246 | t.deepEqual(parser.evaluate('f(x) = x > 2; FILTER(f, [1, 2, 0, 3, -1, 5])'), [3, 5]);
247 | t.deepEqual(parser.evaluate('FILTER(f(x) = x > 2, [1, 2, 0, 3, -1, 5])'), [3, 5]);
248 | });
249 |
250 | const symbols = [
251 | {
252 | ID: 'USA',
253 | value: 1234
254 | },
255 | {
256 | ID: 'Canada',
257 | value: 876
258 | },
259 | {
260 | ID: 'Mexico',
261 | value: 7390
262 | }
263 | ];
264 |
265 | test('PLUCK', t => {
266 | const ctx = { symbols };
267 | t.deepEqual(parser.evaluate('PLUCK(symbols, "value")', ctx), [1234, 876, 7390]);
268 | t.deepEqual(parser.evaluate('PLUCK(symbols, "ID")', ctx), ['USA', 'Canada', 'Mexico']);
269 | t.deepEqual(parser.evaluate('PLUCK(symbols, "constructor")', ctx), [null, null, null]);
270 | });
271 |
272 | test('PLUCK + JOIN', t => {
273 | const ctx = { symbols };
274 | t.deepEqual(
275 | parser.evaluate(`JOIN(PLUCK(symbols, 'ID'), ' and ')`, ctx),
276 | 'USA and Canada and Mexico'
277 | );
278 | t.deepEqual(
279 | parser.evaluate(`JOIN(PLUCK(symbols, 'ID'), ', ', ' and ')`, ctx),
280 | 'USA, Canada and Mexico'
281 | );
282 | });
283 |
284 | test('PLUCK + MAP + JOIN', t => {
285 | const ctx = { symbols };
286 | t.deepEqual(
287 | parser.evaluate(
288 | `JOIN(MAP(f(a) = CONCAT('',a,''),PLUCK(symbols, 'ID')), ', ', ' and ')`,
289 | ctx
290 | ),
291 | 'USA, Canada and Mexico'
292 | );
293 | });
294 |
295 | test('SORT', t => {
296 | const ctx = { symbols };
297 | t.is(parser.evaluate(`(SORT(symbols, FALSE, 'value'))[0].ID`, ctx), 'Mexico');
298 | t.is(parser.evaluate(`(SORT(symbols, TRUE, 'value'))[0].ID`, ctx), 'Canada');
299 | t.is(parser.evaluate(`(SORT(symbols, TRUE, f(d) = d.value))[0].ID`, ctx), 'Canada');
300 | t.is(parser.evaluate(`(PLUCK(SORT(symbols, TRUE, 'value'), 'ID'))[0]`, ctx), 'Canada');
301 | });
302 |
303 | test('Member-access', t => {
304 | const ctx = { symbols };
305 | t.is(parser.evaluate(`symbols[0].ID`, ctx), 'USA');
306 | });
307 |
308 | test('RANGE', t => {
309 | t.deepEqual(parser.evaluate(`RANGE(5)`), [0, 1, 2, 3, 4]);
310 | t.deepEqual(parser.evaluate(`MAP(f(x) = x*x, RANGE(5))`), [0, 1, 4, 9, 16]);
311 | });
312 |
313 | test('FOLD', t => {
314 | t.deepEqual(parser.evaluate(`FOLD(MAX, 0, [0,1,2,3,4,5])`), 5);
315 | t.deepEqual(parser.evaluate(`FOLD(MIN, 10, [0,1,2,3,4,5])`), 0);
316 | t.deepEqual(parser.evaluate(`FOLD(f(a,b) = a * b, 1, [1,2,3,4,5])`), 120);
317 | });
318 |
319 | test('UPPER, LOWER, PROPER, TITLE', t => {
320 | t.is(parser.evaluate(`UPPER("hellO worLd")`), 'HELLO WORLD');
321 | t.is(parser.evaluate(`LOWER("HellO WorLd")`), 'hello world');
322 | t.is(parser.evaluate(`PROPER("HellO WorLd")`), 'Hello World');
323 | t.is(parser.evaluate(`TITLE("HellO WorLd")`), 'Hello World');
324 | t.is(parser.evaluate(`TITLE("2-way street")`), '2-way Street');
325 | t.is(parser.evaluate(`PROPER("ämilian der schöpfer")`), 'Ämilian Der Schöpfer');
326 | t.is(parser.evaluate(`TITLE("ämilian der schöpfer")`), 'Ämilian Der Schöpfer');
327 | t.is(parser.evaluate(`PROPER("baron lloyd-webber")`), 'Baron Lloyd-Webber');
328 | t.is(parser.evaluate(`TITLE("baron lloyd-webber")`), 'Baron Lloyd-webber');
329 | t.is(parser.evaluate(`PROPER("2-way street")`), '2-Way Street');
330 | t.is(parser.evaluate(`PROPER("rgb2csv")`), 'Rgb2Csv');
331 | t.is(parser.evaluate(`TITLE("rgb2csv")`), 'Rgb2csv');
332 | t.is(parser.evaluate(`TITLE 'heLLo wORld'`), 'Hello World');
333 | t.is(parser.evaluate(`UPPER 'heLLo wORld'`), 'HELLO WORLD');
334 | t.is(parser.evaluate(`LOWER 'heLLo wORld'`), 'hello world');
335 | t.is(parser.evaluate(`PROPER 'heLLo wORld'`), 'Hello World');
336 | });
337 |
338 | test('SUM', t => {
339 | t.is(parser.evaluate(`SUM(1,2,3,4)`), 10);
340 | t.is(parser.evaluate(`SUM([1,2,3,4])`), 10);
341 | t.is(parser.evaluate(`SUM([1,2,3,4,"foo"])`), 10);
342 | t.is(parser.evaluate(`SUM([1,2,3,4,1/0])`), 10);
343 | t.is(parser.evaluate(`SUM([1,2,FALSE,3,4,1/0])`), 10);
344 | });
345 |
346 | test('MEAN', t => {
347 | t.is(parser.evaluate(`MEAN([1,2,3,4,10])`), 4);
348 | t.is(parser.evaluate(`MEAN(1,2,3,4,10)`), 4);
349 | t.is(parser.evaluate(`MEAN([1,2,3,4,"foo"])`), 2.5);
350 | t.is(parser.evaluate(`MEAN([1,2,3,4,1/0])`), 2.5);
351 | });
352 |
353 | test('MEDIAN', t => {
354 | t.is(parser.evaluate(`MEDIAN(1,2,3,4,10)`), 3);
355 | t.is(parser.evaluate(`MEDIAN([1,2,3,4,10])`), 3);
356 | t.is(parser.evaluate(`MEDIAN([1,2,3,4,"foo"])`), 2.5);
357 | t.is(parser.evaluate(`MEDIAN([1,2,3,4,1/0])`), 2.5);
358 | });
359 |
360 | test('MIN', t => {
361 | t.is(parser.evaluate(`MIN([1,2,3,4,10])`), 1);
362 | t.is(parser.evaluate(`MIN(2,3,4,10)`), 2);
363 | t.is(parser.evaluate(`MIN([1,2,-3,4,"foo"])`), -3);
364 | t.is(parser.evaluate(`MIN([1,2,3,4,1/0])`), 1);
365 | });
366 |
367 | test('MAX', t => {
368 | t.is(parser.evaluate(`MAX([1,2,3,4,10])`), 10);
369 | t.is(parser.evaluate(`MAX(-2,3,4,10)`), 10);
370 | t.is(parser.evaluate(`MAX([1,2,3,4,"foo"])`), 4);
371 | t.is(parser.evaluate(`MAX([1,2,3,4,1/0])`), 4);
372 | });
373 |
374 | test('SLICE', t => {
375 | t.deepEqual(parser.evaluate(`SLICE([1,2,3,4,5], 1)`), [2, 3, 4, 5]);
376 | t.deepEqual(parser.evaluate(`SLICE([1,2,3,4,5], 1,3)`), [2, 3]);
377 | t.deepEqual(parser.evaluate(`SLICE([1,2,3,4,5], -2)`), [4, 5]);
378 | });
379 |
380 | test('FIND', t => {
381 | t.is(parser.evaluate(`FIND([1,2,3,4,5], f(x) = x>2)`), 3);
382 | });
383 |
384 | test('EVERY', t => {
385 | t.is(parser.evaluate(`EVERY([1,2,3,4,5], f(x) = x>2)`), false);
386 | t.is(parser.evaluate(`EVERY([1,2,3,4,5], f(x) = x>0)`), true);
387 | });
388 |
389 | test('SOME', t => {
390 | t.is(parser.evaluate(`SOME([1,2,3,4,5], f(x) = x>2)`), true);
391 | t.is(parser.evaluate(`SOME([1,2,3,4,5], f(x) = x>0)`), true);
392 | t.is(parser.evaluate(`SOME([1,2,3,4,5], f(x) = x>10)`), false);
393 | });
394 |
--------------------------------------------------------------------------------
/lib/dw/visualization.base.mjs:
--------------------------------------------------------------------------------
1 | /* globals dw */
2 |
3 | /*
4 | * Every visualization extends this class.
5 | * It provides the basic API between the chart editor
6 | * and the visualization render code.
7 | */
8 |
9 | import { extend, each, isEqual, isArray, filter, find, indexOf, map, range } from 'underscore';
10 | import get from '@datawrapper/shared/get.js';
11 | import clone from '@datawrapper/shared/clone.js';
12 | import column from './dataset/column.mjs';
13 | import { remove } from './utils/index.mjs';
14 |
15 | const base = function() {}.prototype;
16 |
17 | extend(base, {
18 | // called before rendering
19 | __init() {
20 | this.__renderedDfd = new Promise(resolve => {
21 | this.__renderedResolve = resolve;
22 | });
23 | this.__rendered = false;
24 | this.__colors = {};
25 | this.__callbacks = {};
26 |
27 | if (window.parent && window.parent.postMessage) {
28 | window.parent.postMessage('datawrapper:vis:init', '*');
29 | }
30 | return this;
31 | },
32 |
33 | render(el) {
34 | el.innerHTML = 'implement me!';
35 | },
36 |
37 | theme(theme) {
38 | if (!arguments.length) {
39 | if (typeof this.__theme === 'string') return dw.theme(this.__theme);
40 | return this.__theme;
41 | }
42 |
43 | this.__theme = theme;
44 | return this;
45 | },
46 |
47 | target(target) {
48 | if (!arguments.length) {
49 | return this.__target;
50 | }
51 |
52 | this.__target = target;
53 | return this;
54 | },
55 |
56 | size(width, height) {
57 | const me = this;
58 | if (!arguments.length) return [me.__w, me.__h];
59 | me.__w = width;
60 | me.__h = height;
61 | return me;
62 | },
63 |
64 | /**
65 | * short-cut for this.chart.get('metadata.visualize.*')
66 | */
67 | get(str, _default) {
68 | return get(this.chart().get(), 'metadata.visualize' + (str ? '.' + str : ''), _default);
69 | },
70 |
71 | chart(chart) {
72 | var me = this;
73 | if (!arguments.length) return me.__chart;
74 | me.dataset = chart.dataset();
75 | me.theme(chart.theme());
76 | me.__chart = chart;
77 | var columnFormat = get(chart.get(), 'metadata.data.column-format', {});
78 | var ignore = {};
79 | each(columnFormat, function(format, key) {
80 | ignore[key] = !!format.ignore;
81 | });
82 | if (me.dataset.filterColumns) me.dataset.filterColumns(ignore);
83 | return me;
84 | },
85 |
86 | axes(returnAsColumns, noCache) {
87 | const me = this;
88 | const userAxes = get(me.chart().get(), 'metadata.axes', {});
89 |
90 | if (!noCache && me.__axisCache && isEqual(me.__axisCache.userAxes, userAxes)) {
91 | return me.__axisCache[returnAsColumns ? 'axesAsColumns' : 'axes'];
92 | }
93 |
94 | const dataset = me.dataset;
95 | const usedColumns = {};
96 | const axes = {};
97 | const axesAsColumns = {};
98 |
99 | // get user preference
100 | each(me.meta.axes, (o, key) => {
101 | if (userAxes[key]) {
102 | let columns = userAxes[key];
103 | if (
104 | columnExists(columns) &&
105 | checkColumn(o, columns, true) &&
106 | !!o.multiple === isArray(columns)
107 | ) {
108 | axes[key] = o.multiple && !isArray(columns) ? [columns] : columns;
109 | // mark columns as used
110 | if (!isArray(columns)) columns = [columns];
111 | each(columns, function(column) {
112 | usedColumns[column] = true;
113 | });
114 | }
115 | }
116 | });
117 |
118 | var checked = [];
119 | // auto-populate remaining axes
120 | each(me.meta.axes, (axisDef, key) => {
121 | function remainingRequiredColumns(accepts) {
122 | // returns how many required columns there are for the remaining axes
123 | // either an integer or "multiple" if there's another multi-column axis coming up
124 | function equalAccepts(a1, a2) {
125 | if (typeof a1 === 'undefined' && typeof a2 !== 'undefined') return false;
126 | if (typeof a2 === 'undefined' && typeof a1 !== 'undefined') return false;
127 | if (a1.length !== a2.length) return false;
128 |
129 | for (let i = 0; i < a1.length; i++) {
130 | if (a2.indexOf(a1[i]) === -1) return false;
131 | }
132 | return true;
133 | }
134 |
135 | let res = 0;
136 | each(me.meta.axes, function(axisDef, key) {
137 | if (checked.indexOf(key) > -1) return;
138 | if (!equalAccepts(axisDef.accepts, accepts)) return;
139 | if (typeof res === 'string') return;
140 | if (axisDef.optional) return;
141 | if (axisDef.multiple) {
142 | res = 'multiple';
143 | return;
144 | }
145 | res += 1;
146 | });
147 | return res;
148 | }
149 | function remainingAvailableColumns(dataset) {
150 | let count = 0;
151 | dataset.eachColumn(c => {
152 | if (checkColumn(axisDef, c)) {
153 | count++;
154 | }
155 | });
156 | return count;
157 | }
158 | checked.push(key);
159 | if (axes[key]) return; // user has defined this axis already
160 | if (axisDef.optional) {
161 | // chart settings may override this
162 | if (
163 | axisDef.overrideOptionalKey &&
164 | get(me.chart().get(), 'metadata.' + axisDef.overrideOptionalKey, false)
165 | ) {
166 | // now the axis is mandatory
167 | axisDef.optional = false;
168 | }
169 | }
170 | if (!axisDef.optional) {
171 | // we only populate mandatory axes
172 | if (!axisDef.multiple) {
173 | const accepted = filter(dataset.columns(), c => checkColumn(axisDef, c));
174 | let firstMatch;
175 | if (axisDef.preferred) {
176 | // axis defined a regex for testing column names
177 | const regex = new RegExp(axisDef.preferred, 'i');
178 | firstMatch = find(accepted, function(col) {
179 | return (
180 | regex.test(col.name()) ||
181 | (col.title() !== col.name() && regex.test(col.title()))
182 | );
183 | });
184 | }
185 | // simply use first colulmn accepted by axis
186 | if (!firstMatch) firstMatch = accepted[0];
187 | if (firstMatch) {
188 | usedColumns[firstMatch.name()] = true; // mark column as used
189 | axes[key] = firstMatch.name();
190 | } else {
191 | // try to auto-populate missing text column
192 | if (indexOf(axisDef.accepts, 'text') >= 0) {
193 | // try using the first text column in the dataset instead
194 | const acceptedAllowUsed = filter(dataset.columns(), function(col) {
195 | return indexOf(axisDef.accepts, col.type()) >= 0;
196 | });
197 | if (acceptedAllowUsed.length) {
198 | axes[key] = acceptedAllowUsed[0].name();
199 | } else {
200 | // no other text column in dataset, so genetate one with A,B,C,D...
201 | const col = column(
202 | key,
203 | map(range(dataset.numRows()), function(i) {
204 | return (
205 | (i > 25 ? String.fromCharCode(64 + i / 26) : '') +
206 | String.fromCharCode(65 + (i % 26))
207 | );
208 | }),
209 | 'text'
210 | );
211 | dataset.add(col);
212 | me.chart().dataset(dataset);
213 | usedColumns[col.name()] = true;
214 | axes[key] = col.name();
215 | }
216 | }
217 | }
218 | } else {
219 | const required = remainingRequiredColumns(axisDef.accepts);
220 | let available = remainingAvailableColumns(dataset);
221 |
222 | // fill axis with all accepted columns
223 | axes[key] = [];
224 | dataset.eachColumn(function(c) {
225 | if (required === 'multiple' && axes[key].length) return;
226 | else if (available <= required) return;
227 |
228 | if (checkColumn(axisDef, c)) {
229 | usedColumns[c.name()] = true;
230 | axes[key].push(c.name());
231 | available--;
232 | }
233 | });
234 | }
235 | } else {
236 | axes[key] = false;
237 | }
238 | });
239 |
240 | each(axes, (columns, key) => {
241 | if (!isArray(columns)) {
242 | axesAsColumns[key] = columns !== false ? me.dataset.column(columns) : null;
243 | } else {
244 | axesAsColumns[key] = [];
245 | each(columns, function(column, i) {
246 | axesAsColumns[key][i] = column !== false ? me.dataset.column(column) : null;
247 | });
248 | }
249 | });
250 |
251 | me.__axisCache = {
252 | axes: axes,
253 | axesAsColumns: axesAsColumns,
254 | userAxes: clone(userAxes)
255 | };
256 |
257 | function columnExists(columns) {
258 | if (!isArray(columns)) columns = [columns];
259 | for (var i = 0; i < columns.length; i++) {
260 | if (!dataset.hasColumn(columns[i])) return false;
261 | }
262 | return true;
263 | }
264 |
265 | function checkColumn(axisDef, columns, allowMultipleUse) {
266 | if (!isArray(columns)) columns = [columns];
267 | columns = columns.map(el => (typeof el === 'string' ? dataset.column(el) : el));
268 | for (var i = 0; i < columns.length; i++) {
269 | if (
270 | (!allowMultipleUse && usedColumns[columns[i].name()]) ||
271 | indexOf(axisDef.accepts, columns[i].type()) === -1
272 | ) {
273 | return false;
274 | }
275 | }
276 | return true;
277 | }
278 |
279 | return me.__axisCache[returnAsColumns ? 'axesAsColumns' : 'axes'];
280 | },
281 |
282 | keys() {
283 | const axesDef = this.axes();
284 | if (axesDef.labels) {
285 | const lblCol = this.dataset.column(axesDef.labels);
286 | const keys = [];
287 | lblCol.each(val => {
288 | keys.push(String(val));
289 | });
290 | return keys;
291 | }
292 | return [];
293 | },
294 |
295 | keyLabel(key) {
296 | return key;
297 | },
298 |
299 | /*
300 | * called by the core whenever the chart is re-drawn
301 | * without reloading the page
302 | */
303 | reset() {
304 | this.clear();
305 | const el = this.target();
306 | el.innerHTML = '';
307 | remove('.chart .filter-ui');
308 | remove('.chart .legend');
309 | },
310 |
311 | clear() {},
312 |
313 | renderingComplete() {
314 | if (window.parent && window.parent.postMessage) {
315 | setTimeout(function() {
316 | window.parent.postMessage('datawrapper:vis:rendered', '*');
317 | }, 200);
318 | }
319 | this.__renderedResolve();
320 | this.__rendered = true;
321 | this.postRendering();
322 | },
323 |
324 | postRendering() {},
325 |
326 | rendered() {
327 | return this.__renderedDfd;
328 | },
329 |
330 | /*
331 | * smart rendering means that a visualization is able to
332 | * re-render itself without having to instantiate it again
333 | */
334 | supportsSmartRendering() {
335 | return false;
336 | },
337 |
338 | colorMode(cm) {
339 | if (!arguments.length) {
340 | return this.__colorMode;
341 | }
342 |
343 | this.__colorMode = cm;
344 | },
345 |
346 | colorMap(cm) {
347 | if (!arguments.length) {
348 | return color => {
349 | this.__colors[color] = 1;
350 | if (this.__colorMap) {
351 | return this.__colorMap(color);
352 | }
353 | return color;
354 | };
355 | }
356 |
357 | this.__colorMap = cm;
358 | },
359 |
360 | colorsUsed() {
361 | return Object.keys(this.__colors);
362 | },
363 |
364 | /**
365 | * register an event listener for custom vis events
366 | */
367 | on(eventType, callback) {
368 | if (!this.__callbacks[eventType]) {
369 | this.__callbacks[eventType] = [];
370 | }
371 | this.__callbacks[eventType].push(callback);
372 | },
373 |
374 | /**
375 | * fire a custom vis event
376 | */
377 | fire(eventType, data) {
378 | if (this.__callbacks && this.__callbacks[eventType]) {
379 | this.__callbacks[eventType].forEach(function(cb) {
380 | if (typeof cb === 'function') cb(data);
381 | });
382 | }
383 | }
384 | });
385 |
386 | export default base;
387 |
--------------------------------------------------------------------------------
/lib/dw/visualization.mjs:
--------------------------------------------------------------------------------
1 | import { clone } from 'underscore';
2 | import base from './visualization.base.mjs';
3 |
4 | const __vis = {};
5 |
6 | function visualization(id, target) {
7 | if (!__vis[id]) {
8 | console.warn('unknown visualization type: ' + id);
9 | const known = Object.keys(__vis);
10 | if (known.length > 0) console.warn('try one of these instead: ' + known.join(', '));
11 | return false;
12 | }
13 |
14 | function getParents(vis) {
15 | const parents = [];
16 |
17 | while (vis.parentVis !== 'base') {
18 | vis = __vis[vis.parentVis];
19 | parents.push({ id: vis.parentVis, vis });
20 | }
21 |
22 | return parents.reverse();
23 | }
24 |
25 | const vis = clone(base);
26 |
27 | const parents = getParents(__vis[id]);
28 | parents.push({ id, vis: __vis[id] });
29 | parents.forEach(el => {
30 | Object.assign(
31 | vis,
32 | typeof el.vis.init === 'function' ? el.vis.init({ target }) : el.vis.init,
33 | { id }
34 | );
35 | });
36 |
37 | if (target) {
38 | vis.target(target);
39 | }
40 |
41 | return vis;
42 | }
43 |
44 | visualization.register = function(id) {
45 | let parentVis, init;
46 |
47 | if (arguments.length === 2) {
48 | parentVis = 'base';
49 | init = arguments[1];
50 | } else if (arguments.length === 3) {
51 | parentVis = arguments[1];
52 | init = arguments[2];
53 | }
54 |
55 | __vis[id] = {
56 | parentVis,
57 | init
58 | };
59 | };
60 |
61 | visualization.has = function(id) {
62 | return __vis[id] !== undefined;
63 | };
64 |
65 | visualization.base = base;
66 |
67 | export default visualization;
68 |
--------------------------------------------------------------------------------
/lib/embed.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This is the JS code we ship with the "responsive" embed codes.
3 | * It's main purpose is to catch the postMessage calls for automatic resizing etc.
4 | * When you update this, make sure to upload new versions to http://datawrapper.dwcdn.net/lib/embed.js
5 | * and https://datawrapper.dwcdn.net/lib/embed.min.js
6 | */
7 | window.addEventListener('message', function(event) {
8 | if (typeof event.data['datawrapper-height'] !== 'undefined') {
9 | var iframes = document.querySelectorAll('iframe');
10 | for (var chartId in event.data['datawrapper-height']) {
11 | for (var i = 0; i < iframes.length; i++) {
12 | if (iframes[i].contentWindow === event.source) {
13 | var frame = iframes[i];
14 | frame.style.height = event.data['datawrapper-height'][chartId] + 'px';
15 | }
16 | }
17 | }
18 | }
19 | });
20 |
--------------------------------------------------------------------------------
/lib/shared.mjs:
--------------------------------------------------------------------------------
1 | import purifyHtml from '@datawrapper/shared/purifyHtml.js';
2 |
3 | export function clean(s) {
4 | return purifyHtml(s, '
');
5 | }
6 |
--------------------------------------------------------------------------------
/load-polyfills.js:
--------------------------------------------------------------------------------
1 | import { getBrowser, availablePolyfills } from '@datawrapper/polyfills';
2 | const { browser, version } = getBrowser();
3 | const { polyfillUri } = window.__DW_SVELTE_PROPS__;
4 |
5 | if (browser && availablePolyfills[browser] && version >= availablePolyfills[browser][0]) {
6 | if (version <= availablePolyfills[browser][1]) {
7 | document.write(
8 | ``
9 | );
10 | }
11 | } else {
12 | document.write(``);
13 | }
14 |
--------------------------------------------------------------------------------
/main.mjs:
--------------------------------------------------------------------------------
1 | import VisualizationIframe from './lib/VisualizationIframe.svelte';
2 |
3 | function render() {
4 | const target = document.getElementById('__svelte-dw');
5 | /* eslint-disable no-new */
6 | new VisualizationIframe({
7 | target,
8 | props: {
9 | ...window.__DW_SVELTE_PROPS__,
10 | outerContainer: target
11 | },
12 | hydrate: true
13 | });
14 | }
15 |
16 | render();
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@datawrapper/chart-core",
3 | "version": "8.41.1",
4 | "description": "Svelte component to render charts. Used by Sapper App and Node API.",
5 | "main": "index.js",
6 | "engines": {
7 | "node": ">=14.0.0",
8 | "npm": ">=7.0.0"
9 | },
10 | "files": [
11 | "dist",
12 | "lib"
13 | ],
14 | "scripts": {
15 | "format": "prettier 'lib/**/*.{js,mjs,html,svelte}' --write",
16 | "build": "rollup -c rollup.config.js",
17 | "dev": "rollup -cw rollup.config.js",
18 | "lint": "prettier --check 'lib/**/*.{js,html,mjs,svelte}' && eslint 'lib/**/*.{js,mjs,svelte}'",
19 | "prepublishOnly": "npm run build",
20 | "prepare": "npm run build",
21 | "test": "ava",
22 | "docs:parser": "jsdoc2md --template docs/.tpl.hbs --files 'lib/dw/utils/parser.js' -g grouped | sed '/\\*\\*Kind\\*\\*/d' | sed '/\\*\\*Example\\*\\*/d' | sed '/## $/d' | sed 's/## \\([a-z]\\)/### \\1/' > docs/parser.md && node docs/.fix.js parser.md"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/datawrapper/chart-core.git"
27 | },
28 | "author": "",
29 | "bugs": {
30 | "url": "https://github.com/datawrapper/chart-core/issues"
31 | },
32 | "homepage": "https://github.com/datawrapper/chart-core#readme",
33 | "dependencies": {
34 | "@datawrapper/expr-eval": "^2.0.4",
35 | "@datawrapper/polyfills": "2.1.1",
36 | "@datawrapper/shared": "^0.35.1",
37 | "@emotion/css": "^11.1.3",
38 | "core-js": "3.6.5",
39 | "deepmerge": "^4.2.2",
40 | "fontfaceobserver": "2.1.0",
41 | "svelte-extras": "^2.0.2",
42 | "svelte2": "npm:svelte@^2.16.1",
43 | "underscore": "^1.13.1"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "7.9.0",
47 | "@babel/plugin-transform-runtime": "7.9.0",
48 | "@babel/preset-env": "7.9.5",
49 | "@babel/runtime": "7.9.2",
50 | "@datawrapper/eslint-config": "^0.2.2",
51 | "@rollup/plugin-alias": "^3.1.2",
52 | "@rollup/plugin-commonjs": "11.0.2",
53 | "@rollup/plugin-node-resolve": "7.1.3",
54 | "@rollup/plugin-replace": "~2.3.2",
55 | "ava": "^3.8.1",
56 | "babel-eslint": "10.1.0",
57 | "babel-plugin-transform-async-to-promises": "0.8.15",
58 | "eslint": "~6.8.0",
59 | "eslint-plugin-svelte3": "~2.7.3",
60 | "husky": "~4.2.5",
61 | "jsdoc-to-markdown": "^5.0.0",
62 | "lint-staged": "^10.1.7",
63 | "node-fetch": "^2.6.0",
64 | "prettier": "~1.19.1",
65 | "prettier-plugin-svelte": "~0.7.0",
66 | "rollup": "2.6.1",
67 | "rollup-plugin-babel": "4.4.0",
68 | "rollup-plugin-svelte": "5.2.1",
69 | "rollup-plugin-terser": "^7.0.0",
70 | "svelte": "3.23.2"
71 | },
72 | "ava": {
73 | "nodeArguments": [
74 | "--experimental-specifier-resolution=node"
75 | ]
76 | },
77 | "husky": {
78 | "hooks": {
79 | "pre-commit": "lint-staged"
80 | }
81 | },
82 | "lint-staged": {
83 | "*.{js,svelte}": [
84 | "prettier --write",
85 | "eslint"
86 | ]
87 | },
88 | "prettier": {
89 | "arrowParens": "avoid",
90 | "printWidth": 100,
91 | "semi": true,
92 | "singleQuote": true,
93 | "tabWidth": 4,
94 | "trailingComma": "none"
95 | },
96 | "eslintIgnore": [
97 | "vendor",
98 | "dist",
99 | "dw.js"
100 | ],
101 | "eslintConfig": {
102 | "parser": "babel-eslint",
103 | "plugins": [
104 | "svelte3"
105 | ],
106 | "env": {
107 | "browser": true,
108 | "es6": true
109 | },
110 | "overrides": [
111 | {
112 | "files": [
113 | "**/*.svelte"
114 | ],
115 | "processor": "svelte3/svelte3"
116 | }
117 | ],
118 | "extends": "@datawrapper/eslint-config"
119 | },
120 | "optionalDependencies": {
121 | "fsevents": "^2.3.2"
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | import path from 'path';
4 | import alias from '@rollup/plugin-alias';
5 | import resolve from '@rollup/plugin-node-resolve';
6 | import commonjs from '@rollup/plugin-commonjs';
7 | import replace from '@rollup/plugin-replace';
8 | import svelte from 'rollup-plugin-svelte';
9 | import babel from 'rollup-plugin-babel';
10 | import { terser } from 'rollup-plugin-terser';
11 |
12 | const production = !process.env.ROLLUP_WATCH;
13 |
14 | const output = {
15 | name: 'chart',
16 | dir: path.resolve(__dirname, 'dist'),
17 | compact: true
18 | };
19 |
20 | const babelConfig = {
21 | exclude: [/node_modules\/(?!(@datawrapper|svelte)\/).*/],
22 | extensions: ['.js', '.mjs', '.svelte']
23 | };
24 |
25 | function onwarn(warning, warn) {
26 | if (warning.code === 'EVAL') return;
27 | if (warning.code === 'MISSING_NAME_OPTION_FOR_IIFE_EXPORT') return;
28 | warn(warning);
29 | }
30 |
31 | module.exports = [
32 | {
33 | /* Client side Svelte Visualization Component */
34 | input: path.resolve(__dirname, 'main.mjs'),
35 | plugins: [
36 | svelte({ hydratable: true }),
37 | // for @emotion/css
38 | replace({
39 | 'process.env.NODE_ENV': JSON.stringify('production')
40 | }),
41 | resolve(),
42 | commonjs(),
43 | production &&
44 | babel({
45 | ...babelConfig,
46 | presets: [['@babel/env', { targets: '> 1%', corejs: 3, useBuiltIns: 'entry' }]],
47 | plugins: ['babel-plugin-transform-async-to-promises']
48 | }),
49 | production && terser()
50 | ],
51 | onwarn,
52 | output: {
53 | format: 'iife',
54 | entryFileNames: 'main.js',
55 | ...output
56 | }
57 | },
58 | {
59 | /* Server side rendered Svelte Visualization Component */
60 | input: path.resolve(__dirname, 'lib/Visualization.svelte'),
61 | plugins: [
62 | svelte({ generate: 'ssr', hydratable: true }),
63 | // for @emotion/css
64 | replace({
65 | 'process.env.NODE_ENV': JSON.stringify('production')
66 | }),
67 | resolve(),
68 | commonjs(),
69 | babel({
70 | ...babelConfig,
71 | presets: [['@babel/env', { targets: { node: true } }]]
72 | })
73 | ],
74 | onwarn,
75 | output: {
76 | format: 'umd',
77 | entryFileNames: 'Visualization_SSR.js',
78 | ...output
79 | }
80 | },
81 | {
82 | input: path.resolve(__dirname, 'load-polyfills.js'),
83 | plugins: [
84 | resolve(),
85 | commonjs(),
86 | babel({
87 | ...babelConfig,
88 | presets: [['@babel/env', { targets: '> 1%', corejs: 3, useBuiltIns: 'entry' }]],
89 | plugins: ['babel-plugin-transform-async-to-promises']
90 | }),
91 | production && terser()
92 | ],
93 | onwarn,
94 | output: {
95 | format: 'iife',
96 | entryFileNames: 'load-polyfills.js',
97 | ...output
98 | }
99 | },
100 | {
101 | input: path.resolve(__dirname, 'lib/embed.js'),
102 | plugins: [
103 | resolve(),
104 | commonjs(),
105 | babel({
106 | ...babelConfig,
107 | presets: [['@babel/env', { targets: '> 1%', corejs: 3, useBuiltIns: 'entry' }]],
108 | plugins: ['babel-plugin-transform-async-to-promises']
109 | }),
110 | production && terser()
111 | ],
112 | output: {
113 | name: 'embed',
114 | file: path.resolve(__dirname, 'dist/embed.js'),
115 | format: 'iife'
116 | }
117 | },
118 | {
119 | input: path.resolve(__dirname, 'lib/dw/index.mjs'),
120 | plugins: [
121 | resolve(),
122 | commonjs(),
123 | replace({
124 | __chartCoreVersion__: require('./package.json').version,
125 | 'process.env.NODE_ENV': JSON.stringify('production')
126 | }),
127 | babel({
128 | ...babelConfig,
129 | presets: [['@babel/env', { targets: '> 1%', corejs: 3, useBuiltIns: 'entry' }]],
130 | plugins: ['babel-plugin-transform-async-to-promises']
131 | }),
132 | production && terser()
133 | ],
134 | onwarn,
135 | output: {
136 | sourcemap: true,
137 | file: path.resolve(__dirname, 'dist/dw-2.0.min.js'),
138 | format: 'iife'
139 | }
140 | },
141 | {
142 | input: path.resolve(__dirname, 'lib/dw/index.mjs'),
143 | plugins: [
144 | alias({
145 | entries: [
146 | {
147 | find:
148 | '@emotion/css/create-instance/dist/emotion-css-create-instance.cjs.js',
149 | replacement: '@emotion/css/create-instance'
150 | }
151 | ]
152 | }),
153 | resolve({
154 | modulesOnly: true
155 | }),
156 | commonjs(),
157 | replace({
158 | __chartCoreVersion__: require('./package.json').version,
159 | 'process.env.NODE_ENV': JSON.stringify('production')
160 | })
161 | ],
162 | onwarn,
163 | output: {
164 | sourcemap: true,
165 | file: path.resolve(__dirname, 'dist/dw-2.0.cjs.js'),
166 | format: 'cjs'
167 | }
168 | }
169 | ];
170 |
--------------------------------------------------------------------------------
|