UNPKG

@visactor/vgrammar-wordcloud

Version:

WordCloud layout transform for VGrammar

189 lines (185 loc) 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: !0 }), exports.GridLayout = void 0; const vutils_1 = require("@visactor/vutils"), base_1 = require("./base"), vgrammar_util_1 = require("@visactor/vgrammar-util"); class GridLayout extends base_1.BaseLayout { constructor(options) { super((0, vutils_1.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), (0, vutils_1.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 ((0, vutils_1.isObject)(shape)) { const canvas = (0, vgrammar_util_1.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 = (0, vgrammar_util_1.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} = (0, vgrammar_util_1.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(); } } exports.GridLayout = GridLayout, GridLayout.defaultOptions = { gridSize: 8, ellipticity: 1, maxSingleWordTryCount: 1 }; //# sourceMappingURL=grid-layout.js.map