UNPKG

@visactor/vgrammar-wordcloud-shape

Version:

Layout WordCloud in specified shape, this is a transform for VGrammar.

316 lines (308 loc) 20.2 kB
"use strict"; var __importDefault = this && this.__importDefault || function(mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: !0 }), exports.Layout = void 0; const vutils_1 = require("@visactor/vutils"), segmentation_1 = require("./segmentation"), util_1 = require("./util"), vscale_1 = require("@visactor/vscale"), cloud_shape_layout_1 = __importDefault(require("./cloud-shape-layout")), vrender_core_1 = require("@visactor/vrender-core"), vgrammar_util_1 = require("@visactor/vgrammar-util"), OUTPUT = { x: "x", y: "y", fontFamily: "fontFamily", fontSize: "fontSize", fontStyle: "fontStyle", fontWeight: "fontWeight", angle: "angle", opacity: "opacity", visible: "visible", isFillingWord: "isFillingWord", color: "color" }; class Layout { constructor(options, view) { this.progressiveResult = [], this.options = options, this.view = view; } layout(data) { this.data = data; const options = this.options, segmentationInput = { shapeUrl: options.shape, size: options.size, ratio: options.ratio || .8, tempCanvas: void 0, boardSize: [ 0, 0 ], random: !1, randomGenerator: void 0 }, tempCanvas = vrender_core_1.vglobal.createCanvas({ width: options.size[0], height: options.size[1] }), tempCtx = tempCanvas.getContext("2d", { willReadFrequently: !0 }); tempCtx.textAlign = "center", tempCtx.textBaseline = "middle", segmentationInput.tempCanvas = tempCanvas; const boardW = options.size[0] + 31 >> 5 << 5; if (segmentationInput.boardSize = [ boardW, options.size[1] ], segmentationInput.random ? segmentationInput.randomGenerator = Math.random : segmentationInput.randomGenerator = (0, util_1.fakeRandom)(), this.segmentationInput = segmentationInput, (0, vutils_1.isString)(segmentationInput.shapeUrl)) { segmentationInput.isEmptyPixel = (0, vgrammar_util_1.generateIsEmptyPixel)(); const imagePromise = (0, util_1.loadImage)(segmentationInput.shapeUrl); imagePromise ? (this.isImageFinished = !1, this.isLayoutFinished = !1, imagePromise.then((shapeImage => { this.isImageFinished = !0; const maskCanvas = vrender_core_1.vglobal.createCanvas({ width: options.size[0], height: options.size[1], dpr: 1 }); segmentationInput.maskCanvas = maskCanvas; const ctx = maskCanvas.getContext("2d"); options.removeWhiteBorder && (0, segmentation_1.removeBorder)(shapeImage, maskCanvas, segmentationInput.isEmptyPixel); const shapeConfig = (0, segmentation_1.scaleAndMiddleShape)(shapeImage, options.size); ctx.clearRect(0, 0, options.size[0], options.size[1]), ctx.drawImage(shapeImage, shapeConfig.x, shapeConfig.y, shapeConfig.width, shapeConfig.height), this.options.onUpdateMaskCanvas && this.options.onUpdateMaskCanvas(segmentationInput.maskCanvas); })).catch((error => { this.isImageFinished = !0; }))) : (this.isImageFinished = !0, this.isLayoutFinished = !0); } else if (segmentationInput.shapeUrl && ("text" === segmentationInput.shapeUrl.type || "geometric" === segmentationInput.shapeUrl.type)) { segmentationInput.isEmptyPixel = (0, vgrammar_util_1.generateIsEmptyPixel)(segmentationInput.shapeUrl.backgroundColor); const maskCanvas = (0, vgrammar_util_1.generateMaskCanvas)(segmentationInput.shapeUrl, options.size[0], options.size[1]); segmentationInput.maskCanvas = maskCanvas, this.options.onUpdateMaskCanvas && this.options.onUpdateMaskCanvas(maskCanvas), this.doLayout(), this.isImageFinished = !0, this.isLayoutFinished = !0; } } canAnimate() { return !0; } unfinished() { return !this.isLayoutFinished; } output() { return this.progressiveResult; } progressiveRun() { this.isImageFinished && !this.isLayoutFinished && (this.segmentationInput.maskCanvas && this.doLayout(), this.isLayoutFinished = !0); } progressiveOutput() { return this.progressiveResult; } doLayout() { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x; const segmentationInput = this.segmentationInput, segmentationOutput = (0, segmentation_1.segmentation)(segmentationInput); if (!segmentationOutput.segmentation.regions.length) return; const options = this.options, data = this.data, colorMode = options.colorMode || "ordinal", wordsConfig = { getText: field(options.text), getFontSize: field(options.fontSize), fontSizeRange: options.fontSizeRange, colorMode: colorMode, getColor: options.colorField ? field(options.colorField) : field(options.text), getFillingColor: field(options.fillingColorField), colorList: options.colorList || ("ordinal" === colorMode ? [ "#2E62F1" ] : [ "#537EF5", "#2E62F1", "#2358D8", "#184FBF", "#0C45A6", "#013B8E" ]), getColorHex: field(options.colorHexField), getFontFamily: field(options.fontFamily || "sans-serif"), rotateList: options.rotateList || [ 0 ], getPadding: field(options.padding || 1), getFontStyle: field(options.fontStyle || "normal"), getFontWeight: field(options.fontWeight || "normal"), getFontOpacity: options.fontOpacity ? field(options.fontOpacity) : () => 1 }; initFontSizeScale(data, wordsConfig, segmentationOutput); const layoutConfig = { size: options.size, ratio: options.ratio || .8, shapeUrl: options.shape, random: void 0 === options.random || options.random, textLayoutTimes: null !== (_a = options.textLayoutTimes) && void 0 !== _a ? _a : 3, removeWhiteBorder: options.removeWhiteBorder, layoutMode: null !== (_b = options.layoutMode) && void 0 !== _b ? _b : "default", fontSizeShrinkFactor: null !== (_c = options.fontSizeShrinkFactor) && void 0 !== _c ? _c : .8, stepFactor: null !== (_d = options.stepFactor) && void 0 !== _d ? _d : 1, importantWordCount: null !== (_e = options.importantWordCount) && void 0 !== _e ? _e : 10, globalShinkLimit: options.globalShinkLimit || .2, fontSizeEnlargeFactor: null !== (_f = options.fontSizeEnlargeFactor) && void 0 !== _f ? _f : 1.5, fillingRatio: null !== (_g = options.fillingRatio) && void 0 !== _g ? _g : .7, fillingTimes: null !== (_h = options.fillingTimes) && void 0 !== _h ? _h : 4, fillingXStep: options.fillingXRatioStep ? Math.max(Math.floor(options.size[0] * options.fillingXRatioStep), 1) : null !== (_j = options.fillingXStep) && void 0 !== _j ? _j : 4, fillingYStep: options.fillingYRatioStep ? Math.max(Math.floor(options.size[1] * options.fillingYRatioStep), 1) : null !== (_k = options.fillingYStep) && void 0 !== _k ? _k : 4, fillingInitialFontSize: options.fillingInitialFontSize, fillingDeltaFontSize: options.fillingDeltaFontSize, fillingInitialOpacity: null !== (_l = options.fillingInitialOpacity) && void 0 !== _l ? _l : .8, fillingDeltaOpacity: null !== (_m = options.fillingDeltaOpacity) && void 0 !== _m ? _m : .05, getFillingFontFamily: field(options.fillingFontFamily || "sans-serif"), getFillingFontStyle: field(options.fillingFontStyle || "normal"), getFillingFontWeight: field(options.fillingFontWeight || "normal"), getFillingPadding: field(null !== (_o = options.fillingPadding) && void 0 !== _o ? _o : .4), fillingRotateList: null !== (_p = options.fillingRotateList) && void 0 !== _p ? _p : [ 0, 90 ], fillingDeltaFontSizeFactor: null !== (_q = options.fillingDeltaFontSizeFactor) && void 0 !== _q ? _q : .2, fillingColorList: options.fillingColorList || [ "#537EF5" ], sameColorList: !1, minInitFontSize: null !== (_r = options.minInitFontSize) && void 0 !== _r ? _r : 10, minFontSize: null !== (_s = options.minFontSize) && void 0 !== _s ? _s : 4, minFillFontSize: null !== (_t = options.minFillFontSize) && void 0 !== _t ? _t : 2 }, sameColorList = (0, util_1.colorListEqual)(wordsConfig.colorList, layoutConfig.fillingColorList); layoutConfig.sameColorList = sameColorList, initColorScale(data, wordsConfig, layoutConfig, options), initFillingWordsFontSize(data, wordsConfig, layoutConfig, segmentationOutput); const {getText: getText, getFontFamily: getFontFamily, getFontStyle: getFontStyle, getFontWeight: getFontWeight, getPadding: getPadding, getColor: getColor, getFillingColor: getFillingColor, getColorHex: getColorHex, fontSizeScale: fontSizeScale, colorScale: colorScale, fillingColorScale: fillingColorScale, getFontOpacity: getFontOpacity, rotateList: rotateList} = wordsConfig, words = data.map((datum => { var _a, _b; return { x: 0, y: 0, weight: 0, text: getText(datum), fontFamily: getFontFamily(datum), fontWeight: getFontWeight(datum), fontStyle: getFontStyle(datum), rotate: rotateList[~~(segmentationInput.randomGenerator() * rotateList.length)], fontSize: Math.max(layoutConfig.minInitFontSize, ~~fontSizeScale(datum)), opacity: getFontOpacity(datum), padding: getPadding(datum), color: getColorHex && getColorHex(datum) || colorScale && colorScale(getColor(datum)) || "black", fillingColor: !getFillingColor || (null === (_a = options.colorField) || void 0 === _a ? void 0 : _a.field) === (null === (_b = options.fillingColorField) || void 0 === _b ? void 0 : _b.field) && sameColorList ? void 0 : getColorHex && getColorHex(datum) || fillingColorScale && fillingColorScale(getFillingColor(datum)) || "black", datum: datum, visible: !0, hasPlaced: !1 }; })), wordsMaxFontSize = (0, vutils_1.maxInArray)(words.map((word => word.fontSize))); words.forEach((word => word.weight = word.fontSize / wordsMaxFontSize)), words.sort(((a, b) => b.weight - a.weight)); const {fillingWords: fillingWords, successedWords: successedWords, failedWords: failedWords} = (0, cloud_shape_layout_1.default)(words, layoutConfig, segmentationOutput), textKey = null !== (_v = null === (_u = options.text) || void 0 === _u ? void 0 : _u.field) && void 0 !== _v ? _v : "textKey", dataIndexKey = null !== (_w = options.dataIndexKey) && void 0 !== _w ? _w : "defaultDataIndexKey", as = options.as ? Object.assign(Object.assign({}, OUTPUT), options.as) : OUTPUT; let w, t; const modKeywords = []; for (let i = 0; i < words.length; ++i) w = words[i], t = w.datum, t[as.x] = w.x, t[as.y] = w.y, t[as.fontFamily] = w.fontFamily, t[as.fontSize] = w.fontSize, t[as.fontStyle] = w.fontStyle, t[as.fontWeight] = w.fontWeight, t[as.angle] = (0, vutils_1.degreeToRadian)(w.rotate), t[as.opacity] = w.opacity, t[as.visible] = w.visible, t[as.isFillingWord] = !1, t[as.color] = w.color, t[dataIndexKey] = `${w.text}_${i}_keyword`, modKeywords.push(t); const fillingWordsData = []; if (fillingWords.forEach(((word, index) => { var _a, _b; const t = Object.assign({}, word.datum); t[as.x] = word.x, t[as.y] = word.y, t[as.fontFamily] = word.fontFamily, t[as.fontSize] = word.fontSize, t[as.fontStyle] = word.fontStyle, t[as.fontWeight] = word.fontWeight, t[as.angle] = (0, vutils_1.degreeToRadian)(word.rotate), t[as.opacity] = word.opacity, t[as.visible] = word.visible, t[as.isFillingWord] = !0, t[as.color] = getFillingColor ? (null === (_a = options.colorField) || void 0 === _a ? void 0 : _a.field) === (null === (_b = options.fillingColorField) || void 0 === _b ? void 0 : _b.field) && sameColorList ? word.color : word.fillingColor : layoutConfig.fillingColorList[~~(segmentationInput.randomGenerator() * layoutConfig.fillingColorList.length)], t[textKey] = word.text, t[dataIndexKey] = `${word.text}_${index}_fillingWords`, fillingWordsData.push(t); })), this.view && this.view.emit) { this.view.emit(util_1.WORDCLOUD_SHAPE_HOOK_EVENT.AFTER_WORDCLOUD_SHAPE_LAYOUT, { successedWords: successedWords, failedWords: failedWords }); const stage = null === (_x = this.view.renderer) || void 0 === _x ? void 0 : _x.stage(); stage && stage.hooks.afterRender.tap(util_1.WORDCLOUD_SHAPE_HOOK_EVENT.AFTER_WORDCLOUD_SHAPE_DRAW, (() => { this.view.emit(util_1.WORDCLOUD_SHAPE_HOOK_EVENT.AFTER_WORDCLOUD_SHAPE_DRAW, { successedWords: successedWords, failedWords: failedWords }), stage.hooks.afterRender.unTap(util_1.WORDCLOUD_SHAPE_HOOK_EVENT.AFTER_WORDCLOUD_SHAPE_DRAW); })); } this.progressiveResult = modKeywords.concat(fillingWordsData); } release() { this.segmentationInput = null, this.data = null, this.progressiveResult = null, this.options = null; } } exports.Layout = Layout; const initColorScale = (data, wordsConfig, layoutConfig, options) => { var _a, _b, _c, _d; const {colorMode: colorMode, getColor: getColor, getFillingColor: getFillingColor} = wordsConfig, {sameColorList: sameColorList} = layoutConfig; let colorScale, fillingColorScale, colorList = wordsConfig.colorList, fillingColorList = layoutConfig.fillingColorList; if ("ordinal" === colorMode) { const uniqueColorField = data.map((word => getColor(word))); if (colorScale = datum => (new vscale_1.OrdinalScale).domain(uniqueColorField).range(colorList).scale(datum), getFillingColor && ((null === (_a = options.colorField) || void 0 === _a ? void 0 : _a.field) !== (null === (_b = options.fillingColorField) || void 0 === _b ? void 0 : _b.field) || !sameColorList)) { const uniquefillingColorField = data.map((datum => getFillingColor(datum))); fillingColorScale = datum => (new vscale_1.OrdinalScale).domain(uniquefillingColorField).range(fillingColorList).scale(datum); } } else { 1 === colorList.length && (colorList = [ colorList[0], colorList[0] ]); const valueScale = (new vscale_1.LinearScale).domain(extent(getColor, data)).range(colorList); if (colorScale = i => valueScale.scale(i), getFillingColor && ((null === (_c = options.colorField) || void 0 === _c ? void 0 : _c.field) !== (null === (_d = options.fillingColorField) || void 0 === _d ? void 0 : _d.field) || !sameColorList)) { 1 === fillingColorList.length && (fillingColorList = [ fillingColorList[0], fillingColorList[0] ]); const fillingValueScale = (new vscale_1.LinearScale).domain(extent(getFillingColor, data)).range(fillingColorList); fillingColorScale = i => fillingValueScale.scale(i); } } Object.assign(wordsConfig, { colorScale: colorScale, fillingColorScale: fillingColorScale }); }, initFontSizeScale = (data, wordsConfig, segmentationOutput) => { let {fontSizeRange: range} = wordsConfig; const {getFontSize: getFontSize, getText: getText} = wordsConfig; let fontSizeScale; if (getFontSize) { if (getFontSize && range) { const sizeScale = (new vscale_1.SqrtScale).domain(extent(getFontSize, data)).range(range); fontSizeScale = datum => sizeScale.scale(getFontSize(datum)); } else if (getFontSize && (0, vutils_1.isFunction)(getFontSize) && !range) { const a = .5, [min, max] = extent(getFontSize, data), words = data.map((datum => ({ text: getText(datum), value: getFontSize(datum), weight: max === min ? 1 : (getFontSize(datum) - min) / (max - min) }))), x = getInitialFontSize(words, segmentationOutput, !0); range = [ ~~(a * x), ~~x ]; const sizeScale = (new vscale_1.SqrtScale).domain(extent(getFontSize, data)).range(range); fontSizeScale = datum => sizeScale.scale(getFontSize(datum)); } } else { const words = data.map((word => ({ text: getText(word) }))), x = getInitialFontSize(words, segmentationOutput, !1); fontSizeScale = (0, util_1.functor)(x); } Object.assign(wordsConfig, { getFontSize: getFontSize, fontSizeRange: range, fontSizeScale: fontSizeScale }); }, getInitialFontSize = (words, segmentationOutput, weight) => { const shapeArea = segmentationOutput.shapeArea, ratio = segmentationOutput.ratio, regions = segmentationOutput.segmentation.regions, shapeSizeLimitTextLength = Math.ceil(Math.sqrt(shapeArea) / 12), wordArea = words.reduce(((acc, word) => { const textLength = (0, util_1.calTextLength)(word.text); return textLength < shapeSizeLimitTextLength ? acc + textLength * (weight ? (.5 + .5 * word.weight) ** 2 : 1) : acc; }), 0); if (0 === wordArea) return 12; let x = 20; if (1 === regions.length) x = Math.sqrt(ratio * (shapeArea / (1.7 * wordArea))); else { const xArr = []; for (let i = 0; i < regions.length; i++) { const regionArea = regions[i].area, regionAspect = regions[i].ratio, regionRatio = regionArea / shapeArea; if (regionRatio < .1) continue; const regionWordArea = regionRatio * (wordArea * (regionAspect < 1 ? 2.7 - regionAspect : 1.7)), x = Math.sqrt(ratio * (regionArea / regionWordArea)); xArr.push(x); } x = xArr.length ? Math.min(...xArr) : Math.sqrt(ratio * (shapeArea / (1.7 * wordArea))); } return x; }; function initFillingWordsFontSize(data, wordsConfig, layoutConfig, segmentationOutput) { const {getText: getText} = wordsConfig; let {fillingInitialFontSize: fillingInitialFontSize, fillingDeltaFontSize: fillingDeltaFontSize} = layoutConfig; const {fillingRatio: fillingRatio} = layoutConfig, shapeSizeLimitTextLength = Math.ceil(Math.sqrt(segmentationOutput.shapeArea) / 4); if (!fillingInitialFontSize || !fillingDeltaFontSize) { const a = fillingRatio / 100, averageLength = data.reduce(((acc, word) => { const length = (0, util_1.calTextLength)(getText(word)); return length > shapeSizeLimitTextLength ? acc : acc + length; }), 0) / data.length; let fontSize; if (0 === averageLength) fontSize = 8; else { const area = .2 * segmentationOutput.shapeArea; fontSize = Math.sqrt(a * (area / averageLength)); } fillingInitialFontSize = ~~fontSize, fillingDeltaFontSize = fontSize * layoutConfig.fillingDeltaFontSizeFactor, Object.assign(layoutConfig, { fillingInitialFontSize: fillingInitialFontSize, fillingDeltaFontSize: fillingDeltaFontSize }); } } const extent = (field, data) => { let min = 1 / 0, max = -1 / 0; const n = data.length; let v; for (let i = 0; i < n; ++i) v = (0, vutils_1.toNumber)(field(data[i])), v < min && (min = v), v > max && (max = v); return 1 === data.length && min === max && (min -= 1e4), [ min, max ]; }, field = option => option ? "string" == typeof option || "number" == typeof option ? () => option : (0, vutils_1.isFunction)(option) ? option : datum => datum[option.field] : null; //# sourceMappingURL=layout.js.map