@visactor/vgrammar-wordcloud
Version:
WordCloud layout transform for VGrammar
187 lines (183 loc) • 9.95 kB
JavaScript
import { isObject, merge, shuffleArray } from "@visactor/vutils";
import { BaseLayout } from "./base";
import { generateIsEmptyPixel, generateMaskCanvas, getMaxRadiusAndCenter } from "@visactor/vgrammar-util";
export class GridLayout extends BaseLayout {
constructor(options) {
super(merge({}, GridLayout.defaultOptions, options)), this.fillGridAt = (x, y) => {
x >= this.ngx || y >= this.ngy || x < 0 || y < 0 || (this.grid[x][y] = !1);
}, this.updateGrid = (gx, gy, gw, gh, info) => {
const occupied = info.occupied;
let i = occupied.length;
for (;i--; ) {
const px = gx + occupied[i][0], py = gy + occupied[i][1];
px >= this.ngx || py >= this.ngy || px < 0 || py < 0 || this.fillGridAt(px, py);
}
}, this.gridSize = Math.max(Math.floor(this.options.gridSize), 4);
}
getPointsAtRadius(radius) {
if (this.pointsAtRadius[radius]) return this.pointsAtRadius[radius];
const T = 8 * radius;
let t = T;
const points = [];
for (0 === radius && points.push([ this.center[0], this.center[1], 0 ]); t--; ) {
const rx = this.shape(t / T * 2 * Math.PI);
points.push([ this.center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI), this.center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) * this.options.ellipticity, t / T * 2 * Math.PI ]);
}
return this.pointsAtRadius[radius] = points, points;
}
getTextInfo(item, shrinkRatio = 1, index) {
var _a;
const sizeShrinkRatio = this.options.clip ? 1 : shrinkRatio, fontSize = Math.max(Math.floor((this.isTryRepeatFill ? this.options.fillTextFontSize : this.getTextFontSize(item)) * sizeShrinkRatio), this.options.minFontSize);
let word = this.getText(item) + "";
if (this.options.clip && (word = word.slice(0, Math.ceil(word.length * shrinkRatio))),
!word) return null;
const fontWeight = this.getTextFontWeight(item), fontStyle = this.getTextFontStyle(item), angle = this.getTextRotate && null !== (_a = this.getTextRotate(item, index)) && void 0 !== _a ? _a : 0, fontFamily = this.getTextFontFamily(item), fcanvas = document.createElement("canvas"), fctx = fcanvas.getContext("2d", {
willReadFrequently: !0
});
fctx.font = fontStyle + " " + fontWeight + " " + fontSize.toString(10) + "px " + fontFamily;
const fw = fctx.measureText(word).width, fh = Math.max(fontSize, fctx.measureText("m").width, fctx.measureText("W").width);
let boxWidth = fw + 2 * fh, boxHeight = 3 * fh;
const fgw = Math.ceil(boxWidth / this.gridSize), fgh = Math.ceil(boxHeight / this.gridSize);
boxWidth = fgw * this.gridSize, boxHeight = fgh * this.gridSize;
const fillTextOffsetX = -fw / 2, fillTextOffsetY = .4 * -fh, cgh = Math.ceil((boxWidth * Math.abs(Math.sin(angle)) + boxHeight * Math.abs(Math.cos(angle))) / this.gridSize), cgw = Math.ceil((boxWidth * Math.abs(Math.cos(angle)) + boxHeight * Math.abs(Math.sin(angle))) / this.gridSize), width = cgw * this.gridSize, height = cgh * this.gridSize;
fcanvas.setAttribute("width", "" + width), fcanvas.setAttribute("height", "" + height),
fctx.scale(1, 1), fctx.translate(width / 2, height / 2), fctx.rotate(-angle), fctx.font = fontStyle + " " + fontWeight + " " + fontSize.toString(10) + "px " + fontFamily,
fctx.fillStyle = "#000", fctx.textBaseline = "middle", fctx.fillText(word, fillTextOffsetX, fillTextOffsetY);
const imageData = fctx.getImageData(0, 0, width, height).data;
if (this.exceedTime()) return null;
const occupied = [];
let gy, gx = cgw;
const bounds = [ cgh / 2, cgw / 2, cgh / 2, cgw / 2 ], singleGridLoop = (gx, gy, out) => {
let y = this.gridSize;
for (;y--; ) {
let x = this.gridSize;
for (;x--; ) if (imageData[4 * ((gy * this.gridSize + y) * width + (gx * this.gridSize + x)) + 3]) return out.push([ gx, gy ]),
gx < bounds[3] && (bounds[3] = gx), gx > bounds[1] && (bounds[1] = gx), gy < bounds[0] && (bounds[0] = gy),
void (gy > bounds[2] && (bounds[2] = gy));
}
};
for (;gx--; ) for (gy = cgh; gy--; ) singleGridLoop(gx, gy, occupied);
return {
datum: item,
occupied: occupied,
bounds: bounds,
gw: cgw,
gh: cgh,
fillTextOffsetX: fillTextOffsetX,
fillTextOffsetY: fillTextOffsetY,
fillTextWidth: fw,
fillTextHeight: fh,
fontSize: fontSize,
fontStyle: fontStyle,
fontWeight: fontWeight,
fontFamily: fontFamily,
angle: angle,
text: word
};
}
calculateEmptyRate() {
const totalCount = this.ngx * this.ngy;
let emptyCount = 0;
for (let gx = 0; gx < this.ngx; gx++) for (let gy = 0; gy < this.ngy; gy++) this.grid[gx][gy] && emptyCount++;
return emptyCount / totalCount;
}
canFitText(gx, gy, gw, gh, occupied) {
let i = occupied.length;
for (;i--; ) {
const px = gx + occupied[i][0], py = gy + occupied[i][1];
if (px >= this.ngx || py >= this.ngy || px < 0 || py < 0) {
if (!this.options.drawOutOfBound) return !1;
} else if (!this.grid[px][py]) return !1;
}
return !0;
}
layoutWord(index, shrinkRatio = 1) {
const item = this.data[index], info = this.getTextInfo(item, shrinkRatio, index);
if (!info) return !1;
if (this.exceedTime()) return !1;
if (!this.options.drawOutOfBound && (!this.options.shrink || info.fontSize <= this.options.minFontSize) && !this.options.clip) {
const bounds = info.bounds;
if (bounds[1] - bounds[3] + 1 > this.ngx || bounds[2] - bounds[0] + 1 > this.ngy) return !1;
}
let r = this.maxRadius + 1;
const tryToPutWordAtPoint = gxy => {
const gx = Math.floor(gxy[0] - info.gw / 2), gy = Math.floor(gxy[1] - info.gh / 2), gw = info.gw, gh = info.gh;
return !!this.canFitText(gx, gy, gw, gh, info.occupied) && (info.distance = this.maxRadius - r,
info.theta = gxy[2], this.outputText(gx, gy, info), this.updateGrid(gx, gy, gw, gh, info),
!0);
};
for (;r--; ) {
let points = this.getPointsAtRadius(this.maxRadius - r);
this.options.random && (points = [].concat(points), shuffleArray(points));
if (points.some(tryToPutWordAtPoint)) return !0;
}
return (this.options.clip || !!(this.options.shrink && info.fontSize > this.options.minFontSize)) && this.layoutWord(index, .75 * shrinkRatio);
}
outputText(gx, gy, info) {
const color = this.getTextColor(info), output = {
text: info.text,
datum: info.datum,
color: color,
fontStyle: info.fontStyle,
fontWeight: info.fontWeight,
fontFamily: info.fontFamily,
angle: info.angle,
width: info.fillTextWidth,
height: info.fillTextHeight,
x: (gx + info.gw / 2) * this.gridSize,
y: (gy + info.gh / 2) * this.gridSize + info.fillTextOffsetY + .5 * info.fontSize,
fontSize: info.fontSize
};
this.result.push(output), this.progressiveResult && this.progressiveResult.push(output);
}
initGrid(config) {
this.grid = [];
const shape = this.options.shape;
if (isObject(shape)) {
const canvas = generateMaskCanvas(shape, config.width, config.height);
let imageData = canvas.getContext("2d").getImageData(0, 0, this.ngx * this.gridSize, this.ngy * this.gridSize);
this.options.onUpdateMaskCanvas && this.options.onUpdateMaskCanvas(canvas);
let i, isEmptyPixel = generateIsEmptyPixel(shape.backgroundColor);
const singleGridLoop = (gx, gy) => {
let y = this.gridSize;
for (;y--; ) {
let x = this.gridSize;
for (;x--; ) if (i = 4, !isEmptyPixel(imageData, gy * this.gridSize + y, gx * this.gridSize + x)) return void (this.grid[gx][gy] = !0);
}
this.grid[gx][gy] = !1;
};
let gx = this.ngx;
for (;gx--; ) {
this.grid[gx] = [];
let gy = this.ngy;
for (;gy--; ) singleGridLoop(gx, gy), !1 !== this.grid[gx][gy] && (this.grid[gx][gy] = !0);
}
imageData = isEmptyPixel = void 0;
} else {
let gx = this.ngx;
for (;gx--; ) {
this.grid[gx] = [];
let gy = this.ngy;
for (;gy--; ) this.grid[gx][gy] = !0;
}
}
}
canRepeat() {
return this.calculateEmptyRate() > .001;
}
layout(data, config) {
this.initProgressive(), this.drawnCount = 0, this.isTryRepeatFill = !1, this.originalData = data,
this.data = data, this.pointsAtRadius = [], this.ngx = Math.floor(config.width / this.gridSize),
this.ngy = Math.floor(config.height / this.gridSize);
const {center: center, maxRadius: maxRadius} = getMaxRadiusAndCenter(this.options.shape, [ config.width, config.height ]);
return this.center = config.origin ? [ config.origin[0] / this.gridSize, config.origin[1] / this.gridSize ] : [ center[0] / this.gridSize, center[1] / this.gridSize ],
this.maxRadius = Math.floor(maxRadius / this.gridSize), this.initGrid(config), this.result = [],
this.progressiveRun();
}
}
GridLayout.defaultOptions = {
gridSize: 8,
ellipticity: 1,
maxSingleWordTryCount: 1
};
//# sourceMappingURL=grid-layout.js.map