vega-label
Advanced tools
Comparing version 1.2.0 to 1.2.1
@@ -7,6 +7,7 @@ (function (global, factory) { | ||
// 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 | ||
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]))); | ||
@@ -18,24 +19,24 @@ return [bitmap, undefined]; | ||
const width = $.width, | ||
height = $.height, | ||
border = labelInside || isGroupArea, | ||
context = vegaCanvas.canvas(width, height).getContext('2d'), | ||
baseMarkContext = vegaCanvas.canvas(width, height).getContext('2d'), | ||
strokeContext = border && vegaCanvas.canvas(width, height).getContext('2d'); // render all marks to be avoided into canvas | ||
height = $.height, | ||
border = labelInside || isGroupArea, | ||
context = vegaCanvas.canvas(width, height).getContext('2d'), | ||
baseMarkContext = vegaCanvas.canvas(width, height).getContext('2d'), | ||
strokeContext = border && vegaCanvas.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 | ||
} | ||
// 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 | ||
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) { | ||
@@ -47,3 +48,2 @@ for (x = 0; x < width; ++x) { | ||
strokeAlpha = border && strokeBuffer[index] & ALPHA_MASK; | ||
if (alpha || strokeAlpha || baseMarkAlpha) { | ||
@@ -53,3 +53,2 @@ u = $(x); | ||
if (!isGroupArea && (alpha || baseMarkAlpha)) layer1.set(u, v); // update interior bitmap | ||
if (border && (alpha || strokeAlpha)) layer2.set(u, v); // update border bitmap | ||
@@ -62,11 +61,8 @@ } | ||
} | ||
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') { | ||
@@ -82,2 +78,3 @@ items.forEach(group => { | ||
} | ||
/** | ||
@@ -88,9 +85,7 @@ * Prepare item before drawing into canvas (setting stroke and opacity) | ||
*/ | ||
function prepare(source) { | ||
const item = vegaDataflow.rederive(source, {}); | ||
if (item.stroke && item.strokeOpacity !== 0 || item.fill && item.fillOpacity !== 0) { | ||
return { ...item, | ||
return { | ||
...item, | ||
strokeOpacity: 1, | ||
@@ -101,3 +96,2 @@ stroke: '#000', | ||
} | ||
return item; | ||
@@ -107,14 +101,13 @@ } | ||
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 | ||
// 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) { | ||
@@ -124,14 +117,10 @@ RIGHT1[i] = RIGHT1[i - 1] << 1 | 1; | ||
} | ||
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 { | ||
@@ -145,3 +134,2 @@ array: array, | ||
const index = y * w + x; | ||
_set(index >>> DIV, 1 << (index & MOD)); | ||
@@ -151,3 +139,2 @@ }, | ||
const index = y * w + x; | ||
_clear(index >>> DIV, ~(1 << (index & MOD))); | ||
@@ -157,7 +144,6 @@ }, | ||
let r = y2, | ||
start, | ||
end, | ||
indexStart, | ||
indexEnd; | ||
start, | ||
end, | ||
indexStart, | ||
indexEnd; | ||
for (; r >= y; --r) { | ||
@@ -168,3 +154,2 @@ start = r * w + x; | ||
indexEnd = end >>> DIV; | ||
if (indexStart === indexEnd) { | ||
@@ -177,3 +162,2 @@ if (array[indexStart] & RIGHT0[start & MOD] & RIGHT1[(end & MOD) + 1]) { | ||
if (array[indexEnd] & RIGHT1[(end & MOD) + 1]) return true; | ||
for (let i = indexStart + 1; i < indexEnd; ++i) { | ||
@@ -184,3 +168,2 @@ if (array[i]) return true; | ||
} | ||
return false; | ||
@@ -190,3 +173,2 @@ }, | ||
let start, end, indexStart, indexEnd, i; | ||
for (; y <= y2; ++y) { | ||
@@ -197,3 +179,2 @@ start = y * w + x; | ||
indexEnd = end >>> DIV; | ||
if (indexStart === indexEnd) { | ||
@@ -203,5 +184,3 @@ _set(indexStart, RIGHT0[start & MOD] & RIGHT1[(end & MOD) + 1]); | ||
_set(indexStart, RIGHT0[start & MOD]); | ||
_set(indexEnd, RIGHT1[(end & MOD) + 1]); | ||
for (i = indexStart + 1; i < indexEnd; ++i) _set(i, 0xffffffff); | ||
@@ -213,3 +192,2 @@ } | ||
let start, end, indexStart, indexEnd, i; | ||
for (; y <= y2; ++y) { | ||
@@ -220,3 +198,2 @@ start = y * w + x; | ||
indexEnd = end >>> DIV; | ||
if (indexStart === indexEnd) { | ||
@@ -226,5 +203,3 @@ _clear(indexStart, RIGHT1[start & MOD] | RIGHT0[(end & MOD) + 1]); | ||
_clear(indexStart, RIGHT1[start & MOD]); | ||
_clear(indexEnd, RIGHT0[(end & MOD) + 1]); | ||
for (i = indexStart + 1; i < indexEnd; ++i) _clear(i, 0); | ||
@@ -240,10 +215,7 @@ } | ||
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); | ||
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; | ||
@@ -258,22 +230,24 @@ scale.padding = padding; | ||
const width = $.width, | ||
height = $.height; // try to place a label within an input area mark | ||
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 = vegaScenegraph.textMetrics.width(d.datum, d.datum.text); // label height | ||
// area points | ||
n = items.length, | ||
// number of points | ||
textHeight = d.datum.fontSize, | ||
// label width | ||
textWidth = vegaScenegraph.textMetrics.width(d.datum, d.datum.text); // label height | ||
let maxAreaWidth = 0, | ||
x1, | ||
x2, | ||
y1, | ||
y2, | ||
x, | ||
y, | ||
areaWidth; // for each area sample point | ||
x1, | ||
x2, | ||
y1, | ||
y2, | ||
x, | ||
y, | ||
areaWidth; | ||
// for each area sample point | ||
for (let i = 0; i < n; ++i) { | ||
@@ -287,3 +261,2 @@ x1 = items[i].x; | ||
areaWidth = Math.abs(x2 - x1 + y2 - y1); | ||
if (areaWidth >= maxAreaWidth) { | ||
@@ -295,3 +268,2 @@ maxAreaWidth = areaWidth; | ||
} | ||
x = textWidth / 2; | ||
@@ -304,3 +276,2 @@ y = textHeight / 2; | ||
d.align = 'center'; | ||
if (x1 < 0 && x2 <= width) { | ||
@@ -311,5 +282,3 @@ d.align = 'left'; | ||
} | ||
d.baseline = 'middle'; | ||
if (y1 < 0 && y2 <= height) { | ||
@@ -320,3 +289,2 @@ d.baseline = 'top'; | ||
} | ||
return true; | ||
@@ -332,6 +300,6 @@ }; | ||
const w = textWidth * h / (textHeight * 2), | ||
x1 = $(x - w), | ||
x2 = $(x + w), | ||
y1 = $(y - (h = h / 2)), | ||
y2 = $(y + h); | ||
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); | ||
@@ -342,14 +310,13 @@ } | ||
const width = $.width, | ||
height = $.height, | ||
bm0 = bitmaps[0], | ||
// where labels have been placed | ||
bm1 = bitmaps[1]; // area outlines | ||
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); | ||
y = $.invert(_y); | ||
let lo = maxSize, | ||
hi = height, | ||
mid; | ||
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)) { | ||
@@ -360,3 +327,2 @@ // if the label fits at the current sample point, | ||
mid = (lo + hi) / 2; | ||
if (collision($, x, y, textHeight, textWidth, mid, bm0, bm1)) { | ||
@@ -367,5 +333,4 @@ hi = mid; | ||
} | ||
} // place label if current lower bound exceeds prior max font size | ||
} | ||
// place label if current lower bound exceeds prior max font size | ||
if (lo > maxSize) { | ||
@@ -375,37 +340,37 @@ return [x, y, lo, true]; | ||
} | ||
} // try to place a label within an input area mark | ||
} | ||
// 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 = vegaScenegraph.textMetrics.width(d.datum, d.datum.text); // label height | ||
// area points | ||
n = items.length, | ||
// number of points | ||
textHeight = d.datum.fontSize, | ||
// label width | ||
textWidth = vegaScenegraph.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 | ||
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) { | ||
@@ -416,3 +381,2 @@ x1 = items[i].x; | ||
y2 = items[i].y2 === undefined ? y1 : items[i].y2; | ||
if (x1 > x2) { | ||
@@ -423,3 +387,2 @@ swapTmp = x1; | ||
} | ||
if (y1 > y2) { | ||
@@ -430,3 +393,2 @@ swapTmp = y1; | ||
} | ||
_x1 = $(x1); | ||
@@ -437,8 +399,8 @@ _x2 = $(x2); | ||
_y2 = $(y2); | ||
_yMid = ~~((_y1 + _y2) / 2); // search along the line from mid point between the 2 border to lower border | ||
_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) { | ||
@@ -448,9 +410,8 @@ [d.x, d.y, maxSize, labelPlaced] = result; | ||
} | ||
} // search along the line from mid point between the 2 border to upper border | ||
} | ||
// 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) { | ||
@@ -460,6 +421,6 @@ [d.x, d.y, maxSize, labelPlaced] = result; | ||
} | ||
} // place label at slice center if not placed through other means | ||
} | ||
// place label at slice center if not placed through other means | ||
// and if we're not avoiding overlap with other areas | ||
if (!labelPlaced && !avoidBaseMark) { | ||
@@ -469,4 +430,5 @@ // one span is zero, hence we can add | ||
x = (x1 + x2) / 2; | ||
y = (y1 + y2) / 2; // place label if it fits and improves the max area width | ||
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)) { | ||
@@ -479,5 +441,5 @@ maxAreaWidth = areaWidth; | ||
} | ||
} // record current label placement information, update label bitmap | ||
} | ||
// record current label placement information, update label bitmap | ||
if (labelPlaced || labelPlaced2) { | ||
@@ -496,2 +458,3 @@ x = textWidth / 2; | ||
// pixel direction offsets for flood fill search | ||
const X_DIR = [-1, -1, 1, 1]; | ||
@@ -501,39 +464,39 @@ const Y_DIR = [-1, 1, -1, 1]; | ||
const width = $.width, | ||
height = $.height, | ||
bm0 = bitmaps[0], | ||
// where labels have been placed | ||
bm1 = bitmaps[1], | ||
// area outlines | ||
bm2 = $.bitmap(); // flood-fill visitations | ||
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 = vegaScenegraph.textMetrics.width(d.datum, d.datum.text), | ||
// label height | ||
stack = []; // flood fill stack | ||
// area points | ||
n = items.length, | ||
// number of points | ||
textHeight = d.datum.fontSize, | ||
// label width | ||
textWidth = vegaScenegraph.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 | ||
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) { | ||
@@ -543,14 +506,17 @@ x1 = items[i].x; | ||
x2 = items[i].x2 === undefined ? x1 : items[i].x2; | ||
y2 = items[i].y2 === undefined ? y1 : items[i].y2; // add scaled center point to stack | ||
y2 = items[i].y2 === undefined ? y1 : items[i].y2; | ||
stack.push([$((x1 + x2) / 2), $((y1 + y2) / 2)]); // perform flood fill, visit points | ||
// 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 | ||
[_x, _y] = stack.pop(); | ||
if (bm0.get(_x, _y) || bm1.get(_x, _y) || bm2.get(_x, _y)) continue; // mark point in flood fill bitmap | ||
// 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) { | ||
@@ -560,5 +526,5 @@ x = _x + X_DIR[j]; | ||
if (!bm2.outOfBounds(x, y, x, y)) stack.push([x, y]); | ||
} // unscale point back to x, y space | ||
} | ||
// unscale point back to x, y space | ||
x = $.invert(_x); | ||
@@ -574,3 +540,2 @@ y = $.invert(_y); | ||
mid = (lo + hi) / 2; | ||
if (collision($, x, y, textHeight, textWidth, mid, bm0, bm1)) { | ||
@@ -581,5 +546,4 @@ hi = mid; | ||
} | ||
} // place label if current lower bound exceeds prior max font size | ||
} | ||
// place label if current lower bound exceeds prior max font size | ||
if (lo > maxSize) { | ||
@@ -592,6 +556,6 @@ d.x = x; | ||
} | ||
} // place label at slice center if not placed through other means | ||
} | ||
// place label at slice center if not placed through other means | ||
// and if we're not avoiding overlap with other areas | ||
if (!labelPlaced && !avoidBaseMark) { | ||
@@ -601,4 +565,5 @@ // one span is zero, hence we can add | ||
x = (x1 + x2) / 2; | ||
y = (y1 + y2) / 2; // place label if it fits and improves the max area width | ||
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)) { | ||
@@ -611,5 +576,5 @@ maxAreaWidth = areaWidth; | ||
} | ||
} // record current label placement information, update label bitmap | ||
} | ||
// record current label placement information, update label bitmap | ||
if (labelPlaced || labelPlaced2) { | ||
@@ -629,35 +594,35 @@ x = textWidth / 2; | ||
const Aligns = ['right', 'center', 'left'], | ||
Baselines = ['bottom', 'middle', 'top']; | ||
Baselines = ['bottom', 'middle', 'top']; | ||
function placeMarkLabel ($, bitmaps, anchors, offsets) { | ||
const width = $.width, | ||
height = $.height, | ||
bm0 = bitmaps[0], | ||
bm1 = bitmaps[1], | ||
n = offsets.length; | ||
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 | ||
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 | ||
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) { | ||
@@ -676,3 +641,2 @@ dx = (anchors[i] & 0x3) - 1; | ||
_y2 = $(y2); | ||
if (!textWidth) { | ||
@@ -688,3 +652,2 @@ // to avoid finding width of text label, | ||
} | ||
xc = x1 + insideFactor * textWidth * dx / 2; | ||
@@ -695,3 +658,2 @@ x1 = xc - textWidth / 2; | ||
_x2 = $(x2); | ||
if (test(_x1, _x2, _y1, _y2, bm0, bm1, x1, x2, y1, y2, boundary, isInside)) { | ||
@@ -707,7 +669,7 @@ // place label if the position is placeable | ||
} | ||
return false; | ||
}; | ||
} // Test if a label with the given dimensions can be added without overlap | ||
} | ||
// 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) { | ||
@@ -717,9 +679,11 @@ 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 | ||
MIDDLE = 0x4, | ||
BOTTOM = 0x8, | ||
LEFT = 0x0, | ||
CENTER = 0x1, | ||
RIGHT = 0x2; | ||
// Mapping from text anchor to number representation | ||
const anchorCode = { | ||
@@ -745,13 +709,14 @@ 'top-left': TOP + LEFT, | ||
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'; | ||
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 | ||
maxTextHeight = -1; | ||
// prepare text mark data for placing | ||
const data = texts.map(d => { | ||
@@ -775,3 +740,2 @@ const textWidth = infPadding ? vegaScenegraph.textMetrics.width(d, d.text) : undefined; | ||
let bitmaps; | ||
if (!isNaiveGroupArea) { | ||
@@ -781,7 +745,6 @@ // sort labels in priority order, if comparator is provided | ||
data.sort((a, b) => compare(a.datum, b.datum)); | ||
} // flag indicating if label can be placed inside its base mark | ||
} | ||
// flag indicating if label can be placed inside its base mark | ||
let labelInside = false; | ||
for (let i = 0; i < anchors.length && !labelInside; ++i) { | ||
@@ -791,43 +754,37 @@ // label inside if anchor is at center | ||
labelInside = anchors[i] === 0x5 || offsets[i] < 0; | ||
} // extract data information from base mark when base mark is to be avoided | ||
} | ||
// 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); | ||
const baseMark = (marktype && avoidBaseMark || isGroupArea) && texts.map(d => d.datum); // generate bitmaps for layout calculation | ||
// generate bitmaps for layout calculation | ||
bitmaps = avoidMarks.length || baseMark ? markBitmaps($, baseMark || [], avoidMarks, labelInside, isGroupArea) : baseBitmaps($, avoidBaseMark && data); | ||
} // generate label placement function | ||
} | ||
// generate label placement function | ||
const place = isGroupArea ? placeAreaLabel[method]($, bitmaps, avoidBaseMark, markIndex) : placeMarkLabel($, bitmaps, anchors, offsets); | ||
const place = isGroupArea ? placeAreaLabel[method]($, bitmaps, avoidBaseMark, markIndex) : placeMarkLabel($, bitmaps, anchors, offsets); // place all labels | ||
// place all labels | ||
data.forEach(d => d.opacity = +place(d)); | ||
return data; | ||
} | ||
function getOffsets(_, count) { | ||
const offsets = new Float64Array(count), | ||
n = _.length; | ||
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; | ||
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; | ||
} | ||
/** | ||
@@ -840,7 +797,4 @@ * Factory function for function for getting base mark boundary, depending | ||
*/ | ||
function markBoundary(marktype, grouptype, lineAnchor, markIndex) { | ||
const xy = d => [d.x, d.x, d.x, d.y, d.y, d.y]; | ||
if (!marktype) { | ||
@@ -868,2 +822,3 @@ return xy; // no reactive geometry | ||
const Anchors = ['top-left', 'left', 'bottom-left', 'top', 'bottom', 'top-right', 'right', 'bottom-right']; | ||
/** | ||
@@ -896,3 +851,2 @@ * Compute text label layout to annotate marks. | ||
*/ | ||
function Label(params) { | ||
@@ -965,13 +919,10 @@ vegaDataflow.Transform.call(this, null, params); | ||
} | ||
const mod = _.modified(); | ||
if (!(mod || pulse.changed(pulse.ADD_REM) || modp('sort'))) return; | ||
if (!_.size || _.size.length !== 2) { | ||
vegaUtil.error('Size parameter should be specified as a [width, height] array.'); | ||
} | ||
const as = _.as || Output; | ||
const as = _.as || Output; // run label layout | ||
// run label layout | ||
labelLayout(pulse.materialize(pulse.SOURCE).source || [], _.size, _.sort, vegaUtil.array(_.offset == null ? 1 : _.offset), vegaUtil.array(_.anchor || Anchors), _.avoidMarks || [], _.avoidBaseMark !== false, _.lineAnchor || 'end', _.markIndex || 0, _.padding === undefined ? 0 : _.padding, _.method || 'naive').forEach(l => { | ||
@@ -988,3 +939,2 @@ // write layout results to data stream | ||
} | ||
}); | ||
@@ -994,4 +944,2 @@ | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
})); |
@@ -1,2 +0,2 @@ | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("vega-scenegraph"),require("vega-canvas"),require("vega-dataflow"),require("vega-util")):"function"==typeof define&&define.amd?define(["exports","vega-scenegraph","vega-canvas","vega-dataflow","vega-util"],e):e(((t="undefined"!=typeof globalThis?globalThis:t||self).vega=t.vega||{},t.vega.transforms={}),t.vega,t.vega,t.vega,t.vega)}(this,(function(t,e,n,a,r){"use strict";const i=4278190080;function o(t,e,n){return new Uint32Array(t.getImageData(0,0,e,n).data.buffer)}function u(t,n,a){if(!n.length)return;const r=n[0].mark.marktype;"group"===r?n.forEach((e=>{e.items.forEach((e=>u(t,e.items,a)))})):e.Marks[r].draw(t,{items:a?n.map(s):n})}function s(t){const e=a.rederive(t,{});return e.stroke&&0!==e.strokeOpacity||e.fill&&0!==e.fillOpacity?{...e,strokeOpacity:1,stroke:"#000",fillOpacity:0}:e}const l=31,f=new Uint32Array(33),d=new Uint32Array(33);d[0]=0,f[0]=~d[0];for(let t=1;t<=32;++t)d[t]=d[t-1]<<1|1,f[t]=~d[t];function c(t,e,n){const a=Math.max(1,Math.sqrt(t*e/1e6)),r=~~((t+2*n+a)/a),i=~~((e+2*n+a)/a),o=t=>~~((t+n)/a);return o.invert=t=>t*a-n,o.bitmap=()=>function(t,e){const n=new Uint32Array(~~((t*e+32)/32));function a(t,e){n[t]|=e}function r(t,e){n[t]&=e}return{array:n,get:(e,a)=>{const r=a*t+e;return n[r>>>5]&1<<(r&l)},set:(e,n)=>{const r=n*t+e;a(r>>>5,1<<(r&l))},clear:(e,n)=>{const a=n*t+e;r(a>>>5,~(1<<(a&l)))},getRange:(e,a,r,i)=>{let o,u,s,c,m=i;for(;m>=a;--m)if(o=m*t+e,u=m*t+r,s=o>>>5,c=u>>>5,s===c){if(n[s]&f[o&l]&d[1+(u&l)])return!0}else{if(n[s]&f[o&l])return!0;if(n[c]&d[1+(u&l)])return!0;for(let t=s+1;t<c;++t)if(n[t])return!0}return!1},setRange:(e,n,r,i)=>{let o,u,s,c,m;for(;n<=i;++n)if(o=n*t+e,u=n*t+r,s=o>>>5,c=u>>>5,s===c)a(s,f[o&l]&d[1+(u&l)]);else for(a(s,f[o&l]),a(c,d[1+(u&l)]),m=s+1;m<c;++m)a(m,4294967295)},clearRange:(e,n,a,i)=>{let o,u,s,c,m;for(;n<=i;++n)if(o=n*t+e,u=n*t+a,s=o>>>5,c=u>>>5,s===c)r(s,d[o&l]|f[1+(u&l)]);else for(r(s,d[o&l]),r(c,f[1+(u&l)]),m=s+1;m<c;++m)r(m,0)},outOfBounds:(n,a,r,i)=>n<0||a<0||i>=e||r>=t}}(r,i),o.ratio=a,o.padding=n,o.width=t,o.height=e,o}function m(t,e,n,a,r,i){let o=n/2;return t-o<0||t+o>r||e-(o=a/2)<0||e+o>i}function g(t,e,n,a,r,i,o,u){const s=r*i/(2*a),l=t(e-s),f=t(e+s),d=t(n-(i/=2)),c=t(n+i);return o.outOfBounds(l,d,f,c)||o.getRange(l,d,f,c)||u&&u.getRange(l,d,f,c)}const h=[-1,-1,1,1],y=[-1,1,-1,1];const p=["right","center","left"],x=["bottom","middle","top"];function v(t,e,n,a,r,i,o,u,s,l,f,d){return!(r.outOfBounds(t,n,e,a)||(d&&i||r).getRange(t,n,e,a))}const b={"top-left":0,top:1,"top-right":2,left:4,middle:5,right:6,"bottom-left":8,bottom:9,"bottom-right":10},M={naive:function(t,n,a,r){const i=t.width,o=t.height;return function(t){const n=t.datum.datum.items[r].items,a=n.length,u=t.datum.fontSize,s=e.textMetrics.width(t.datum,t.datum.text);let l,f,d,c,m,g,h,y=0;for(let e=0;e<a;++e)l=n[e].x,d=n[e].y,f=void 0===n[e].x2?l:n[e].x2,c=void 0===n[e].y2?d:n[e].y2,m=(l+f)/2,g=(d+c)/2,h=Math.abs(f-l+c-d),h>=y&&(y=h,t.x=m,t.y=g);return m=s/2,g=u/2,l=t.x-m,f=t.x+m,d=t.y-g,c=t.y+g,t.align="center",l<0&&f<=i?t.align="left":0<=l&&i<f&&(t.align="right"),t.baseline="middle",d<0&&c<=o?t.baseline="top":0<=d&&o<c&&(t.baseline="bottom"),!0}},"reduced-search":function(t,n,a,r){const i=t.width,o=t.height,u=n[0],s=n[1];function l(e,n,a,r,l){const f=t.invert(e),d=t.invert(n);let c,h=a,y=o;if(!m(f,d,r,l,i,o)&&!g(t,f,d,l,r,h,u,s)&&!g(t,f,d,l,r,l,u,null)){for(;y-h>=1;)c=(h+y)/2,g(t,f,d,l,r,c,u,s)?y=c:h=c;if(h>a)return[f,d,h,!0]}}return function(n){const s=n.datum.datum.items[r].items,f=s.length,d=n.datum.fontSize,c=e.textMetrics.width(n.datum,n.datum.text);let h,y,p,x,v,b,M,w,k,R,z,O,A,E,S,q,B,T=a?d:0,U=!1,C=!1,D=0;for(let e=0;e<f;++e){for(h=s[e].x,p=s[e].y,y=void 0===s[e].x2?h:s[e].x2,x=void 0===s[e].y2?p:s[e].y2,h>y&&(B=h,h=y,y=B),p>x&&(B=p,p=x,x=B),k=t(h),z=t(y),R=~~((k+z)/2),O=t(p),E=t(x),A=~~((O+E)/2),M=R;M>=k;--M)for(w=A;w>=O;--w)q=l(M,w,T,c,d),q&&([n.x,n.y,T,U]=q);for(M=R;M<=z;++M)for(w=A;w<=E;++w)q=l(M,w,T,c,d),q&&([n.x,n.y,T,U]=q);U||a||(S=Math.abs(y-h+x-p),v=(h+y)/2,b=(p+x)/2,S>=D&&!m(v,b,c,d,i,o)&&!g(t,v,b,d,c,d,u,null)&&(D=S,n.x=v,n.y=b,C=!0))}return!(!U&&!C)&&(v=c/2,b=d/2,u.setRange(t(n.x-v),t(n.y-b),t(n.x+v),t(n.y+b)),n.align="center",n.baseline="middle",!0)}},floodfill:function(t,n,a,r){const i=t.width,o=t.height,u=n[0],s=n[1],l=t.bitmap();return function(n){const f=n.datum.datum.items[r].items,d=f.length,c=n.datum.fontSize,p=e.textMetrics.width(n.datum,n.datum.text),x=[];let v,b,M,w,k,R,z,O,A,E,S,q,B=a?c:0,T=!1,U=!1,C=0;for(let e=0;e<d;++e){for(v=f[e].x,M=f[e].y,b=void 0===f[e].x2?v:f[e].x2,w=void 0===f[e].y2?M:f[e].y2,x.push([t((v+b)/2),t((M+w)/2)]);x.length;)if([z,O]=x.pop(),!(u.get(z,O)||s.get(z,O)||l.get(z,O))){l.set(z,O);for(let t=0;t<4;++t)k=z+h[t],R=O+y[t],l.outOfBounds(k,R,k,R)||x.push([k,R]);if(k=t.invert(z),R=t.invert(O),A=B,E=o,!m(k,R,p,c,i,o)&&!g(t,k,R,c,p,A,u,s)&&!g(t,k,R,c,p,c,u,null)){for(;E-A>=1;)S=(A+E)/2,g(t,k,R,c,p,S,u,s)?E=S:A=S;A>B&&(n.x=k,n.y=R,B=A,T=!0)}}T||a||(q=Math.abs(b-v+w-M),k=(v+b)/2,R=(M+w)/2,q>=C&&!m(k,R,p,c,i,o)&&!g(t,k,R,c,p,c,u,null)&&(C=q,n.x=k,n.y=R,U=!0))}return!(!T&&!U)&&(k=p/2,R=c/2,u.setRange(t(n.x-k),t(n.y-R),t(n.x+k),t(n.y+R)),n.align="center",n.baseline="middle",!0)}}};function w(t,a,r,s,l,f,d,m,g,h,y){if(!t.length)return t;const w=Math.max(s.length,l.length),k=function(t,e){const n=new Float64Array(e),a=t.length;for(let e=0;e<a;++e)n[e]=t[e]||0;for(let t=a;t<e;++t)n[t]=n[a-1];return n}(s,w),R=function(t,e){const n=new Int8Array(e),a=t.length;for(let e=0;e<a;++e)n[e]|=b[t[e]];for(let t=a;t<e;++t)n[t]=n[a-1];return n}(l,w),z=(B=t[0].datum)&&B.mark&&B.mark.marktype,O="group"===z&&t[0].datum.items[g].marktype,A="area"===O,E=function(t,e,n,a){const r=t=>[t.x,t.x,t.x,t.y,t.y,t.y];return t?"line"===t||"area"===t?t=>r(t.datum):"line"===e?t=>{const e=t.datum.items[a].items;return r(e.length?e["start"===n?0:e.length-1]:{x:NaN,y:NaN})}:t=>{const e=t.datum.bounds;return[e.x1,(e.x1+e.x2)/2,e.x2,e.y1,(e.y1+e.y2)/2,e.y2]}:r}(z,O,m,g),S=null===h||h===1/0,q=A&&"naive"===y;var B;let T=-1,U=-1;const C=t.map((t=>{const n=S?e.textMetrics.width(t,t.text):void 0;return T=Math.max(T,n),U=Math.max(U,t.fontSize),{datum:t,opacity:0,x:void 0,y:void 0,align:void 0,baseline:void 0,boundary:E(t),textWidth:n}}));h=null===h||h===1/0?Math.max(T,U)+Math.max(...s):h;const D=c(a[0],a[1],h);let I;if(!q){r&&C.sort(((t,e)=>r(t.datum,e.datum)));let e=!1;for(let t=0;t<R.length&&!e;++t)e=5===R[t]||k[t]<0;const a=(z&&d||A)&&t.map((t=>t.datum));I=f.length||a?function(t,e,a,r,s){const l=t.width,f=t.height,d=r||s,c=n.canvas(l,f).getContext("2d"),m=n.canvas(l,f).getContext("2d"),g=d&&n.canvas(l,f).getContext("2d");a.forEach((t=>u(c,t,!1))),u(m,e,!1),d&&u(g,e,!0);const h=o(c,l,f),y=o(m,l,f),p=d&&o(g,l,f),x=t.bitmap(),v=d&&t.bitmap();let b,M,w,k,R,z,O,A;for(M=0;M<f;++M)for(b=0;b<l;++b)R=M*l+b,z=h[R]&i,A=y[R]&i,O=d&&p[R]&i,(z||O||A)&&(w=t(b),k=t(M),s||!z&&!A||x.set(w,k),d&&(z||O)&&v.set(w,k));return[x,v]}(D,a||[],f,e,A):function(t,e){const n=t.bitmap();return(e||[]).forEach((e=>n.set(t(e.boundary[0]),t(e.boundary[3])))),[n,void 0]}(D,d&&C)}const N=A?M[y](D,I,d,g):function(t,n,a,r){const i=t.width,o=t.height,u=n[0],s=n[1],l=r.length;return function(n){var f;const d=n.boundary,c=n.datum.fontSize;if(d[2]<0||d[5]<0||d[0]>i||d[3]>o)return!1;let m,g,h,y,b,M,w,k,R,z,O,A,E,S,q,B=null!==(f=n.textWidth)&&void 0!==f?f:0;for(let i=0;i<l;++i){if(m=(3&a[i])-1,g=(a[i]>>>2&3)-1,h=0===m&&0===g||r[i]<0,y=m&&g?Math.SQRT1_2:1,b=r[i]<0?-1:1,M=d[1+m]+r[i]*m*y,O=d[4+g]+b*c*g/2+r[i]*g*y,k=O-c/2,R=O+c/2,A=t(M),S=t(k),q=t(R),!B){if(!v(A,A,S,q,u,s,0,0,0,0,0,h))continue;B=e.textMetrics.width(n.datum,n.datum.text)}if(z=M+b*B*m/2,M=z-B/2,w=z+B/2,A=t(M),E=t(w),v(A,E,S,q,u,s,0,0,0,0,0,h))return n.x=m?m*b<0?w:M:z,n.y=g?g*b<0?R:k:O,n.align=p[m*b+1],n.baseline=x[g*b+1],u.setRange(A,S,E,q),!0}return!1}}(D,I,R,k);return C.forEach((t=>t.opacity=+N(t))),C}const k=["x","y","opacity","align","baseline"],R=["top-left","left","bottom-left","top","bottom","top-right","right","bottom-right"];function z(t){a.Transform.call(this,null,t)}z.Definition={type:"Label",metadata:{modifies:!0},params:[{name:"size",type:"number",array:!0,length:2,required:!0},{name:"sort",type:"compare"},{name:"anchor",type:"string",array:!0,default:R},{name:"offset",type:"number",array:!0,default:[1]},{name:"padding",type:"number",default:0,null:!0},{name:"lineAnchor",type:"string",values:["start","end"],default:"end"},{name:"markIndex",type:"number",default:0},{name:"avoidBaseMark",type:"boolean",default:!0},{name:"avoidMarks",type:"data",array:!0},{name:"method",type:"string",default:"naive"},{name:"as",type:"string",array:!0,length:k.length,default:k}]},r.inherits(z,a.Transform,{transform(t,e){const n=t.modified();if(!(n||e.changed(e.ADD_REM)||function(n){const a=t[n];return r.isFunction(a)&&e.modified(a.fields)}("sort")))return;t.size&&2===t.size.length||r.error("Size parameter should be specified as a [width, height] array.");const a=t.as||k;return w(e.materialize(e.SOURCE).source||[],t.size,t.sort,r.array(null==t.offset?1:t.offset),r.array(t.anchor||R),t.avoidMarks||[],!1!==t.avoidBaseMark,t.lineAnchor||"end",t.markIndex||0,void 0===t.padding?0:t.padding,t.method||"naive").forEach((t=>{const e=t.datum;e[a[0]]=t.x,e[a[1]]=t.y,e[a[2]]=t.opacity,e[a[3]]=t.align,e[a[4]]=t.baseline})),e.reflow(n).modifies(a)}}),t.label=z,Object.defineProperty(t,"__esModule",{value:!0})})); | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("vega-scenegraph"),require("vega-canvas"),require("vega-dataflow"),require("vega-util")):"function"==typeof define&&define.amd?define(["exports","vega-scenegraph","vega-canvas","vega-dataflow","vega-util"],e):e(((t="undefined"!=typeof globalThis?globalThis:t||self).vega=t.vega||{},t.vega.transforms={}),t.vega,t.vega,t.vega,t.vega)}(this,(function(t,e,n,a,r){"use strict";const i=4278190080;function o(t,e,n){return new Uint32Array(t.getImageData(0,0,e,n).data.buffer)}function s(t,n,a){if(!n.length)return;const r=n[0].mark.marktype;"group"===r?n.forEach((e=>{e.items.forEach((e=>s(t,e.items,a)))})):e.Marks[r].draw(t,{items:a?n.map(u):n})}function u(t){const e=a.rederive(t,{});return e.stroke&&0!==e.strokeOpacity||e.fill&&0!==e.fillOpacity?{...e,strokeOpacity:1,stroke:"#000",fillOpacity:0}:e}const f=5,l=31,d=32,c=new Uint32Array(d+1),m=new Uint32Array(d+1);m[0]=0,c[0]=~m[0];for(let t=1;t<=d;++t)m[t]=m[t-1]<<1|1,c[t]=~m[t];function g(t,e,n){const a=Math.max(1,Math.sqrt(t*e/1e6)),r=~~((t+2*n+a)/a),i=~~((e+2*n+a)/a),o=t=>~~((t+n)/a);return o.invert=t=>t*a-n,o.bitmap=()=>function(t,e){const n=new Uint32Array(~~((t*e+d)/d));function a(t,e){n[t]|=e}function r(t,e){n[t]&=e}return{array:n,get:(e,a)=>{const r=a*t+e;return n[r>>>f]&1<<(r&l)},set:(e,n)=>{const r=n*t+e;a(r>>>f,1<<(r&l))},clear:(e,n)=>{const a=n*t+e;r(a>>>f,~(1<<(a&l)))},getRange:(e,a,r,i)=>{let o,s,u,d,g=i;for(;g>=a;--g)if(o=g*t+e,s=g*t+r,u=o>>>f,d=s>>>f,u===d){if(n[u]&c[o&l]&m[1+(s&l)])return!0}else{if(n[u]&c[o&l])return!0;if(n[d]&m[1+(s&l)])return!0;for(let t=u+1;t<d;++t)if(n[t])return!0}return!1},setRange:(e,n,r,i)=>{let o,s,u,d,g;for(;n<=i;++n)if(o=n*t+e,s=n*t+r,u=o>>>f,d=s>>>f,u===d)a(u,c[o&l]&m[1+(s&l)]);else for(a(u,c[o&l]),a(d,m[1+(s&l)]),g=u+1;g<d;++g)a(g,4294967295)},clearRange:(e,n,a,i)=>{let o,s,u,d,g;for(;n<=i;++n)if(o=n*t+e,s=n*t+a,u=o>>>f,d=s>>>f,u===d)r(u,m[o&l]|c[1+(s&l)]);else for(r(u,m[o&l]),r(d,c[1+(s&l)]),g=u+1;g<d;++g)r(g,0)},outOfBounds:(n,a,r,i)=>n<0||a<0||i>=e||r>=t}}(r,i),o.ratio=a,o.padding=n,o.width=t,o.height=e,o}function h(t,e,n,a,r,i){let o=n/2;return t-o<0||t+o>r||e-(o=a/2)<0||e+o>i}function y(t,e,n,a,r,i,o,s){const u=r*i/(2*a),f=t(e-u),l=t(e+u),d=t(n-(i/=2)),c=t(n+i);return o.outOfBounds(f,d,l,c)||o.getRange(f,d,l,c)||s&&s.getRange(f,d,l,c)}const p=[-1,-1,1,1],x=[-1,1,-1,1];const v=["right","center","left"],b=["bottom","middle","top"];function w(t,e,n,a,r,i,o,s,u,f,l,d){return!(r.outOfBounds(t,n,e,a)||(d&&i||r).getRange(t,n,e,a))}const M={"top-left":0,top:1,"top-right":2,left:4,middle:5,right:6,"bottom-left":8,bottom:9,"bottom-right":10},k={naive:function(t,n,a,r){const i=t.width,o=t.height;return function(t){const n=t.datum.datum.items[r].items,a=n.length,s=t.datum.fontSize,u=e.textMetrics.width(t.datum,t.datum.text);let f,l,d,c,m,g,h,y=0;for(let e=0;e<a;++e)f=n[e].x,d=n[e].y,l=void 0===n[e].x2?f:n[e].x2,c=void 0===n[e].y2?d:n[e].y2,m=(f+l)/2,g=(d+c)/2,h=Math.abs(l-f+c-d),h>=y&&(y=h,t.x=m,t.y=g);return m=u/2,g=s/2,f=t.x-m,l=t.x+m,d=t.y-g,c=t.y+g,t.align="center",f<0&&l<=i?t.align="left":0<=f&&i<l&&(t.align="right"),t.baseline="middle",d<0&&c<=o?t.baseline="top":0<=d&&o<c&&(t.baseline="bottom"),!0}},"reduced-search":function(t,n,a,r){const i=t.width,o=t.height,s=n[0],u=n[1];function f(e,n,a,r,f){const l=t.invert(e),d=t.invert(n);let c,m=a,g=o;if(!h(l,d,r,f,i,o)&&!y(t,l,d,f,r,m,s,u)&&!y(t,l,d,f,r,f,s,null)){for(;g-m>=1;)c=(m+g)/2,y(t,l,d,f,r,c,s,u)?g=c:m=c;if(m>a)return[l,d,m,!0]}}return function(n){const u=n.datum.datum.items[r].items,l=u.length,d=n.datum.fontSize,c=e.textMetrics.width(n.datum,n.datum.text);let m,g,p,x,v,b,w,M,k,R,z,A,O,E,S,q,B,T=a?d:0,U=!1,C=!1,D=0;for(let e=0;e<l;++e){for(m=u[e].x,p=u[e].y,g=void 0===u[e].x2?m:u[e].x2,x=void 0===u[e].y2?p:u[e].y2,m>g&&(B=m,m=g,g=B),p>x&&(B=p,p=x,x=B),k=t(m),z=t(g),R=~~((k+z)/2),A=t(p),E=t(x),O=~~((A+E)/2),w=R;w>=k;--w)for(M=O;M>=A;--M)q=f(w,M,T,c,d),q&&([n.x,n.y,T,U]=q);for(w=R;w<=z;++w)for(M=O;M<=E;++M)q=f(w,M,T,c,d),q&&([n.x,n.y,T,U]=q);U||a||(S=Math.abs(g-m+x-p),v=(m+g)/2,b=(p+x)/2,S>=D&&!h(v,b,c,d,i,o)&&!y(t,v,b,d,c,d,s,null)&&(D=S,n.x=v,n.y=b,C=!0))}return!(!U&&!C)&&(v=c/2,b=d/2,s.setRange(t(n.x-v),t(n.y-b),t(n.x+v),t(n.y+b)),n.align="center",n.baseline="middle",!0)}},floodfill:function(t,n,a,r){const i=t.width,o=t.height,s=n[0],u=n[1],f=t.bitmap();return function(n){const l=n.datum.datum.items[r].items,d=l.length,c=n.datum.fontSize,m=e.textMetrics.width(n.datum,n.datum.text),g=[];let v,b,w,M,k,R,z,A,O,E,S,q,B=a?c:0,T=!1,U=!1,C=0;for(let e=0;e<d;++e){for(v=l[e].x,w=l[e].y,b=void 0===l[e].x2?v:l[e].x2,M=void 0===l[e].y2?w:l[e].y2,g.push([t((v+b)/2),t((w+M)/2)]);g.length;)if([z,A]=g.pop(),!(s.get(z,A)||u.get(z,A)||f.get(z,A))){f.set(z,A);for(let t=0;t<4;++t)k=z+p[t],R=A+x[t],f.outOfBounds(k,R,k,R)||g.push([k,R]);if(k=t.invert(z),R=t.invert(A),O=B,E=o,!h(k,R,m,c,i,o)&&!y(t,k,R,c,m,O,s,u)&&!y(t,k,R,c,m,c,s,null)){for(;E-O>=1;)S=(O+E)/2,y(t,k,R,c,m,S,s,u)?E=S:O=S;O>B&&(n.x=k,n.y=R,B=O,T=!0)}}T||a||(q=Math.abs(b-v+M-w),k=(v+b)/2,R=(w+M)/2,q>=C&&!h(k,R,m,c,i,o)&&!y(t,k,R,c,m,c,s,null)&&(C=q,n.x=k,n.y=R,U=!0))}return!(!T&&!U)&&(k=m/2,R=c/2,s.setRange(t(n.x-k),t(n.y-R),t(n.x+k),t(n.y+R)),n.align="center",n.baseline="middle",!0)}}};function R(t,a,r,u,f,l,d,c,m,h,y){if(!t.length)return t;const p=Math.max(u.length,f.length),x=function(t,e){const n=new Float64Array(e),a=t.length;for(let e=0;e<a;++e)n[e]=t[e]||0;for(let t=a;t<e;++t)n[t]=n[a-1];return n}(u,p),R=function(t,e){const n=new Int8Array(e),a=t.length;for(let e=0;e<a;++e)n[e]|=M[t[e]];for(let t=a;t<e;++t)n[t]=n[a-1];return n}(f,p),z=(B=t[0].datum)&&B.mark&&B.mark.marktype,A="group"===z&&t[0].datum.items[m].marktype,O="area"===A,E=function(t,e,n,a){const r=t=>[t.x,t.x,t.x,t.y,t.y,t.y];return t?"line"===t||"area"===t?t=>r(t.datum):"line"===e?t=>{const e=t.datum.items[a].items;return r(e.length?e["start"===n?0:e.length-1]:{x:NaN,y:NaN})}:t=>{const e=t.datum.bounds;return[e.x1,(e.x1+e.x2)/2,e.x2,e.y1,(e.y1+e.y2)/2,e.y2]}:r}(z,A,c,m),S=null===h||h===1/0,q=O&&"naive"===y;var B;let T=-1,U=-1;const C=t.map((t=>{const n=S?e.textMetrics.width(t,t.text):void 0;return T=Math.max(T,n),U=Math.max(U,t.fontSize),{datum:t,opacity:0,x:void 0,y:void 0,align:void 0,baseline:void 0,boundary:E(t),textWidth:n}}));h=null===h||h===1/0?Math.max(T,U)+Math.max(...u):h;const D=g(a[0],a[1],h);let I;if(!q){r&&C.sort(((t,e)=>r(t.datum,e.datum)));let e=!1;for(let t=0;t<R.length&&!e;++t)e=5===R[t]||x[t]<0;const a=(z&&d||O)&&t.map((t=>t.datum));I=l.length||a?function(t,e,a,r,u){const f=t.width,l=t.height,d=r||u,c=n.canvas(f,l).getContext("2d"),m=n.canvas(f,l).getContext("2d"),g=d&&n.canvas(f,l).getContext("2d");a.forEach((t=>s(c,t,!1))),s(m,e,!1),d&&s(g,e,!0);const h=o(c,f,l),y=o(m,f,l),p=d&&o(g,f,l),x=t.bitmap(),v=d&&t.bitmap();let b,w,M,k,R,z,A,O;for(w=0;w<l;++w)for(b=0;b<f;++b)R=w*f+b,z=h[R]&i,O=y[R]&i,A=d&&p[R]&i,(z||A||O)&&(M=t(b),k=t(w),u||!z&&!O||x.set(M,k),d&&(z||A)&&v.set(M,k));return[x,v]}(D,a||[],l,e,O):function(t,e){const n=t.bitmap();return(e||[]).forEach((e=>n.set(t(e.boundary[0]),t(e.boundary[3])))),[n,void 0]}(D,d&&C)}const N=O?k[y](D,I,d,m):function(t,n,a,r){const i=t.width,o=t.height,s=n[0],u=n[1],f=r.length;return function(n){const l=n.boundary,d=n.datum.fontSize;if(l[2]<0||l[5]<0||l[0]>i||l[3]>o)return!1;let c,m,g,h,y,p,x,M,k,R,z,A,O,E,S,q=n.textWidth??0;for(let i=0;i<f;++i){if(c=(3&a[i])-1,m=(a[i]>>>2&3)-1,g=0===c&&0===m||r[i]<0,h=c&&m?Math.SQRT1_2:1,y=r[i]<0?-1:1,p=l[1+c]+r[i]*c*h,z=l[4+m]+y*d*m/2+r[i]*m*h,M=z-d/2,k=z+d/2,A=t(p),E=t(M),S=t(k),!q){if(!w(A,A,E,S,s,u,0,0,0,0,0,g))continue;q=e.textMetrics.width(n.datum,n.datum.text)}if(R=p+y*q*c/2,p=R-q/2,x=R+q/2,A=t(p),O=t(x),w(A,O,E,S,s,u,0,0,0,0,0,g))return n.x=c?c*y<0?x:p:R,n.y=m?m*y<0?k:M:z,n.align=v[c*y+1],n.baseline=b[m*y+1],s.setRange(A,E,O,S),!0}return!1}}(D,I,R,x);return C.forEach((t=>t.opacity=+N(t))),C}const z=["x","y","opacity","align","baseline"],A=["top-left","left","bottom-left","top","bottom","top-right","right","bottom-right"];function O(t){a.Transform.call(this,null,t)}O.Definition={type:"Label",metadata:{modifies:!0},params:[{name:"size",type:"number",array:!0,length:2,required:!0},{name:"sort",type:"compare"},{name:"anchor",type:"string",array:!0,default:A},{name:"offset",type:"number",array:!0,default:[1]},{name:"padding",type:"number",default:0,null:!0},{name:"lineAnchor",type:"string",values:["start","end"],default:"end"},{name:"markIndex",type:"number",default:0},{name:"avoidBaseMark",type:"boolean",default:!0},{name:"avoidMarks",type:"data",array:!0},{name:"method",type:"string",default:"naive"},{name:"as",type:"string",array:!0,length:z.length,default:z}]},r.inherits(O,a.Transform,{transform(t,e){const n=t.modified();if(!(n||e.changed(e.ADD_REM)||function(n){const a=t[n];return r.isFunction(a)&&e.modified(a.fields)}("sort")))return;t.size&&2===t.size.length||r.error("Size parameter should be specified as a [width, height] array.");const a=t.as||z;return R(e.materialize(e.SOURCE).source||[],t.size,t.sort,r.array(null==t.offset?1:t.offset),r.array(t.anchor||A),t.avoidMarks||[],!1!==t.avoidBaseMark,t.lineAnchor||"end",t.markIndex||0,void 0===t.padding?0:t.padding,t.method||"naive").forEach((t=>{const e=t.datum;e[a[0]]=t.x,e[a[1]]=t.y,e[a[2]]=t.opacity,e[a[3]]=t.align,e[a[4]]=t.baseline})),e.reflow(n).modifies(a)}}),t.label=O})); | ||
//# sourceMappingURL=vega-label.min.js.map |
import { Marks, textMetrics } from 'vega-scenegraph'; | ||
import { canvas } from 'vega-canvas'; | ||
import { rederive, Transform } from 'vega-dataflow'; | ||
import { inherits, isFunction, error, array } from 'vega-util'; | ||
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 | ||
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]))); | ||
@@ -16,24 +17,24 @@ return [bitmap, undefined]; | ||
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 | ||
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 | ||
} | ||
// 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 | ||
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) { | ||
@@ -45,3 +46,2 @@ for (x = 0; x < width; ++x) { | ||
strokeAlpha = border && strokeBuffer[index] & ALPHA_MASK; | ||
if (alpha || strokeAlpha || baseMarkAlpha) { | ||
@@ -51,3 +51,2 @@ u = $(x); | ||
if (!isGroupArea && (alpha || baseMarkAlpha)) layer1.set(u, v); // update interior bitmap | ||
if (border && (alpha || strokeAlpha)) layer2.set(u, v); // update border bitmap | ||
@@ -60,11 +59,8 @@ } | ||
} | ||
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') { | ||
@@ -80,2 +76,3 @@ items.forEach(group => { | ||
} | ||
/** | ||
@@ -86,9 +83,7 @@ * Prepare item before drawing into canvas (setting stroke and opacity) | ||
*/ | ||
function prepare(source) { | ||
const item = rederive(source, {}); | ||
if (item.stroke && item.strokeOpacity !== 0 || item.fill && item.fillOpacity !== 0) { | ||
return { ...item, | ||
return { | ||
...item, | ||
strokeOpacity: 1, | ||
@@ -99,3 +94,2 @@ stroke: '#000', | ||
} | ||
return item; | ||
@@ -105,14 +99,13 @@ } | ||
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 | ||
// 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) { | ||
@@ -122,14 +115,10 @@ RIGHT1[i] = RIGHT1[i - 1] << 1 | 1; | ||
} | ||
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 { | ||
@@ -143,3 +132,2 @@ array: array, | ||
const index = y * w + x; | ||
_set(index >>> DIV, 1 << (index & MOD)); | ||
@@ -149,3 +137,2 @@ }, | ||
const index = y * w + x; | ||
_clear(index >>> DIV, ~(1 << (index & MOD))); | ||
@@ -155,7 +142,6 @@ }, | ||
let r = y2, | ||
start, | ||
end, | ||
indexStart, | ||
indexEnd; | ||
start, | ||
end, | ||
indexStart, | ||
indexEnd; | ||
for (; r >= y; --r) { | ||
@@ -166,3 +152,2 @@ start = r * w + x; | ||
indexEnd = end >>> DIV; | ||
if (indexStart === indexEnd) { | ||
@@ -175,3 +160,2 @@ if (array[indexStart] & RIGHT0[start & MOD] & RIGHT1[(end & MOD) + 1]) { | ||
if (array[indexEnd] & RIGHT1[(end & MOD) + 1]) return true; | ||
for (let i = indexStart + 1; i < indexEnd; ++i) { | ||
@@ -182,3 +166,2 @@ if (array[i]) return true; | ||
} | ||
return false; | ||
@@ -188,3 +171,2 @@ }, | ||
let start, end, indexStart, indexEnd, i; | ||
for (; y <= y2; ++y) { | ||
@@ -195,3 +177,2 @@ start = y * w + x; | ||
indexEnd = end >>> DIV; | ||
if (indexStart === indexEnd) { | ||
@@ -201,5 +182,3 @@ _set(indexStart, RIGHT0[start & MOD] & RIGHT1[(end & MOD) + 1]); | ||
_set(indexStart, RIGHT0[start & MOD]); | ||
_set(indexEnd, RIGHT1[(end & MOD) + 1]); | ||
for (i = indexStart + 1; i < indexEnd; ++i) _set(i, 0xffffffff); | ||
@@ -211,3 +190,2 @@ } | ||
let start, end, indexStart, indexEnd, i; | ||
for (; y <= y2; ++y) { | ||
@@ -218,3 +196,2 @@ start = y * w + x; | ||
indexEnd = end >>> DIV; | ||
if (indexStart === indexEnd) { | ||
@@ -224,5 +201,3 @@ _clear(indexStart, RIGHT1[start & MOD] | RIGHT0[(end & MOD) + 1]); | ||
_clear(indexStart, RIGHT1[start & MOD]); | ||
_clear(indexEnd, RIGHT0[(end & MOD) + 1]); | ||
for (i = indexStart + 1; i < indexEnd; ++i) _clear(i, 0); | ||
@@ -238,10 +213,7 @@ } | ||
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); | ||
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; | ||
@@ -256,22 +228,24 @@ scale.padding = padding; | ||
const width = $.width, | ||
height = $.height; // try to place a label within an input area mark | ||
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 | ||
// 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 | ||
x1, | ||
x2, | ||
y1, | ||
y2, | ||
x, | ||
y, | ||
areaWidth; | ||
// for each area sample point | ||
for (let i = 0; i < n; ++i) { | ||
@@ -285,3 +259,2 @@ x1 = items[i].x; | ||
areaWidth = Math.abs(x2 - x1 + y2 - y1); | ||
if (areaWidth >= maxAreaWidth) { | ||
@@ -293,3 +266,2 @@ maxAreaWidth = areaWidth; | ||
} | ||
x = textWidth / 2; | ||
@@ -302,3 +274,2 @@ y = textHeight / 2; | ||
d.align = 'center'; | ||
if (x1 < 0 && x2 <= width) { | ||
@@ -309,5 +280,3 @@ d.align = 'left'; | ||
} | ||
d.baseline = 'middle'; | ||
if (y1 < 0 && y2 <= height) { | ||
@@ -318,3 +287,2 @@ d.baseline = 'top'; | ||
} | ||
return true; | ||
@@ -330,6 +298,6 @@ }; | ||
const w = textWidth * h / (textHeight * 2), | ||
x1 = $(x - w), | ||
x2 = $(x + w), | ||
y1 = $(y - (h = h / 2)), | ||
y2 = $(y + h); | ||
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); | ||
@@ -340,14 +308,13 @@ } | ||
const width = $.width, | ||
height = $.height, | ||
bm0 = bitmaps[0], | ||
// where labels have been placed | ||
bm1 = bitmaps[1]; // area outlines | ||
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); | ||
y = $.invert(_y); | ||
let lo = maxSize, | ||
hi = height, | ||
mid; | ||
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)) { | ||
@@ -358,3 +325,2 @@ // if the label fits at the current sample point, | ||
mid = (lo + hi) / 2; | ||
if (collision($, x, y, textHeight, textWidth, mid, bm0, bm1)) { | ||
@@ -365,5 +331,4 @@ hi = mid; | ||
} | ||
} // place label if current lower bound exceeds prior max font size | ||
} | ||
// place label if current lower bound exceeds prior max font size | ||
if (lo > maxSize) { | ||
@@ -373,37 +338,37 @@ return [x, y, lo, true]; | ||
} | ||
} // try to place a label within an input area mark | ||
} | ||
// 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 | ||
// 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 | ||
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) { | ||
@@ -414,3 +379,2 @@ x1 = items[i].x; | ||
y2 = items[i].y2 === undefined ? y1 : items[i].y2; | ||
if (x1 > x2) { | ||
@@ -421,3 +385,2 @@ swapTmp = x1; | ||
} | ||
if (y1 > y2) { | ||
@@ -428,3 +391,2 @@ swapTmp = y1; | ||
} | ||
_x1 = $(x1); | ||
@@ -435,8 +397,8 @@ _x2 = $(x2); | ||
_y2 = $(y2); | ||
_yMid = ~~((_y1 + _y2) / 2); // search along the line from mid point between the 2 border to lower border | ||
_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) { | ||
@@ -446,9 +408,8 @@ [d.x, d.y, maxSize, labelPlaced] = result; | ||
} | ||
} // search along the line from mid point between the 2 border to upper border | ||
} | ||
// 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) { | ||
@@ -458,6 +419,6 @@ [d.x, d.y, maxSize, labelPlaced] = result; | ||
} | ||
} // place label at slice center if not placed through other means | ||
} | ||
// place label at slice center if not placed through other means | ||
// and if we're not avoiding overlap with other areas | ||
if (!labelPlaced && !avoidBaseMark) { | ||
@@ -467,4 +428,5 @@ // one span is zero, hence we can add | ||
x = (x1 + x2) / 2; | ||
y = (y1 + y2) / 2; // place label if it fits and improves the max area width | ||
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)) { | ||
@@ -477,5 +439,5 @@ maxAreaWidth = areaWidth; | ||
} | ||
} // record current label placement information, update label bitmap | ||
} | ||
// record current label placement information, update label bitmap | ||
if (labelPlaced || labelPlaced2) { | ||
@@ -494,2 +456,3 @@ x = textWidth / 2; | ||
// pixel direction offsets for flood fill search | ||
const X_DIR = [-1, -1, 1, 1]; | ||
@@ -499,39 +462,39 @@ const Y_DIR = [-1, 1, -1, 1]; | ||
const width = $.width, | ||
height = $.height, | ||
bm0 = bitmaps[0], | ||
// where labels have been placed | ||
bm1 = bitmaps[1], | ||
// area outlines | ||
bm2 = $.bitmap(); // flood-fill visitations | ||
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 | ||
// 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 | ||
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) { | ||
@@ -541,14 +504,17 @@ x1 = items[i].x; | ||
x2 = items[i].x2 === undefined ? x1 : items[i].x2; | ||
y2 = items[i].y2 === undefined ? y1 : items[i].y2; // add scaled center point to stack | ||
y2 = items[i].y2 === undefined ? y1 : items[i].y2; | ||
stack.push([$((x1 + x2) / 2), $((y1 + y2) / 2)]); // perform flood fill, visit points | ||
// 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 | ||
[_x, _y] = stack.pop(); | ||
if (bm0.get(_x, _y) || bm1.get(_x, _y) || bm2.get(_x, _y)) continue; // mark point in flood fill bitmap | ||
// 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) { | ||
@@ -558,5 +524,5 @@ x = _x + X_DIR[j]; | ||
if (!bm2.outOfBounds(x, y, x, y)) stack.push([x, y]); | ||
} // unscale point back to x, y space | ||
} | ||
// unscale point back to x, y space | ||
x = $.invert(_x); | ||
@@ -572,3 +538,2 @@ y = $.invert(_y); | ||
mid = (lo + hi) / 2; | ||
if (collision($, x, y, textHeight, textWidth, mid, bm0, bm1)) { | ||
@@ -579,5 +544,4 @@ hi = mid; | ||
} | ||
} // place label if current lower bound exceeds prior max font size | ||
} | ||
// place label if current lower bound exceeds prior max font size | ||
if (lo > maxSize) { | ||
@@ -590,6 +554,6 @@ d.x = x; | ||
} | ||
} // place label at slice center if not placed through other means | ||
} | ||
// place label at slice center if not placed through other means | ||
// and if we're not avoiding overlap with other areas | ||
if (!labelPlaced && !avoidBaseMark) { | ||
@@ -599,4 +563,5 @@ // one span is zero, hence we can add | ||
x = (x1 + x2) / 2; | ||
y = (y1 + y2) / 2; // place label if it fits and improves the max area width | ||
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)) { | ||
@@ -609,5 +574,5 @@ maxAreaWidth = areaWidth; | ||
} | ||
} // record current label placement information, update label bitmap | ||
} | ||
// record current label placement information, update label bitmap | ||
if (labelPlaced || labelPlaced2) { | ||
@@ -627,37 +592,35 @@ x = textWidth / 2; | ||
const Aligns = ['right', 'center', 'left'], | ||
Baselines = ['bottom', 'middle', 'top']; | ||
Baselines = ['bottom', 'middle', 'top']; | ||
function placeMarkLabel ($, bitmaps, anchors, offsets) { | ||
const width = $.width, | ||
height = $.height, | ||
bm0 = bitmaps[0], | ||
bm1 = bitmaps[1], | ||
n = offsets.length; | ||
height = $.height, | ||
bm0 = bitmaps[0], | ||
bm1 = bitmaps[1], | ||
n = offsets.length; | ||
return function (d) { | ||
var _d$textWidth; | ||
const boundary = d.boundary, | ||
textHeight = d.datum.fontSize; // can not be placed if the mark is not visible in the graph bound | ||
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; | ||
let textWidth = (_d$textWidth = d.textWidth) !== null && _d$textWidth !== void 0 ? _d$textWidth : 0, | ||
dx, | ||
dy, | ||
isInside, | ||
sizeFactor, | ||
insideFactor, | ||
x1, | ||
x2, | ||
y1, | ||
y2, | ||
xc, | ||
yc, | ||
_x1, | ||
_x2, | ||
_y1, | ||
_y2; // for each anchor and offset | ||
// for each anchor and offset | ||
for (let i = 0; i < n; ++i) { | ||
@@ -676,3 +639,2 @@ dx = (anchors[i] & 0x3) - 1; | ||
_y2 = $(y2); | ||
if (!textWidth) { | ||
@@ -688,3 +650,2 @@ // to avoid finding width of text label, | ||
} | ||
xc = x1 + insideFactor * textWidth * dx / 2; | ||
@@ -695,3 +656,2 @@ x1 = xc - textWidth / 2; | ||
_x2 = $(x2); | ||
if (test(_x1, _x2, _y1, _y2, bm0, bm1, x1, x2, y1, y2, boundary, isInside)) { | ||
@@ -707,7 +667,7 @@ // place label if the position is placeable | ||
} | ||
return false; | ||
}; | ||
} // Test if a label with the given dimensions can be added without overlap | ||
} | ||
// 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) { | ||
@@ -717,9 +677,11 @@ 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 | ||
MIDDLE = 0x4, | ||
BOTTOM = 0x8, | ||
LEFT = 0x0, | ||
CENTER = 0x1, | ||
RIGHT = 0x2; | ||
// Mapping from text anchor to number representation | ||
const anchorCode = { | ||
@@ -745,13 +707,14 @@ 'top-left': TOP + LEFT, | ||
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'; | ||
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 | ||
maxTextHeight = -1; | ||
// prepare text mark data for placing | ||
const data = texts.map(d => { | ||
@@ -775,3 +738,2 @@ const textWidth = infPadding ? textMetrics.width(d, d.text) : undefined; | ||
let bitmaps; | ||
if (!isNaiveGroupArea) { | ||
@@ -781,7 +743,6 @@ // sort labels in priority order, if comparator is provided | ||
data.sort((a, b) => compare(a.datum, b.datum)); | ||
} // flag indicating if label can be placed inside its base mark | ||
} | ||
// flag indicating if label can be placed inside its base mark | ||
let labelInside = false; | ||
for (let i = 0; i < anchors.length && !labelInside; ++i) { | ||
@@ -791,43 +752,37 @@ // label inside if anchor is at center | ||
labelInside = anchors[i] === 0x5 || offsets[i] < 0; | ||
} // extract data information from base mark when base mark is to be avoided | ||
} | ||
// 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); | ||
const baseMark = (marktype && avoidBaseMark || isGroupArea) && texts.map(d => d.datum); // generate bitmaps for layout calculation | ||
// generate bitmaps for layout calculation | ||
bitmaps = avoidMarks.length || baseMark ? markBitmaps($, baseMark || [], avoidMarks, labelInside, isGroupArea) : baseBitmaps($, avoidBaseMark && data); | ||
} // generate label placement function | ||
} | ||
// generate label placement function | ||
const place = isGroupArea ? placeAreaLabel[method]($, bitmaps, avoidBaseMark, markIndex) : placeMarkLabel($, bitmaps, anchors, offsets); | ||
const place = isGroupArea ? placeAreaLabel[method]($, bitmaps, avoidBaseMark, markIndex) : placeMarkLabel($, bitmaps, anchors, offsets); // place all labels | ||
// place all labels | ||
data.forEach(d => d.opacity = +place(d)); | ||
return data; | ||
} | ||
function getOffsets(_, count) { | ||
const offsets = new Float64Array(count), | ||
n = _.length; | ||
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; | ||
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; | ||
} | ||
/** | ||
@@ -840,7 +795,4 @@ * Factory function for function for getting base mark boundary, depending | ||
*/ | ||
function markBoundary(marktype, grouptype, lineAnchor, markIndex) { | ||
const xy = d => [d.x, d.x, d.x, d.y, d.y, d.y]; | ||
if (!marktype) { | ||
@@ -868,2 +820,3 @@ return xy; // no reactive geometry | ||
const Anchors = ['top-left', 'left', 'bottom-left', 'top', 'bottom', 'top-right', 'right', 'bottom-right']; | ||
/** | ||
@@ -896,3 +849,2 @@ * Compute text label layout to annotate marks. | ||
*/ | ||
function Label(params) { | ||
@@ -965,13 +917,10 @@ Transform.call(this, null, params); | ||
} | ||
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; | ||
const as = _.as || Output; // run label layout | ||
// 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 => { | ||
@@ -988,5 +937,4 @@ // write layout results to data stream | ||
} | ||
}); | ||
export { Label as label }; |
{ | ||
"name": "vega-label", | ||
"version": "1.2.0", | ||
"version": "1.2.1", | ||
"description": "Label layout transform for Vega dataflows.", | ||
@@ -21,3 +21,3 @@ "keywords": [ | ||
"prebuild": "rimraf build", | ||
"build": "rollup -c --config-transform", | ||
"build": "rollup -c rollup.config.mjs --config-transform", | ||
"pretest": "yarn build --config-test", | ||
@@ -36,3 +36,3 @@ "test": "tape 'test/**/*-test.js'", | ||
}, | ||
"gitHead": "9a3faca4395cade9ecdfde90af98f1c53e9916b2" | ||
"gitHead": "fb1092f6b931d450f9c210b67ae4752bd3dd461b" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2472
147442