UNPKG

vega-label

Version:

Label layout transform for Vega dataflows.

876 lines (822 loc) 27.8 kB
import { Marks, textMetrics } from 'vega-scenegraph'; import { canvas } from 'vega-canvas'; import { rederive, Transform } from 'vega-dataflow'; import { inherits, error, array, isFunction } from 'vega-util'; // bit mask for getting first 2 bytes of alpha value const ALPHA_MASK = 0xff000000; function baseBitmaps($, data) { const bitmap = $.bitmap(); // when there is no base mark but data points are to be avoided (data || []).forEach(d => bitmap.set($(d.boundary[0]), $(d.boundary[3]))); return [bitmap, undefined]; } function markBitmaps($, baseMark, avoidMarks, labelInside, isGroupArea) { // create canvas const width = $.width, height = $.height, border = labelInside || isGroupArea, context = canvas(width, height).getContext('2d'), baseMarkContext = canvas(width, height).getContext('2d'), strokeContext = border && canvas(width, height).getContext('2d'); // render all marks to be avoided into canvas avoidMarks.forEach(items => draw(context, items, false)); draw(baseMarkContext, baseMark, false); if (border) { draw(strokeContext, baseMark, true); } // get canvas buffer, create bitmaps const buffer = getBuffer(context, width, height), baseMarkBuffer = getBuffer(baseMarkContext, width, height), strokeBuffer = border && getBuffer(strokeContext, width, height), layer1 = $.bitmap(), layer2 = border && $.bitmap(); // populate bitmap layers let x, y, u, v, index, alpha, strokeAlpha, baseMarkAlpha; for (y = 0; y < height; ++y) { for (x = 0; x < width; ++x) { index = y * width + x; alpha = buffer[index] & ALPHA_MASK; baseMarkAlpha = baseMarkBuffer[index] & ALPHA_MASK; strokeAlpha = border && strokeBuffer[index] & ALPHA_MASK; if (alpha || strokeAlpha || baseMarkAlpha) { u = $(x); v = $(y); if (!isGroupArea && (alpha || baseMarkAlpha)) layer1.set(u, v); // update interior bitmap if (border && (alpha || strokeAlpha)) layer2.set(u, v); // update border bitmap } } } return [layer1, layer2]; } function getBuffer(context, width, height) { return new Uint32Array(context.getImageData(0, 0, width, height).data.buffer); } function draw(context, items, interior) { if (!items.length) return; const type = items[0].mark.marktype; if (type === 'group') { items.forEach(group => { group.items.forEach(mark => draw(context, mark.items, interior)); }); } else { Marks[type].draw(context, { items: interior ? items.map(prepare) : items }); } } /** * Prepare item before drawing into canvas (setting stroke and opacity) * @param {object} source item to be prepared * @returns prepared item */ function prepare(source) { const item = rederive(source, {}); if (item.stroke && item.strokeOpacity !== 0 || item.fill && item.fillOpacity !== 0) { return { ...item, strokeOpacity: 1, stroke: '#000', fillOpacity: 0 }; } return item; } const DIV = 5, // bit shift from x, y index to bit vector array index MOD = 31, // bit mask for index lookup within a bit vector SIZE = 32, // individual bit vector size RIGHT0 = new Uint32Array(SIZE + 1), // left-anchored bit vectors, full -> 0 RIGHT1 = new Uint32Array(SIZE + 1); // right-anchored bit vectors, 0 -> full RIGHT1[0] = 0; RIGHT0[0] = ~RIGHT1[0]; for (let i = 1; i <= SIZE; ++i) { RIGHT1[i] = RIGHT1[i - 1] << 1 | 1; RIGHT0[i] = ~RIGHT1[i]; } function Bitmap (w, h) { const array = new Uint32Array(~~((w * h + SIZE) / SIZE)); function _set(index, mask) { array[index] |= mask; } function _clear(index, mask) { array[index] &= mask; } return { array: array, get: (x, y) => { const index = y * w + x; return array[index >>> DIV] & 1 << (index & MOD); }, set: (x, y) => { const index = y * w + x; _set(index >>> DIV, 1 << (index & MOD)); }, clear: (x, y) => { const index = y * w + x; _clear(index >>> DIV, ~(1 << (index & MOD))); }, getRange: (x, y, x2, y2) => { let r = y2, start, end, indexStart, indexEnd; for (; r >= y; --r) { start = r * w + x; end = r * w + x2; indexStart = start >>> DIV; indexEnd = end >>> DIV; if (indexStart === indexEnd) { if (array[indexStart] & RIGHT0[start & MOD] & RIGHT1[(end & MOD) + 1]) { return true; } } else { if (array[indexStart] & RIGHT0[start & MOD]) return true; if (array[indexEnd] & RIGHT1[(end & MOD) + 1]) return true; for (let i = indexStart + 1; i < indexEnd; ++i) { if (array[i]) return true; } } } return false; }, setRange: (x, y, x2, y2) => { let start, end, indexStart, indexEnd, i; for (; y <= y2; ++y) { start = y * w + x; end = y * w + x2; indexStart = start >>> DIV; indexEnd = end >>> DIV; if (indexStart === indexEnd) { _set(indexStart, RIGHT0[start & MOD] & RIGHT1[(end & MOD) + 1]); } else { _set(indexStart, RIGHT0[start & MOD]); _set(indexEnd, RIGHT1[(end & MOD) + 1]); for (i = indexStart + 1; i < indexEnd; ++i) _set(i, 0xffffffff); } } }, clearRange: (x, y, x2, y2) => { let start, end, indexStart, indexEnd, i; for (; y <= y2; ++y) { start = y * w + x; end = y * w + x2; indexStart = start >>> DIV; indexEnd = end >>> DIV; if (indexStart === indexEnd) { _clear(indexStart, RIGHT1[start & MOD] | RIGHT0[(end & MOD) + 1]); } else { _clear(indexStart, RIGHT1[start & MOD]); _clear(indexEnd, RIGHT0[(end & MOD) + 1]); for (i = indexStart + 1; i < indexEnd; ++i) _clear(i, 0); } } }, outOfBounds: (x, y, x2, y2) => x < 0 || y < 0 || y2 >= h || x2 >= w }; } function scaler (width, height, padding) { const ratio = Math.max(1, Math.sqrt(width * height / 1e6)), w = ~~((width + 2 * padding + ratio) / ratio), h = ~~((height + 2 * padding + ratio) / ratio), scale = _ => ~~((_ + padding) / ratio); scale.invert = _ => _ * ratio - padding; scale.bitmap = () => Bitmap(w, h); scale.ratio = ratio; scale.padding = padding; scale.width = width; scale.height = height; return scale; } function placeAreaLabelNaive ($, bitmaps, avoidBaseMark, markIndex) { const width = $.width, height = $.height; // try to place a label within an input area mark return function (d) { const items = d.datum.datum.items[markIndex].items, // area points n = items.length, // number of points textHeight = d.datum.fontSize, // label width textWidth = textMetrics.width(d.datum, d.datum.text); // label height let maxAreaWidth = 0, x1, x2, y1, y2, x, y, areaWidth; // for each area sample point for (let i = 0; i < n; ++i) { x1 = items[i].x; y1 = items[i].y; x2 = items[i].x2 === undefined ? x1 : items[i].x2; y2 = items[i].y2 === undefined ? y1 : items[i].y2; x = (x1 + x2) / 2; y = (y1 + y2) / 2; areaWidth = Math.abs(x2 - x1 + y2 - y1); if (areaWidth >= maxAreaWidth) { maxAreaWidth = areaWidth; d.x = x; d.y = y; } } x = textWidth / 2; y = textHeight / 2; x1 = d.x - x; x2 = d.x + x; y1 = d.y - y; y2 = d.y + y; d.align = 'center'; if (x1 < 0 && x2 <= width) { d.align = 'left'; } else if (0 <= x1 && width < x2) { d.align = 'right'; } d.baseline = 'middle'; if (y1 < 0 && y2 <= height) { d.baseline = 'top'; } else if (0 <= y1 && height < y2) { d.baseline = 'bottom'; } return true; }; } function outOfBounds(x, y, textWidth, textHeight, width, height) { let r = textWidth / 2; return x - r < 0 || x + r > width || y - (r = textHeight / 2) < 0 || y + r > height; } function collision($, x, y, textHeight, textWidth, h, bm0, bm1) { const w = textWidth * h / (textHeight * 2), x1 = $(x - w), x2 = $(x + w), y1 = $(y - (h = h / 2)), y2 = $(y + h); return bm0.outOfBounds(x1, y1, x2, y2) || bm0.getRange(x1, y1, x2, y2) || bm1 && bm1.getRange(x1, y1, x2, y2); } function placeAreaLabelReducedSearch ($, bitmaps, avoidBaseMark, markIndex) { const width = $.width, height = $.height, bm0 = bitmaps[0], // where labels have been placed bm1 = bitmaps[1]; // area outlines function tryLabel(_x, _y, maxSize, textWidth, textHeight) { const x = $.invert(_x), y = $.invert(_y); let lo = maxSize, hi = height, mid; if (!outOfBounds(x, y, textWidth, textHeight, width, height) && !collision($, x, y, textHeight, textWidth, lo, bm0, bm1) && !collision($, x, y, textHeight, textWidth, textHeight, bm0, null)) { // if the label fits at the current sample point, // perform binary search to find the largest font size that fits while (hi - lo >= 1) { mid = (lo + hi) / 2; if (collision($, x, y, textHeight, textWidth, mid, bm0, bm1)) { hi = mid; } else { lo = mid; } } // place label if current lower bound exceeds prior max font size if (lo > maxSize) { return [x, y, lo, true]; } } } // try to place a label within an input area mark return function (d) { const items = d.datum.datum.items[markIndex].items, // area points n = items.length, // number of points textHeight = d.datum.fontSize, // label width textWidth = textMetrics.width(d.datum, d.datum.text); // label height let maxSize = avoidBaseMark ? textHeight : 0, labelPlaced = false, labelPlaced2 = false, maxAreaWidth = 0, x1, x2, y1, y2, x, y, _x, _y, _x1, _xMid, _x2, _y1, _yMid, _y2, areaWidth, result, swapTmp; // for each area sample point for (let i = 0; i < n; ++i) { x1 = items[i].x; y1 = items[i].y; x2 = items[i].x2 === undefined ? x1 : items[i].x2; y2 = items[i].y2 === undefined ? y1 : items[i].y2; if (x1 > x2) { swapTmp = x1; x1 = x2; x2 = swapTmp; } if (y1 > y2) { swapTmp = y1; y1 = y2; y2 = swapTmp; } _x1 = $(x1); _x2 = $(x2); _xMid = ~~((_x1 + _x2) / 2); _y1 = $(y1); _y2 = $(y2); _yMid = ~~((_y1 + _y2) / 2); // search along the line from mid point between the 2 border to lower border for (_x = _xMid; _x >= _x1; --_x) { for (_y = _yMid; _y >= _y1; --_y) { result = tryLabel(_x, _y, maxSize, textWidth, textHeight); if (result) { [d.x, d.y, maxSize, labelPlaced] = result; } } } // search along the line from mid point between the 2 border to upper border for (_x = _xMid; _x <= _x2; ++_x) { for (_y = _yMid; _y <= _y2; ++_y) { result = tryLabel(_x, _y, maxSize, textWidth, textHeight); if (result) { [d.x, d.y, maxSize, labelPlaced] = result; } } } // place label at slice center if not placed through other means // and if we're not avoiding overlap with other areas if (!labelPlaced && !avoidBaseMark) { // one span is zero, hence we can add areaWidth = Math.abs(x2 - x1 + y2 - y1); x = (x1 + x2) / 2; y = (y1 + y2) / 2; // place label if it fits and improves the max area width if (areaWidth >= maxAreaWidth && !outOfBounds(x, y, textWidth, textHeight, width, height) && !collision($, x, y, textHeight, textWidth, textHeight, bm0, null)) { maxAreaWidth = areaWidth; d.x = x; d.y = y; labelPlaced2 = true; } } } // record current label placement information, update label bitmap if (labelPlaced || labelPlaced2) { x = textWidth / 2; y = textHeight / 2; bm0.setRange($(d.x - x), $(d.y - y), $(d.x + x), $(d.y + y)); d.align = 'center'; d.baseline = 'middle'; return true; } else { return false; } }; } // pixel direction offsets for flood fill search const X_DIR = [-1, -1, 1, 1]; const Y_DIR = [-1, 1, -1, 1]; function placeAreaLabelFloodFill ($, bitmaps, avoidBaseMark, markIndex) { const width = $.width, height = $.height, bm0 = bitmaps[0], // where labels have been placed bm1 = bitmaps[1], // area outlines bm2 = $.bitmap(); // flood-fill visitations // try to place a label within an input area mark return function (d) { const items = d.datum.datum.items[markIndex].items, // area points n = items.length, // number of points textHeight = d.datum.fontSize, // label width textWidth = textMetrics.width(d.datum, d.datum.text), // label height stack = []; // flood fill stack let maxSize = avoidBaseMark ? textHeight : 0, labelPlaced = false, labelPlaced2 = false, maxAreaWidth = 0, x1, x2, y1, y2, x, y, _x, _y, lo, hi, mid, areaWidth; // for each area sample point for (let i = 0; i < n; ++i) { x1 = items[i].x; y1 = items[i].y; x2 = items[i].x2 === undefined ? x1 : items[i].x2; y2 = items[i].y2 === undefined ? y1 : items[i].y2; // add scaled center point to stack stack.push([$((x1 + x2) / 2), $((y1 + y2) / 2)]); // perform flood fill, visit points while (stack.length) { [_x, _y] = stack.pop(); // exit if point already marked if (bm0.get(_x, _y) || bm1.get(_x, _y) || bm2.get(_x, _y)) continue; // mark point in flood fill bitmap // add search points for all (in bound) directions bm2.set(_x, _y); for (let j = 0; j < 4; ++j) { x = _x + X_DIR[j]; y = _y + Y_DIR[j]; if (!bm2.outOfBounds(x, y, x, y)) stack.push([x, y]); } // unscale point back to x, y space x = $.invert(_x); y = $.invert(_y); lo = maxSize; hi = height; // TODO: make this bound smaller if (!outOfBounds(x, y, textWidth, textHeight, width, height) && !collision($, x, y, textHeight, textWidth, lo, bm0, bm1) && !collision($, x, y, textHeight, textWidth, textHeight, bm0, null)) { // if the label fits at the current sample point, // perform binary search to find the largest font size that fits while (hi - lo >= 1) { mid = (lo + hi) / 2; if (collision($, x, y, textHeight, textWidth, mid, bm0, bm1)) { hi = mid; } else { lo = mid; } } // place label if current lower bound exceeds prior max font size if (lo > maxSize) { d.x = x; d.y = y; maxSize = lo; labelPlaced = true; } } } // place label at slice center if not placed through other means // and if we're not avoiding overlap with other areas if (!labelPlaced && !avoidBaseMark) { // one span is zero, hence we can add areaWidth = Math.abs(x2 - x1 + y2 - y1); x = (x1 + x2) / 2; y = (y1 + y2) / 2; // place label if it fits and improves the max area width if (areaWidth >= maxAreaWidth && !outOfBounds(x, y, textWidth, textHeight, width, height) && !collision($, x, y, textHeight, textWidth, textHeight, bm0, null)) { maxAreaWidth = areaWidth; d.x = x; d.y = y; labelPlaced2 = true; } } } // record current label placement information, update label bitmap if (labelPlaced || labelPlaced2) { x = textWidth / 2; y = textHeight / 2; bm0.setRange($(d.x - x), $(d.y - y), $(d.x + x), $(d.y + y)); d.align = 'center'; d.baseline = 'middle'; return true; } else { return false; } }; } const Aligns = ['right', 'center', 'left'], Baselines = ['bottom', 'middle', 'top']; function placeMarkLabel ($, bitmaps, anchors, offsets) { const width = $.width, height = $.height, bm0 = bitmaps[0], bm1 = bitmaps[1], n = offsets.length; return function (d) { const boundary = d.boundary, textHeight = d.datum.fontSize; // can not be placed if the mark is not visible in the graph bound if (boundary[2] < 0 || boundary[5] < 0 || boundary[0] > width || boundary[3] > height) { return false; } let textWidth = d.textWidth ?? 0, dx, dy, isInside, sizeFactor, insideFactor, x1, x2, y1, y2, xc, yc, _x1, _x2, _y1, _y2; // for each anchor and offset for (let i = 0; i < n; ++i) { dx = (anchors[i] & 0x3) - 1; dy = (anchors[i] >>> 0x2 & 0x3) - 1; isInside = dx === 0 && dy === 0 || offsets[i] < 0; sizeFactor = dx && dy ? Math.SQRT1_2 : 1; insideFactor = offsets[i] < 0 ? -1 : 1; x1 = boundary[1 + dx] + offsets[i] * dx * sizeFactor; yc = boundary[4 + dy] + insideFactor * textHeight * dy / 2 + offsets[i] * dy * sizeFactor; y1 = yc - textHeight / 2; y2 = yc + textHeight / 2; _x1 = $(x1); _y1 = $(y1); _y2 = $(y2); if (!textWidth) { // to avoid finding width of text label, if (!test(_x1, _x1, _y1, _y2, bm0, bm1, x1, x1, y1, y2, boundary, isInside)) { // skip this anchor/offset option if we fail to place a label with 1px width continue; } else { // Otherwise, find the label width textWidth = textMetrics.width(d.datum, d.datum.text); } } xc = x1 + insideFactor * textWidth * dx / 2; x1 = xc - textWidth / 2; x2 = xc + textWidth / 2; _x1 = $(x1); _x2 = $(x2); if (test(_x1, _x2, _y1, _y2, bm0, bm1, x1, x2, y1, y2, boundary, isInside)) { // place label if the position is placeable d.x = !dx ? xc : dx * insideFactor < 0 ? x2 : x1; d.y = !dy ? yc : dy * insideFactor < 0 ? y2 : y1; d.align = Aligns[dx * insideFactor + 1]; d.baseline = Baselines[dy * insideFactor + 1]; bm0.setRange(_x1, _y1, _x2, _y2); return true; } } return false; }; } // Test if a label with the given dimensions can be added without overlap function test(_x1, _x2, _y1, _y2, bm0, bm1, x1, x2, y1, y2, boundary, isInside) { return !(bm0.outOfBounds(_x1, _y1, _x2, _y2) || (isInside && bm1 || bm0).getRange(_x1, _y1, _x2, _y2)); } // 8-bit representation of anchors const TOP = 0x0, MIDDLE = 0x4, BOTTOM = 0x8, LEFT = 0x0, CENTER = 0x1, RIGHT = 0x2; // Mapping from text anchor to number representation const anchorCode = { 'top-left': TOP + LEFT, 'top': TOP + CENTER, 'top-right': TOP + RIGHT, 'left': MIDDLE + LEFT, 'middle': MIDDLE + CENTER, 'right': MIDDLE + RIGHT, 'bottom-left': BOTTOM + LEFT, 'bottom': BOTTOM + CENTER, 'bottom-right': BOTTOM + RIGHT }; const placeAreaLabel = { 'naive': placeAreaLabelNaive, 'reduced-search': placeAreaLabelReducedSearch, 'floodfill': placeAreaLabelFloodFill }; function labelLayout (texts, size, compare, offset, anchor, avoidMarks, avoidBaseMark, lineAnchor, markIndex, padding, method) { // early exit for empty data if (!texts.length) return texts; const positions = Math.max(offset.length, anchor.length), offsets = getOffsets(offset, positions), anchors = getAnchors(anchor, positions), marktype = markType(texts[0].datum), grouptype = marktype === 'group' && texts[0].datum.items[markIndex].marktype, isGroupArea = grouptype === 'area', boundary = markBoundary(marktype, grouptype, lineAnchor, markIndex), infPadding = padding === null || padding === Infinity, isNaiveGroupArea = isGroupArea && method === 'naive'; let maxTextWidth = -1, maxTextHeight = -1; // prepare text mark data for placing const data = texts.map(d => { const textWidth = infPadding ? textMetrics.width(d, d.text) : undefined; maxTextWidth = Math.max(maxTextWidth, textWidth); maxTextHeight = Math.max(maxTextHeight, d.fontSize); return { datum: d, opacity: 0, x: undefined, y: undefined, align: undefined, baseline: undefined, boundary: boundary(d), textWidth }; }); padding = padding === null || padding === Infinity ? Math.max(maxTextWidth, maxTextHeight) + Math.max(...offset) : padding; const $ = scaler(size[0], size[1], padding); let bitmaps; if (!isNaiveGroupArea) { // sort labels in priority order, if comparator is provided if (compare) { data.sort((a, b) => compare(a.datum, b.datum)); } // flag indicating if label can be placed inside its base mark let labelInside = false; for (let i = 0; i < anchors.length && !labelInside; ++i) { // label inside if anchor is at center // label inside if offset to be inside the mark bound labelInside = anchors[i] === 0x5 || offsets[i] < 0; } // extract data information from base mark when base mark is to be avoided // base mark is implicitly avoided if it is a group area const baseMark = (marktype && avoidBaseMark || isGroupArea) && texts.map(d => d.datum); // generate bitmaps for layout calculation bitmaps = avoidMarks.length || baseMark ? markBitmaps($, baseMark || [], avoidMarks, labelInside, isGroupArea) : baseBitmaps($, avoidBaseMark && data); } // generate label placement function const place = isGroupArea ? placeAreaLabel[method]($, bitmaps, avoidBaseMark, markIndex) : placeMarkLabel($, bitmaps, anchors, offsets); // place all labels data.forEach(d => d.opacity = +place(d)); return data; } function getOffsets(_, count) { const offsets = new Float64Array(count), n = _.length; for (let i = 0; i < n; ++i) offsets[i] = _[i] || 0; for (let i = n; i < count; ++i) offsets[i] = offsets[n - 1]; return offsets; } function getAnchors(_, count) { const anchors = new Int8Array(count), n = _.length; for (let i = 0; i < n; ++i) anchors[i] |= anchorCode[_[i]]; for (let i = n; i < count; ++i) anchors[i] = anchors[n - 1]; return anchors; } function markType(item) { return item && item.mark && item.mark.marktype; } /** * Factory function for function for getting base mark boundary, depending * on mark and group type. When mark type is undefined, line or area: boundary * is the coordinate of each data point. When base mark is grouped line, * boundary is either at the start or end of the line depending on the * value of lineAnchor. Otherwise, use bounds of base mark. */ function markBoundary(marktype, grouptype, lineAnchor, markIndex) { const xy = d => [d.x, d.x, d.x, d.y, d.y, d.y]; if (!marktype) { return xy; // no reactive geometry } else if (marktype === 'line' || marktype === 'area') { return d => xy(d.datum); } else if (grouptype === 'line') { return d => { const items = d.datum.items[markIndex].items; return xy(items.length ? items[lineAnchor === 'start' ? 0 : items.length - 1] : { x: NaN, y: NaN }); }; } else { return d => { const b = d.datum.bounds; return [b.x1, (b.x1 + b.x2) / 2, b.x2, b.y1, (b.y1 + b.y2) / 2, b.y2]; }; } } const Output = ['x', 'y', 'opacity', 'align', 'baseline']; const Anchors = ['top-left', 'left', 'bottom-left', 'top', 'bottom', 'top-right', 'right', 'bottom-right']; /** * Compute text label layout to annotate marks. * @constructor * @param {object} params - The parameters for this operator. * @param {Array<number>} params.size - The size of the layout, provided as a [width, height] array. * @param {function(*,*): number} [params.sort] - An optional * comparator function for sorting label data in priority order. * @param {Array<string>} [params.anchor] - Label anchor points relative to the base mark bounding box. * The available options are 'top-left', 'left', 'bottom-left', 'top', * 'bottom', 'top-right', 'right', 'bottom-right', 'middle'. * @param {Array<number>} [params.offset] - Label offsets (in pixels) from the base mark bounding box. * This parameter is parallel to the list of anchor points. * @param {number | null} [params.padding=0] - The amount (in pixels) that a label may exceed the layout size. * If this parameter is null, a label may exceed the layout size without any boundary. * @param {string} [params.lineAnchor='end'] - For group line mark labels only, indicates the anchor * position for labels. One of 'start' or 'end'. * @param {string} [params.markIndex=0] - For group mark labels only, an index indicating * which mark within the group should be labeled. * @param {Array<number>} [params.avoidMarks] - A list of additional mark names for which the label * layout should avoid overlap. * @param {boolean} [params.avoidBaseMark=true] - Boolean flag indicating if labels should avoid * overlap with the underlying base mark being labeled. * @param {string} [params.method='naive'] - For area make labels only, a method for * place labels. One of 'naive', 'reduced-search', or 'floodfill'. * @param {Array<string>} [params.as] - The output fields written by the transform. * The default is ['x', 'y', 'opacity', 'align', 'baseline']. */ function Label(params) { Transform.call(this, null, params); } Label.Definition = { type: 'Label', metadata: { modifies: true }, params: [{ name: 'size', type: 'number', array: true, length: 2, required: true }, { name: 'sort', type: 'compare' }, { name: 'anchor', type: 'string', array: true, default: Anchors }, { name: 'offset', type: 'number', array: true, default: [1] }, { name: 'padding', type: 'number', default: 0, null: true }, { name: 'lineAnchor', type: 'string', values: ['start', 'end'], default: 'end' }, { name: 'markIndex', type: 'number', default: 0 }, { name: 'avoidBaseMark', type: 'boolean', default: true }, { name: 'avoidMarks', type: 'data', array: true }, { name: 'method', type: 'string', default: 'naive' }, { name: 'as', type: 'string', array: true, length: Output.length, default: Output }] }; inherits(Label, Transform, { transform(_, pulse) { function modp(param) { const p = _[param]; return isFunction(p) && pulse.modified(p.fields); } const mod = _.modified(); if (!(mod || pulse.changed(pulse.ADD_REM) || modp('sort'))) return; if (!_.size || _.size.length !== 2) { error('Size parameter should be specified as a [width, height] array.'); } const as = _.as || Output; // run label layout labelLayout(pulse.materialize(pulse.SOURCE).source || [], _.size, _.sort, array(_.offset == null ? 1 : _.offset), array(_.anchor || Anchors), _.avoidMarks || [], _.avoidBaseMark !== false, _.lineAnchor || 'end', _.markIndex || 0, _.padding === undefined ? 0 : _.padding, _.method || 'naive').forEach(l => { // write layout results to data stream const t = l.datum; t[as[0]] = l.x; t[as[1]] = l.y; t[as[2]] = l.opacity; t[as[3]] = l.align; t[as[4]] = l.baseline; }); return pulse.reflow(mod).modifies(as); } }); export { Label as label }; //# sourceMappingURL=vega-label.js.map