/** * vegaToDevExtreme.js * * Converts a Vega-Lite chart specification into a DevExtreme dxChart * configuration object. * * Supported Vega-Lite features: * - Mark types: bar, line, area, point, circle, square, tick, rule, arc, rect, trail * - Encodings: x, y, color, size, tooltip (theta/radius for pie) * - Transforms: aggregate, fold, filter (predicate form), bin * - Inline shorthand aggregation on the y encoding * - Layered specs (each layer becomes an additional series) * - title, width, height, color scale palette */ (function (global) { 'use strict'; // ââ Mark type â DevExtreme series type ââââââââââââââââââââââââââââââââââââ var MARK_TYPE_MAP = { bar : 'bar', line : 'line', area : 'area', point : 'scatter', circle : 'scatter', square : 'scatter', tick : 'scatter', rule : 'line', arc : 'pie', rect : 'bar', trail : 'spline', boxplot : 'candlestick' }; // ââ Vega-Lite encoding type â DevExtreme axis type âââââââââââââââââââââââââ var AXIS_TYPE_MAP = { quantitative : 'continuous', temporal : 'datetime', ordinal : 'discrete', nominal : 'discrete' }; // ââ d3 format â DevExtreme format âââââââââââââââââââââââââââââââââââââââââ var FORMAT_MAP = { 'd' : { type: 'decimal' }, 'f' : { type: 'fixedPoint', precision: 2 }, '.0f' : { type: 'fixedPoint', precision: 0 }, '.1f' : { type: 'fixedPoint', precision: 1 }, '.2f' : { type: 'fixedPoint', precision: 2 }, '.3f' : { type: 'fixedPoint', precision: 3 }, '$' : { type: 'currency', currency: 'USD' }, '$.2f' : { type: 'currency', currency: 'USD' }, '%' : { type: 'percent' }, '.0%' : { type: 'percent', precision: 0 }, '.1%' : { type: 'percent', precision: 1 }, '.2%' : { type: 'percent', precision: 2 }, 's' : { type: 'largeNumber' } }; // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ // Internal helpers // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ function getMarkString(mark) { return typeof mark === 'string' ? mark : (mark && mark.type) || 'line'; } function mapMarkType(mark) { return MARK_TYPE_MAP[getMarkString(mark)] || 'line'; } function mapFormat(fmt) { return fmt ? (FORMAT_MAP[fmt] || undefined) : undefined; } /** * Build an argument or value axis config from a Vega-Lite encoding channel. */ function buildAxisConfig(enc) { if (!enc) return {}; var axis = {}; // Title: explicit title > axis.title > field name var titleText = (enc.title !== undefined) ? enc.title : (enc.axis && enc.axis.title !== undefined) ? enc.axis.title : enc.field || null; if (titleText) { axis.title = { text: titleText }; } // Type var axisType = enc.scale && enc.scale.type === 'log' ? 'logarithmic' : AXIS_TYPE_MAP[enc.type] || undefined; if (axisType) axis.type = axisType; // Label format var fmt = enc.axis && enc.axis.format ? mapFormat(enc.axis.format) : undefined; if (fmt) axis.label = { format: fmt }; return axis; } // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ // Data transform processing // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ /** * Apply an array of Vega-Lite transforms to a data array. * Returns a new array; the original is not mutated. */ function applyTransforms(data, transforms) { var result = data.slice(); transforms.forEach(function (t) { // ââ fold ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ if (t.fold) { var foldFields = t.fold; var keyName = (t.as && t.as[0]) || 'key'; var valueName = (t.as && t.as[1]) || 'value'; var folded = []; result.forEach(function (row) { foldFields.forEach(function (field) { var newRow = Object.assign({}, row); newRow[keyName] = field; newRow[valueName] = row[field]; folded.push(newRow); }); }); result = folded; // ââ filter (predicate object form) ââââââââââââââââââââââââââââââââ } else if (t.filter && typeof t.filter === 'object' && !Array.isArray(t.filter)) { var f = t.filter; result = result.filter(function (row) { var v = row[f.field]; if (f.equal !== undefined) return v == f.equal; // intentional == if (f.gt !== undefined) return parseFloat(v) > parseFloat(f.gt); if (f.gte !== undefined) return parseFloat(v) >= parseFloat(f.gte); if (f.lt !== undefined) return parseFloat(v) < parseFloat(f.lt); if (f.lte !== undefined) return parseFloat(v) <= parseFloat(f.lte); if (f.range) return parseFloat(v) >= parseFloat(f.range[0]) && parseFloat(v) <= parseFloat(f.range[1]); if (f.oneOf) return f.oneOf.indexOf(v) !== -1; return true; }); // ââ aggregate / groupby âââââââââââââââââââââââââââââââââââââââââââ } else if (t.aggregate) { result = aggregateData(result, t.aggregate, t.groupby || []); // ââ bin âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ } else if (t.bin !== undefined) { var binField = t.field; var binAs = Array.isArray(t.as) ? t.as[0] : (t.as || binField + '_bin'); var step = (typeof t.bin === 'object') ? t.bin.step : undefined; if (step && binField) { result = result.map(function (row) { var newRow = Object.assign({}, row); var val = parseFloat(row[binField]); newRow[binAs] = Math.floor(val / step) * step; return newRow; }); } } // calculate and window transforms require eval / complex logic â skipped }); return result; } /** * Group data rows by groupby fields, then compute aggregations. */ function aggregateData(data, aggs, groupByFields) { var groups = {}; var groupOrder = []; data.forEach(function (row) { var key = groupByFields.map(function (f) { return row[f]; }).join('\x00'); if (!groups[key]) { groups[key] = { items: [], proto: {} }; groupByFields.forEach(function (f) { groups[key].proto[f] = row[f]; }); groupOrder.push(key); } groups[key].items.push(row); }); return groupOrder.map(function (key) { var group = groups[key]; var items = group.items; var outRow = Object.assign({}, group.proto); aggs.forEach(function (agg) { var field = agg.field; var op = agg.op; var as = agg.as || (op + '_' + field); switch (op) { case 'sum': outRow[as] = items.reduce(function (acc, r) { return acc + (parseFloat(r[field]) || 0); }, 0); break; case 'mean': case 'average': { var total = items.reduce(function (acc, r) { return acc + (parseFloat(r[field]) || 0); }, 0); outRow[as] = items.length ? total / items.length : 0; break; } case 'count': outRow[as] = items.length; break; case 'distinct': outRow[as] = new Set(items.map(function (r) { return r[field]; })).size; break; case 'min': outRow[as] = Math.min.apply(null, items.map(function (r) { return parseFloat(r[field]) || 0; })); break; case 'max': outRow[as] = Math.max.apply(null, items.map(function (r) { return parseFloat(r[field]) || 0; })); break; case 'median': { var sorted = items.map(function (r) { return parseFloat(r[field]) || 0; }) .sort(function (a, b) { return a - b; }); var mid = Math.floor(sorted.length / 2); outRow[as] = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; break; } case 'valid': outRow[as] = items.filter(function (r) { return r[field] != null && !isNaN(r[field]); }).length; break; default: outRow[as] = null; } }); return outRow; }); } // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ // Resolve the output field name for an encoding channel // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ function resolveOutputField(enc) { if (!enc) return null; if (enc.as) return enc.as; if (enc.aggregate) return enc.aggregate + '_' + (enc.field || 'value'); return enc.field || null; } // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ // Main converter // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ /** * Convert a Vega-Lite specification to a DevExtreme dxChart options object. * * @param {Object} vegaSpec Vega-Lite specification * @returns {Object} DevExtreme dxChart options * * @example * var opts = vegaToDevExtreme(spec); * $('