vega-label
Version:
Label layout transform for Vega dataflows.
876 lines (822 loc) • 27.8 kB
JavaScript
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