diff --git a/README.md b/README.md index d992671..9cbe380 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,6 @@ -# Word-cloud +# G-Interface -[Live demo here](https://x-image-privacy.github.io/word-cloud/) - -This repository creates a word cloud from a list of words. - -![WordCloud Demo](docs/word-cloud_v2.png) - -# Documentation - -You can find the documentation about the word cloud in [this file](technical_descriptions.md) +This repository provides an interface for interacting with graphs for explainability purposes. # Installation @@ -24,7 +16,7 @@ You can install the project dependencies using: yarn ``` -You should now be ready to start developing the app +You should now be ready to start developing the app. ## Usage @@ -33,9 +25,3 @@ To start the app: ```bash yarn dev ``` - -To run all automated tests: - -```bash -yarn test -``` diff --git a/docs/circleintervals.png b/docs/circleintervals.png deleted file mode 100644 index 95e38ea..0000000 Binary files a/docs/circleintervals.png and /dev/null differ diff --git a/docs/circleparent.png b/docs/circleparent.png deleted file mode 100644 index ccd2416..0000000 Binary files a/docs/circleparent.png and /dev/null differ diff --git a/docs/collision.png b/docs/collision.png deleted file mode 100644 index bd60196..0000000 Binary files a/docs/collision.png and /dev/null differ diff --git a/docs/moveword.drawio.png b/docs/moveword.drawio.png deleted file mode 100644 index 57aa992..0000000 Binary files a/docs/moveword.drawio.png and /dev/null differ diff --git a/docs/word-cloud_v2.png b/docs/word-cloud_v2.png deleted file mode 100644 index 62ec1ba..0000000 Binary files a/docs/word-cloud_v2.png and /dev/null differ diff --git a/docs/word_cloud.png b/docs/word_cloud.png deleted file mode 100644 index 87650d7..0000000 Binary files a/docs/word_cloud.png and /dev/null differ diff --git a/package.json b/package.json index dfff559..40646a3 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,16 @@ { - "name": "@x-image-privacy/wordcloud", + "name": "@graphnex/g-interface", "private": true, "version": "0.0.0", "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "contributors": [ + "Basile Spaenlehauer", + "Roxane Burri", + "Juan Carlos Farah" + ], "scripts": { "dev": "vite", "build:app": "tsc && vite build", diff --git a/public/privacy-use-case/categories.json b/public/privacy-use-case/categories.json deleted file mode 100644 index 84ede21..0000000 --- a/public/privacy-use-case/categories.json +++ /dev/null @@ -1,166 +0,0 @@ -[ - { - "elements": [ - 41, - 42, - 43, - 44, - 45, - 46, - 47 - ], - "id": "c0", - "name": "kitchen", - "score": 0.036130096244321175 - }, - { - "elements": [ - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10 - ], - "id": "c1", - "name": "vehicle", - "score": 0.7918275929910786 - }, - { - "elements": [ - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57 - ], - "id": "c2", - "name": "food", - "score": 0.09470090119799135 - }, - { - "elements": [ - 26, - 27, - 28, - 29, - 30 - ], - "id": "c3", - "name": "accessory", - "score": 0.2571571585102601 - }, - { - "elements": [ - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40 - ], - "id": "c4", - "name": "sports", - "score": 0.7913268668078243 - }, - { - "elements": [ - 64, - 65, - 66, - 67, - 68, - 69 - ], - "id": "c5", - "name": "electronic", - "score": 0.9939624780369383 - }, - { - "elements": [ - 75, - 76, - 77, - 78, - 79, - 80, - 81 - ], - "id": "c6", - "name": "indoor", - "score": 0.9562275883265232 - }, - { - "elements": [ - 11, - 12, - 13, - 14, - 15 - ], - "id": "c7", - "name": "outdoor", - "score": 0.30679977637448674 - }, - { - "elements": [ - 70, - 71, - 72, - 73, - 74 - ], - "id": "c8", - "name": "appliance", - "score": 0.060329260950528374 - }, - { - "elements": [ - 58, - 59, - 60, - 61, - 62, - 63 - ], - "id": "c9", - "name": "furniture", - "score": 0.8125364179111876 - }, - { - "elements": [ - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25 - ], - "id": "c10", - "name": "animal", - "score": 0.8665025102191475 - }, - { - "elements": [ - 2 - ], - "id": "c11", - "name": "person", - "score": 0.577010028326078 - } -] \ No newline at end of file diff --git a/public/privacy-use-case/nodes.json b/public/privacy-use-case/nodes.json deleted file mode 100644 index 377e420..0000000 --- a/public/privacy-use-case/nodes.json +++ /dev/null @@ -1,412 +0,0 @@ -[ - { - "id": 0, - "name": "private", - "score": 0.3997 - }, - { - "id": 1, - "name": "public", - "score": 0.0925 - }, - { - "id": 2, - "name": "person", - "score": 0.8306 - }, - { - "id": 3, - "name": "bicycle", - "score": 0.5445 - }, - { - "id": 4, - "name": "car", - "score": 0.8041 - }, - { - "id": 5, - "name": "motorbike", - "score": 0.8976 - }, - { - "id": 6, - "name": "aeroplane", - "score": 0.0568 - }, - { - "id": 7, - "name": "bus", - "score": 0.2308 - }, - { - "id": 8, - "name": "train", - "score": 0.285 - }, - { - "id": 9, - "name": "truck", - "score": 0.5052 - }, - { - "id": 10, - "name": "boat", - "score": 0.156 - }, - { - "id": 11, - "name": "traffic light", - "score": 0.251 - }, - { - "id": 12, - "name": "fire hydrant", - "score": 0.155 - }, - { - "id": 13, - "name": "stop sign", - "score": 0.1279 - }, - { - "id": 14, - "name": "parking meter", - "score": 0.4257 - }, - { - "id": 15, - "name": "bench", - "score": 0.6162 - }, - { - "id": 16, - "name": "bird", - "score": 0.6427 - }, - { - "id": 17, - "name": "cat", - "score": 0.2701 - }, - { - "id": 18, - "name": "dog", - "score": 0.1785 - }, - { - "id": 19, - "name": "horse", - "score": 0.0419 - }, - { - "id": 20, - "name": "sheep", - "score": 0.1666 - }, - { - "id": 21, - "name": "cow", - "score": 0.4187 - }, - { - "id": 22, - "name": "elephant", - "score": 0.916 - }, - { - "id": 23, - "name": "bear", - "score": 0.0699 - }, - { - "id": 24, - "name": "zebra", - "score": 0.6559 - }, - { - "id": 25, - "name": "giraffe", - "score": 0.4271 - }, - { - "id": 26, - "name": "backpack", - "score": 0.2111 - }, - { - "id": 27, - "name": "umbrella", - "score": 0.7296 - }, - { - "id": 28, - "name": "handbag", - "score": 0.2427 - }, - { - "id": 29, - "name": "tie", - "score": 0.392 - }, - { - "id": 30, - "name": "suitcase", - "score": 0.8415 - }, - { - "id": 31, - "name": "frisbee", - "score": 0.2431 - }, - { - "id": 32, - "name": "skis", - "score": 0.1585 - }, - { - "id": 33, - "name": "snowboard", - "score": 0.8859 - }, - { - "id": 34, - "name": "sports ball", - "score": 0.819 - }, - { - "id": 35, - "name": "kite", - "score": 0.0196 - }, - { - "id": 36, - "name": "baseball bat", - "score": 0.2863 - }, - { - "id": 37, - "name": "baseball glove", - "score": 0.4496 - }, - { - "id": 38, - "name": "skateboard", - "score": 0.1255 - }, - { - "id": 39, - "name": "surfboard", - "score": 0.4843 - }, - { - "id": 40, - "name": "tennis racket", - "score": 0.4433 - }, - { - "id": 41, - "name": "bottle", - "score": 0.8865 - }, - { - "id": 42, - "name": "wine glass", - "score": 0.3414 - }, - { - "id": 43, - "name": "cup", - "score": 0.3147 - }, - { - "id": 44, - "name": "fork", - "score": 0.2458 - }, - { - "id": 45, - "name": "knife", - "score": 0.3125 - }, - { - "id": 46, - "name": "spoon", - "score": 0.3645 - }, - { - "id": 47, - "name": "bowl", - "score": 0.1657 - }, - { - "id": 48, - "name": "banana", - "score": 0.8322 - }, - { - "id": 49, - "name": "apple", - "score": 0.7334 - }, - { - "id": 50, - "name": "sandwich", - "score": 0.6392 - }, - { - "id": 51, - "name": "orange", - "score": 0.1126 - }, - { - "id": 52, - "name": "broccoli", - "score": 0.4412 - }, - { - "id": 53, - "name": "carrot", - "score": 0.0435 - }, - { - "id": 54, - "name": "hot dog", - "score": 0.3056 - }, - { - "id": 55, - "name": "pizza", - "score": 0.0254 - }, - { - "id": 56, - "name": "donut", - "score": 0.9916 - }, - { - "id": 57, - "name": "cake", - "score": 0.3759 - }, - { - "id": 58, - "name": "chair", - "score": 0.7682 - }, - { - "id": 59, - "name": "sofa", - "score": 0.6534 - }, - { - "id": 60, - "name": "pottedplant", - "score": 0.5422 - }, - { - "id": 61, - "name": "bed", - "score": 0.7645 - }, - { - "id": 62, - "name": "diningtable", - "score": 0.6762 - }, - { - "id": 63, - "name": "toilet", - "score": 0.8873 - }, - { - "id": 64, - "name": "tvmonitor", - "score": 0.5894 - }, - { - "id": 65, - "name": "laptop", - "score": 0.7115 - }, - { - "id": 66, - "name": "mouse", - "score": 0.5317 - }, - { - "id": 67, - "name": "remote", - "score": 0.2525 - }, - { - "id": 68, - "name": "keyboard", - "score": 0.6879 - }, - { - "id": 69, - "name": "cell phone", - "score": 0.8533 - }, - { - "id": 70, - "name": "microwave", - "score": 0.6426 - }, - { - "id": 71, - "name": "oven", - "score": 0.9994 - }, - { - "id": 72, - "name": "toaster", - "score": 0.4629 - }, - { - "id": 73, - "name": "sink", - "score": 0.4069 - }, - { - "id": 74, - "name": "refrigerator", - "score": 0.7855 - }, - { - "id": 75, - "name": "book", - "score": 0.9992 - }, - { - "id": 76, - "name": "clock", - "score": 0.1941 - }, - { - "id": 77, - "name": "vase", - "score": 0.0018 - }, - { - "id": 78, - "name": "scissors", - "score": 0.9892 - }, - { - "id": 79, - "name": "teddy bear", - "score": 0.179 - }, - { - "id": 80, - "name": "hair drier", - "score": 0.33 - }, - { - "id": 81, - "name": "toothbrush", - "score": 0.509 - } -] \ No newline at end of file diff --git a/src/WordCloud/WordBounds.tsx b/src/WordCloud/WordBounds.tsx deleted file mode 100644 index bee4206..0000000 --- a/src/WordCloud/WordBounds.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { WordCloudData } from "./components/types"; - -type Props = { - showWordBounds: boolean; - wordClouds?: WordCloudData; -}; - -const WordBounds = ({ - wordClouds, - showWordBounds, -}: Props): JSX.Element | null => { - if (!showWordBounds) { - return null; - } - return ( - <> - {wordClouds?.map((wordCloud) => - wordCloud.words.map(({ id, text, rect }) => { - return ( - - - - {text} - - - ); - }) - )} - - ); -}; - -export default WordBounds; diff --git a/src/WordCloud/WordCloud.tsx b/src/WordCloud/WordCloud.tsx deleted file mode 100644 index 7ab9d9c..0000000 --- a/src/WordCloud/WordCloud.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { - boundParent, - computeFontSize, - futureSpiralPosition, - getBoundingWordCloud, - slideWords, -} from "./components/utils"; - -import { MARGIN_HEIGHT, MARGIN_WIDTH } from "./components/constants"; -import { useEffect, useState } from "react"; -import { ExplanationData } from "./types"; -import WordBounds from "./WordBounds"; -import { PlacedWordCloud, Word, WordCloudData } from "./components/types"; -import CategoryCloudDisplay from "./components/CategoryCloudDisplay"; - -const useWordCloudLayout = (wordClouds: WordCloudData): PlacedWordCloud => { - wordClouds.forEach((cloud, cloudIdx, originalWordClouds) => { - const newWordCloud = cloud.words.slice(1).reduce( - (placedWords, word) => { - const futureItem = futureSpiralPosition( - word.rect, - placedWords.map(({ rect }) => rect) - ); - return [...placedWords, { ...word, rect: futureItem }]; - }, - [cloud.words[0]] - ); - originalWordClouds[cloudIdx].words = newWordCloud; - }); - - const placedWordCloud: PlacedWordCloud = wordClouds.map((cloud) => ({ - ...cloud, - bound: getBoundingWordCloud(cloud.words), - })); - - placedWordCloud.sort((cloudA, cloudB) => { - return cloudA.bound.width * cloudA.bound.height >= - cloudB.bound.width * cloudB.bound.height - ? -1 - : 1; - }); - - const newWordClouds = placedWordCloud.reduce( - (accWordClouds, cloud, cloudIdx) => { - if (cloudIdx === 0) { - accWordClouds.push({ - ...cloud, - bound: { - ...cloud.bound, - x: -cloud.bound.width / 2, - y: -cloud.bound.height / 2, - }, - }); - return accWordClouds; - } - const previousBounds = accWordClouds.map(({ bound }) => bound); - - const futurePos = futureSpiralPosition(cloud.bound, previousBounds); - - const newCloud = { - ...cloud, - bound: futurePos, - words: slideWords(cloud.words, { - x: futurePos.x - cloud.bound.x, - y: futurePos.y - cloud.bound.y, - }), - }; - accWordClouds.push(newCloud); - return accWordClouds; - }, - [] - ); - - return newWordClouds; - // return placedWordCloud; -}; - -const getHiddenElementId = (id: string) => `hidden-${id}`; - -type Props = { - data?: ExplanationData; - width?: string; - height?: string; - hideWords?: boolean; - showOrigin?: boolean; - showBounds?: boolean; - showWordBounds?: boolean; -}; - -const Wordcloud = ({ - data, - height = "100%", - width = "100%", - hideWords = false, - showOrigin = false, - showBounds = false, - showWordBounds = false, -}: Props) => { - const [wordClouds, setWordClouds] = useState(); - - // casting is fine here https://codereview.stackexchange.com/questions/135363/filtering-undefined-elements-out-of-an-array - const rects = wordClouds - ?.map((wordCloud) => wordCloud.words.map((w) => w.rect)) - .reduce((acc, wordCloud) => [...acc, ...wordCloud], []); - const bounds = wordClouds?.map((wordCloud) => ({ - id: wordCloud.category, - bound: boundParent(wordCloud.words.map((w) => w.rect)), - })); - const bound = rects?.length - ? boundParent(rects) - : { - x: -100, - y: -100, - width: 200, - height: 200, - }; - - useEffect(() => { - if (data) { - // get rectangles from data - const wordCloudsWithRectangles = data.map( - ({ category, words: prevWords }) => { - // get rectangle from canvas - const words = prevWords.map((w) => { - const rect = document - .getElementById(getHiddenElementId(w.id)) - ?.getBoundingClientRect() || { x: 0, y: 0, width: 0, height: 0 }; - const width = rect?.width + MARGIN_WIDTH; - const height = rect?.height + MARGIN_HEIGHT; - return { - ...w, - rect: { - // center rect in the middle - x: -width / 2, - y: -height / 2, - width, - height, - }, - }; - }); - // sort rectangles based on their coefficient - words.sort((a, b) => (a.coef > b.coef ? -1 : 1)); - - return { - category, - words, - }; - } - ); - - const layedOutWordClouds = useWordCloudLayout(wordCloudsWithRectangles); - - // set rectangles and let hook re-render - setWordClouds(layedOutWordClouds); - } - }, [data]); - - return ( - <> - - {data?.map((c) => - c.words.map((w) => ( - - {w.text} - - )) - )} - - - {showOrigin && ( - <> - - - - )} - {bounds?.map(({ id, bound: b }) => ( - <> - - Category: {id}; Score: TBA - - - ))} - {wordClouds?.map((wordCloud) => ( - - {hideWords ? ( - - ) : ( - wordCloud.words.map((word) => { - const fontSize = computeFontSize(word.coef); - return ( - - {word.text} - - Word: {word.text}; Score: {word.coef} - - - ); - }) - )} - - ))} - - - - ); -}; -export default Wordcloud; diff --git a/src/WordCloud/components/CategoryCloudDisplay.tsx b/src/WordCloud/components/CategoryCloudDisplay.tsx deleted file mode 100644 index dd45e45..0000000 --- a/src/WordCloud/components/CategoryCloudDisplay.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { CategoryCloud, Word } from "./types"; - -type Props = { - wordCloud: CategoryCloud; -}; - -const CategoryCloudDisplay = ({ wordCloud }: Props) => { - const centerRect = wordCloud.words[0].rect; - const { category } = wordCloud; - const x = centerRect.x + centerRect.width / 2; - const y = centerRect.y + centerRect.height / 2; - return ( - - {category} - Category: {category}; Score: TBA - - ); -}; -export default CategoryCloudDisplay; diff --git a/src/WordCloud/components/constants.ts b/src/WordCloud/components/constants.ts deleted file mode 100644 index 07fc521..0000000 --- a/src/WordCloud/components/constants.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const CONTAINER_WIDTH = 700; -export const CONTAINER_HEIGHT = 500; - -export const CENTER_Y = CONTAINER_HEIGHT / 2; -export const CENTER_X = CONTAINER_WIDTH / 2; - -export const DEFAULT_RECT = { - x: CENTER_X, - y: CENTER_Y, - width: 10, - height: 10, -}; -export const MARGIN_WIDTH = 10; -export const MARGIN_HEIGHT = -5; - -export const WORD_CLOUD_MARGIN_WIDTH = 40; -export const WORD_CLOUD_MARGIN_HEIGHT = 40; - -export const NUMBER_OF_INTERVALS = 6; - -export const CUT_OFF = 0.5; - -export const MAX_FONT_SIZE = 20; -export const MIN_FONT_SIZE = 6; diff --git a/src/WordCloud/components/layout-utils.txt b/src/WordCloud/components/layout-utils.txt deleted file mode 100644 index 4b31b9f..0000000 --- a/src/WordCloud/components/layout-utils.txt +++ /dev/null @@ -1,457 +0,0 @@ -// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/ -// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf - -var dispatch = require("d3-dispatch").dispatch; - -var cloudRadians = Math.PI / 180, - cw = (1 << 11) >> 5, - ch = 1 << 11; - -module.exports = function () { - var size = [256, 256], - text = cloudText, - font = cloudFont, - fontSize = cloudFontSize, - fontStyle = cloudFontNormal, - fontWeight = cloudFontNormal, - rotate = cloudRotate, - padding = cloudPadding, - spiral = archimedeanSpiral, - words = [], - timeInterval = Infinity, - event = dispatch("word", "end"), - timer = null, - random = Math.random, - cloud = {}, - canvas = cloudCanvas; - - cloud.canvas = function (_) { - return arguments.length ? ((canvas = functor(_)), cloud) : canvas; - }; - - cloud.start = function () { - var contextAndRatio = getContext(canvas()), - board = zeroArray((size[0] >> 5) * size[1]), - bounds = null, - n = words.length, - i = -1, - tags = [], - data = words - .map(function (d, i) { - d.text = text.call(this, d, i); - d.font = font.call(this, d, i); - d.style = fontStyle.call(this, d, i); - d.weight = fontWeight.call(this, d, i); - d.rotate = rotate.call(this, d, i); - d.size = ~~fontSize.call(this, d, i); - d.padding = padding.call(this, d, i); - return d; - }) - .sort(function (a, b) { - return b.size - a.size; - }); - - if (timer) clearInterval(timer); - timer = setInterval(step, 0); - step(); - - return cloud; - - function step() { - var start = Date.now(); - while (Date.now() - start < timeInterval && ++i < n && timer) { - var d = data[i]; - d.x = (size[0] * (random() + 0.5)) >> 1; - d.y = (size[1] * (random() + 0.5)) >> 1; - cloudSprite(contextAndRatio, d, data, i); - if (d.hasText && place(board, d, bounds)) { - tags.push(d); - event.call("word", cloud, d); - if (bounds) cloudBounds(bounds, d); - else - bounds = [ - { x: d.x + d.x0, y: d.y + d.y0 }, - { x: d.x + d.x1, y: d.y + d.y1 }, - ]; - // Temporary hack - d.x -= size[0] >> 1; - d.y -= size[1] >> 1; - } - } - if (i >= n) { - cloud.stop(); - event.call("end", cloud, tags, bounds); - } - } - }; - - cloud.stop = function () { - if (timer) { - clearInterval(timer); - timer = null; - } - return cloud; - }; - - function getContext(canvas) { - canvas.width = canvas.height = 1; - var ratio = Math.sqrt( - // divide by 4 - canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2 - ); - // 32 - canvas.width = (cw << 5) / ratio; - canvas.height = ch / ratio; - - var context = canvas.getContext("2d"); - // fill canvas red - context.fillStyle = context.strokeStyle = "red"; - context.textAlign = "center"; - - return { context: context, ratio: ratio }; - } - - function place(board, tag, bounds) { - var perimeter = [ - { x: 0, y: 0 }, - { x: size[0], y: size[1] }, - ], - startX = tag.x, - startY = tag.y, - maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]), - s = spiral(size), - dt = random() < 0.5 ? 1 : -1, - t = -dt, - dxdy, - dx, - dy; - // get the new x and y values along the spiral - while ((dxdy = s((t += dt)))) { - // no idea why they use the double negation of the bits - dx = ~~dxdy[0]; - dy = ~~dxdy[1]; - - // if the smallest displacement on x or y is bigger than the maxDelta -> exit - if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break; - - // increase the position of the tag by the displacement - tag.x = startX + dx; - tag.y = startY + dy; - - // in case the tag gets out of the screen skip to next iteration - if ( - tag.x + tag.x0 < 0 || - tag.y + tag.y0 < 0 || - tag.x + tag.x1 > size[0] || - tag.y + tag.y1 > size[1] - ) - continue; - // TODO only check for collisions within current bounds. - if (!bounds || !cloudCollide(tag, board, size[0])) { - if (!bounds || collideRects(tag, bounds)) { - var sprite = tag.sprite, - w = tag.width >> 5, - sw = size[0] >> 5, - lx = tag.x - (w << 4), - sx = lx & 0x7f, - msx = 32 - sx, - h = tag.y1 - tag.y0, - x = (tag.y + tag.y0) * sw + (lx >> 5), - last; - for (var j = 0; j < h; j++) { - last = 0; - for (var i = 0; i <= w; i++) { - board[x + i] |= - (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0); - } - x += sw; - } - delete tag.sprite; - return true; - } - } - } - return false; - } - - cloud.timeInterval = function (_) { - return arguments.length - ? ((timeInterval = _ == null ? Infinity : _), cloud) - : timeInterval; - }; - - cloud.words = function (_) { - return arguments.length ? ((words = _), cloud) : words; - }; - - cloud.size = function (_) { - return arguments.length ? ((size = [+_[0], +_[1]]), cloud) : size; - }; - - cloud.font = function (_) { - return arguments.length ? ((font = functor(_)), cloud) : font; - }; - - cloud.fontStyle = function (_) { - return arguments.length ? ((fontStyle = functor(_)), cloud) : fontStyle; - }; - - cloud.fontWeight = function (_) { - return arguments.length ? ((fontWeight = functor(_)), cloud) : fontWeight; - }; - - cloud.rotate = function (_) { - return arguments.length ? ((rotate = functor(_)), cloud) : rotate; - }; - - cloud.text = function (_) { - return arguments.length ? ((text = functor(_)), cloud) : text; - }; - - cloud.spiral = function (_) { - return arguments.length ? ((spiral = spirals[_] || _), cloud) : spiral; - }; - - cloud.fontSize = function (_) { - return arguments.length ? ((fontSize = functor(_)), cloud) : fontSize; - }; - - cloud.padding = function (_) { - return arguments.length ? ((padding = functor(_)), cloud) : padding; - }; - - cloud.random = function (_) { - return arguments.length ? ((random = _), cloud) : random; - }; - - cloud.on = function () { - var value = event.on.apply(event, arguments); - return value === event ? cloud : value; - }; - - return cloud; -}; - -function cloudText(d) { - return d.text; -} - -function cloudFont() { - return "serif"; -} - -function cloudFontNormal() { - return "normal"; -} - -function cloudFontSize(d) { - return Math.sqrt(d.value); -} - -function cloudRotate() { - return (~~(Math.random() * 6) - 3) * 30; -} - -function cloudPadding() { - return 1; -} - -// Fetches a monochrome sprite bitmap for the specified text. -// Load in batches for speed. -function cloudSprite(contextAndRatio, d, data, di) { - if (d.sprite) return; - var c = contextAndRatio.context, - ratio = contextAndRatio.ratio; - - c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio); - var x = 0, - y = 0, - maxh = 0, - n = data.length; - --di; - while (++di < n) { - d = data[di]; - c.save(); - c.font = - d.style + - " " + - d.weight + - " " + - ~~((d.size + 1) / ratio) + - "px " + - d.font; - var w = c.measureText(d.text + "m").width * ratio, - h = d.size << 1; - if (d.rotate) { - var sr = Math.sin(d.rotate * cloudRadians), - cr = Math.cos(d.rotate * cloudRadians), - wcr = w * cr, - wsr = w * sr, - hcr = h * cr, - hsr = h * sr; - w = - ((Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5) << 5; - h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr)); - } else { - w = ((w + 0x1f) >> 5) << 5; - } - if (h > maxh) maxh = h; - if (x + w >= cw << 5) { - x = 0; - y += maxh; - maxh = 0; - } - if (y + h >= ch) break; - c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio); - if (d.rotate) c.rotate(d.rotate * cloudRadians); - c.fillText(d.text, 0, 0); - if (d.padding) (c.lineWidth = 2 * d.padding), c.strokeText(d.text, 0, 0); - c.restore(); - d.width = w; - d.height = h; - d.xoff = x; - d.yoff = y; - d.x1 = w >> 1; - d.y1 = h >> 1; - d.x0 = -d.x1; - d.y0 = -d.y1; - d.hasText = true; - x += w; - } - var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data, - sprite = []; - while (--di >= 0) { - d = data[di]; - if (!d.hasText) continue; - var w = d.width, - w32 = w >> 5, - h = d.y1 - d.y0; - // Zero the buffer - for (var i = 0; i < h * w32; i++) sprite[i] = 0; - x = d.xoff; - if (x == null) return; - y = d.yoff; - var seen = 0, - seenRow = -1; - for (var j = 0; j < h; j++) { - for (var i = 0; i < w; i++) { - var k = w32 * j + (i >> 5), - m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] - ? 1 << (31 - (i % 32)) - : 0; - sprite[k] |= m; - seen |= m; - } - if (seen) seenRow = j; - else { - d.y0++; - h--; - j--; - y++; - } - } - d.y1 = d.y0 + seenRow; - d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32); - } -} - -// Use mask-based collision detection. -function cloudCollide(tag, board, sw) { - sw >>= 5; - var sprite = tag.sprite, - w = tag.width >> 5, - lx = tag.x - (w << 4), - sx = lx & 0x7f, - msx = 32 - sx, - h = tag.y1 - tag.y0, - x = (tag.y + tag.y0) * sw + (lx >> 5), - last; - for (var j = 0; j < h; j++) { - last = 0; - for (var i = 0; i <= w; i++) { - if ( - ((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) & - board[x + i] - ) - return true; - } - x += sw; - } - return false; -} - -function cloudBounds(bounds, d) { - var b0 = bounds[0], - b1 = bounds[1]; - if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0; - if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0; - if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1; - if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1; -} - -function collideRects(a, b) { - return ( - a.x + a.x1 > b[0].x && - a.x + a.x0 < b[1].x && - a.y + a.y1 > b[0].y && - a.y + a.y0 < b[1].y - ); -} - -function archimedeanSpiral(size) { - var e = size[0] / size[1]; - return function (t) { - return [e * (t *= 0.1) * Math.cos(t), t * Math.sin(t)]; - }; -} - -function rectangularSpiral(size) { - var dy = 4, - dx = (dy * size[0]) / size[1], - x = 0, - y = 0; - return function (t) { - var sign = t < 0 ? -1 : 1; - // See triangular numbers: T_n = n * (n + 1) / 2. - switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) { - case 0: - x += dx; - break; - case 1: - y += dy; - break; - case 2: - x -= dx; - break; - default: - y -= dy; - break; - } - return [x, y]; - }; -} - -// TODO reuse arrays? -function zeroArray(n) { - var a = [], - i = -1; - while (++i < n) a[i] = 0; - return a; -} - -function cloudCanvas() { - return document.createElement("canvas"); -} - -function functor(d) { - return typeof d === "function" - ? d - : function () { - return d; - }; -} - -var spirals = { - archimedean: archimedeanSpiral, - rectangular: rectangularSpiral, -}; diff --git a/src/WordCloud/components/types.ts b/src/WordCloud/components/types.ts deleted file mode 100644 index 473eccb..0000000 --- a/src/WordCloud/components/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { InputNode } from "../types"; - -export type Rectangle = { - x: number; - y: number; - width: number; - height: number; -}; - -export type Coordinate = { - x: number; - y: number; -}; - -export type CenterCoordinate = { - cx: number; - cy: number; -}; - -export type Circle = { - x: number; - y: number; - radius: number; -}; - -export type CategoryCloud = { - category: string; - words: T[]; -}; -export type PlacedCategoryCloud = CategoryCloud & { bound: B }; - -export type Word = InputNode & { rect: Rectangle }; -export type WordCloudData = CategoryCloud[]; -export type PlacedWordCloud = PlacedCategoryCloud[]; diff --git a/src/WordCloud/components/utils.test.ts b/src/WordCloud/components/utils.test.ts deleted file mode 100644 index 68b1d22..0000000 --- a/src/WordCloud/components/utils.test.ts +++ /dev/null @@ -1,562 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - areCentersTooClose, - allCollision, - getMoveDirection, - getTheCircle, - getDistance, - randomInterval, - cumulativeBins, - slideWords, - boundParent, - rangeWithStep, - getAreaRectangle, - placeFirstWord, -} from "./utils"; -import { Coordinate, Rectangle } from "./types"; -import { CONTAINER_WIDTH } from "./constants"; - -const origin: Coordinate = { - x: 0, - y: 0, -}; - -const originRectangle: Rectangle = { - x: 0, - y: 0, - width: 1, - height: 1, -}; - -const rectangle: Rectangle = { - x: 1, - y: 0, - width: 10, - height: 5, -}; - -describe("areCentersTooClose", () => { - it("No collisions", () => { - expect( - areCentersTooClose({ cx: 0, cy: 2 }, { cx: -5.5, cy: -3 }, 6.5, 4) - ).toBe(false); - expect(areCentersTooClose({ cx: 0, cy: 0 }, { cx: 6, cy: 0 }, 2, 2)).toBe( - false - ); - expect(areCentersTooClose({ cx: 0, cy: 0 }, { cx: 0, cy: 6 }, 2, 2)).toBe( - false - ); - expect(areCentersTooClose({ cx: 0, cy: 6 }, { cx: 0, cy: 0 }, 2, 2)).toBe( - false - ); - }); - it("Collisions in X", () => { - expect(areCentersTooClose({ cx: 0, cy: 0 }, { cx: 2, cy: 0 }, 3, 2)).toBe( - true - ); - expect(areCentersTooClose({ cx: 0, cy: 0 }, { cx: 2, cy: 0 }, 2.1, 2)).toBe( - true - ); - expect(areCentersTooClose({ cx: 2, cy: 0 }, { cx: 0, cy: 0 }, 2.1, 2)).toBe( - true - ); - }); - it("Collisions in Y", () => { - expect(areCentersTooClose({ cx: 0, cy: 0 }, { cx: 0, cy: 2 }, 2, 3)).toBe( - true - ); - expect(areCentersTooClose({ cx: 0, cy: 0 }, { cx: 0, cy: 2 }, 2, 2.1)).toBe( - true - ); - expect(areCentersTooClose({ cx: 0, cy: 2 }, { cx: 0, cy: 0 }, 2, 2.1)).toBe( - true - ); - }); - it("Collisions in X and Y", () => { - expect( - areCentersTooClose({ cx: 0, cy: 0 }, { cx: 2, cy: 2 }, 2.5, 2.5) - ).toBe(true); - expect( - areCentersTooClose({ cx: 0, cy: 0 }, { cx: -2, cy: -2 }, 2.5, 2.5) - ).toBe(true); - expect( - areCentersTooClose({ cx: -2, cy: -2 }, { cx: 0, cy: 0 }, 2.5, 2.5) - ).toBe(true); - }); -}); - -describe("Get move direction", () => { - it("1 rectangle", () => { - expect( - getMoveDirection([{ x: 4, y: 6, width: 4, height: 4 }], { - x: 3, - y: 4, - width: 3, - height: 3, - }) - ).toEqual({ x: 1, y: 2 }); - }); - - it("2 rectangles", () => { - expect( - getMoveDirection( - [ - { x: 4, y: 6, width: 4, height: 4 }, - { x: 8, y: 5, width: 7, height: 7 }, - ], - { x: 3, y: 4, width: 3, height: 3 } - ) - ).toEqual({ - x: 6, - y: 3, - }); - }); - - it("Negative coordinate", () => { - expect( - getMoveDirection([{ x: -4, y: -6, width: 4, height: 4 }], { - x: 3, - y: -4, - width: 3, - height: 3, - }) - ).toEqual({ - x: -7, - y: -2, - }); - }); - - it("Same coordinate", () => { - expect( - getMoveDirection([{ x: 0, y: 0, width: 4, height: 4 }], { - x: 0, - y: 0, - width: 3, - height: 3, - }) - ).toEqual({ - x: 0, - y: 0, - }); - }); -}); - -describe("All collision", () => { - it("Collision with self", () => { - expect(allCollision(originRectangle, [originRectangle])).toBe(true); - }); - - it("No collision with 2 rectangles", () => { - expect( - allCollision(originRectangle, [{ x: 4, y: 4, width: 2, height: 2 }]) - ).toBe(false); - }); - - it("Collision with 2 rectangles", () => { - expect( - allCollision(originRectangle, [ - { x: 2, y: 2, width: 4, height: 4 }, - { x: 6, y: 6, width: 2, height: 2 }, - ]) - ).toBe(false); - expect( - allCollision({ x: 1.1, y: 1.1, width: 1, height: 1 }, [ - { x: 2, y: 2, width: 4, height: 4 }, - { x: 6, y: 6, width: 2, height: 2 }, - ]) - ).toBe(true); - expect( - allCollision({ x: 5.9, y: 5.9, width: 1, height: 1 }, [ - { x: 2, y: 2, width: 4, height: 4 }, - { x: 6, y: 6, width: 2, height: 2 }, - ]) - ).toBe(true); - expect( - allCollision({ x: 5.9, y: 4, width: 1, height: 1 }, [ - { x: 2, y: 2, width: 4, height: 4 }, - { x: 6, y: 6, width: 2, height: 2 }, - ]) - ).toBe(true); - expect( - allCollision({ x: 4, y: 5.9, width: 1, height: 1 }, [ - { x: 2, y: 2, width: 4, height: 4 }, - { x: 6, y: 6, width: 2, height: 2 }, - ]) - ).toBe(true); - }); -}); - -describe("Get distance", () => { - it("Naive test", () => { - expect( - getDistance({ x: 1, y: 1 }, { x: 1, y: 1, width: 4, height: 4 }) - ).toEqual(0); - }); - - it("Naive test", () => { - expect( - getDistance({ x: 10, y: 5 }, { x: 1, y: 5, width: 4, height: 4 }) - ).toEqual(9); - }); -}); - -describe("Get the circle", () => { - it("Center of mass one word", () => { - expect(getTheCircle([{ x: 1, y: 1, width: 4, height: 4 }])).toEqual({ - x: 1, - y: 1, - radius: CONTAINER_WIDTH / 2, - }); - }); - - it("Center of mass multiple words", () => { - expect( - getTheCircle([ - { x: 1, y: 1, width: 4, height: 4 }, - { x: 3, y: 4, width: 4, height: 4 }, - { x: 2, y: 2, width: 4, height: 4 }, - { x: -2, y: 1, width: 4, height: 4 }, - ]) - ).toEqual({ - x: 1, - y: 2, - radius: CONTAINER_WIDTH / 2, - }); - }); - - it("Center of mass update radius", () => { - expect( - getTheCircle([ - { x: 0, y: 0, width: 4, height: 4 }, - { x: 800, y: 0, width: 4, height: 4 }, - ]) - ).toEqual({ - x: 400, - y: 0, - radius: 400, - }); - }); -}); - -describe("Random interval", () => { - it("Less than or equal of max of interval", () => { - expect(randomInterval(1, 4)).toBeLessThanOrEqual(4); - }); - it("Greater than or equal of min of interval", () => { - expect(randomInterval(1, 4)).toBeGreaterThanOrEqual(1); - }); -}); - -describe("CumulativeBins", () => { - it("With same value", () => { - expect(cumulativeBins([1, 1, 1, 1])).toEqual([1, 2, 3, 4]); - }); - - it("With different value", () => { - expect(cumulativeBins([4, 2, 1, 3])).toEqual([4, 6, 7, 10]); - }); - - it("With negative value", () => { - expect(cumulativeBins([4, -2, 1, 3])).toEqual([4, 2, 3, 6]); - }); -}); - -describe("slideWords", () => { - describe("Slide one word", () => { - it("No move", () => { - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: originRectangle, - }, - ], - { x: 0, y: 0 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: originRectangle, - }, - ]); - }); - it("On one direction", () => { - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 0, y: 0, width: 1, height: 1 }, - }, - ], - { x: 0, y: 2 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 0, y: 2, width: 1, height: 1 }, - }, - ]); - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 1, y: 4, width: 4, height: 4 }, - }, - ], - { x: 0, y: -2 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 1, y: 2, width: 4, height: 4 }, - }, - ]); - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 2, y: 0, width: 1, height: 1 }, - }, - ], - { x: 2, y: 0 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 4, y: 0, width: 1, height: 1 }, - }, - ]); - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 2, y: 0, width: 1, height: 1 }, - }, - ], - { x: -2, y: 0 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 0, y: 0, width: 1, height: 1 }, - }, - ]); - }); - - it("On multiple direction", () => { - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 2, y: 0, width: 1, height: 1 }, - }, - ], - { x: 2, y: 4 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 4, y: 4, width: 1, height: 1 }, - }, - ]); - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 2, y: 4, width: 1, height: 1 }, - }, - ], - { x: -2, y: -2 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 0, y: 2, width: 1, height: 1 }, - }, - ]); - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 2, y: 0, width: 1, height: 1 }, - }, - ], - { x: -1, y: 4 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 1, y: 4, width: 1, height: 1 }, - }, - ]); - }); - }); - - describe("Slide multiple word", () => { - it("On the bottom and right", () => { - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 1, y: 4, width: 4, height: 4 }, - }, - { - id: "2", - text: "2", - coef: 0.5, - rect: { x: 6, y: 9, width: 4, height: 4 }, - }, - ], - { x: 1, y: 2 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 2, y: 6, width: 4, height: 4 }, - }, - { - id: "2", - text: "2", - coef: 0.5, - rect: { x: 7, y: 11, width: 4, height: 4 }, - }, - ]); - expect( - slideWords( - [ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 1, y: 4, width: 4, height: 4 }, - }, - { - id: "2", - text: "2", - coef: 0.5, - rect: { x: 6, y: 9, width: 4, height: 4 }, - }, - ], - { x: -1, y: -2 } - ) - ).toEqual([ - { - id: "1", - text: "1", - coef: 0.5, - rect: { x: 0, y: 2, width: 4, height: 4 }, - }, - { - id: "2", - text: "2", - coef: 0.5, - rect: { x: 5, y: 7, width: 4, height: 4 }, - }, - ]); - }); - }); -}); - -describe("BoundParent", () => { - it("With one rectangle", () => { - expect(boundParent([{ x: 2, y: 2, width: 2, height: 2 }])).toEqual({ - x: 2, - y: 2, - width: 2, - height: 2, - }); - }); - it("With two rectangles", () => { - expect( - boundParent([ - { x: 7, y: 8, width: 4, height: 4 }, - { x: 3, y: 5, width: 4, height: 4 }, - ]) - ).toEqual({ x: 3, y: 5, width: 8, height: 7 }); - }); - it("no negative widths", () => { - expect( - boundParent([ - { x: -4, y: -10, width: 4, height: 4 }, - { x: -2, y: -12, width: 1, height: 4 }, - ]) - ).toEqual({ x: -4, y: -12, width: 4, height: 6 }); - }); -}); - -describe("Range with step", () => { - it("Start equal to end", () => { - expect(rangeWithStep(1, 1, 2)).toEqual([1]); - }); - it("End bigger than start", () => { - expect(rangeWithStep(4, 1, 2)).toEqual([]); - }); - it("No step", () => { - expect(rangeWithStep(0, 10, 1)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - }); - it("Step of 2", () => { - expect(rangeWithStep(0, 9, 2)).toEqual([0, 2, 4, 6, 8]); - }); -}); - -describe("Get area rectangle", () => { - it("Get area", () => { - expect(getAreaRectangle(rectangle)).toEqual(50); - }); -}); - -describe("Place first item", () => { - it("Put in centre", () => { - expect(placeFirstWord(rectangle, 0, 0)).toEqual({ - x: 0, - y: 0, - width: 10, - height: 5, - }); - }); -}); diff --git a/src/WordCloud/components/utils.ts b/src/WordCloud/components/utils.ts deleted file mode 100644 index aab483b..0000000 --- a/src/WordCloud/components/utils.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { - CONTAINER_HEIGHT, - CONTAINER_WIDTH, - CUT_OFF, - DEFAULT_RECT, - MARGIN_HEIGHT, - MARGIN_WIDTH, - MAX_FONT_SIZE, - MIN_FONT_SIZE, - NUMBER_OF_INTERVALS, - WORD_CLOUD_MARGIN_HEIGHT, - WORD_CLOUD_MARGIN_WIDTH, -} from "./constants"; -import { CenterCoordinate, Circle, Coordinate, Rectangle, Word } from "./types"; - -export const computeFontSize = (coef: number): number => { - return ( - (coef - CUT_OFF) * - (1 / (1 - CUT_OFF)) ** 2 * - (MAX_FONT_SIZE - MIN_FONT_SIZE) + - MIN_FONT_SIZE - ); -}; - -// This function returns the bound of the word cloud -export const boundParent = (rects: Rectangle[]): Rectangle => { - const topLeftPoints: Coordinate[] = rects.map((r) => ({ - x: r.x, - y: r.y, - })); - const bottomRightPoints: Coordinate[] = rects.map((r) => ({ - x: r.x + r.width, - y: r.y + r.height, - })); - - const xMin = Math.min(...topLeftPoints.map((r) => r.x)); - const xMax = Math.max(...bottomRightPoints.map((r) => r.x)); - const yMin = Math.min(...topLeftPoints.map((r) => r.y)); - const yMax = Math.max(...bottomRightPoints.map((r) => r.y)); - - return { - x: xMin, - y: yMin, - width: Math.abs(xMax - xMin), - height: Math.abs(yMax - yMin), - }; -}; - -export const getBoundingWordCloud = (word: Word[]): Rectangle => { - const rect = word.map((w) => w.rect); - - const tightBound = boundParent(rect); - return { - x: tightBound.x - WORD_CLOUD_MARGIN_WIDTH / 2, - y: tightBound.y - WORD_CLOUD_MARGIN_HEIGHT / 2, - width: tightBound.width + WORD_CLOUD_MARGIN_WIDTH, - height: tightBound.height + WORD_CLOUD_MARGIN_HEIGHT, - }; -}; - -// This function indicates whether rectangles are in a collision -export const areCentersTooClose = ( - centerA: CenterCoordinate, - centerB: CenterCoordinate, - minX: number, - minY: number -): boolean => - Math.abs(centerA.cx - centerB.cx) <= minX && - Math.abs(centerA.cy - centerB.cy) <= minY; - -// This function computes the collisions -export const allCollision = ( - word: Rectangle, - passRect: Rectangle[] -): boolean => { - return passRect - .map((rect) => - areCentersTooClose( - { cx: rect.x + rect.width / 2, cy: rect.y + rect.height / 2 }, - { cx: word.x + word.width / 2, cy: word.y + word.height / 2 }, - (rect.width + word.width) / 2, - (rect.height + word.height) / 2 - ) - ) - .some((t) => t === true); -}; - -// This function slides an array of words -export const slideWords = (words: Word[], sliding: Coordinate): Word[] => { - return words.map((w) => ({ - ...w, - rect: { - ...w.rect, - x: w.rect.x + sliding.x, - y: w.rect.y + sliding.y, - }, - })); -}; - -export function archimedeanSpiral(size: [number, number]) { - // change the aspect of the spiral based on the ratio of width and height - var e = size[0] / size[1]; - return function (t: number) { - return [e * (t *= 0.5) * Math.cos(t), t * Math.sin(t)]; - }; -} - -export const futureSpiralPosition = ( - rectangle: Rectangle, - placedRects: Rectangle[] -) => { - let position = { - ...rectangle, - x: -rectangle.width / 2, - y: -rectangle.height / 2, - }; - let t = 0; - const dt = Math.random() < 0.5 ? 1 : -1; - while (allCollision(position, placedRects) && t < 3000) { - // compute point on spiral - const spiral = archimedeanSpiral([1, 1]); - const [dx, dy] = spiral(t); - position = { - ...rectangle, - x: rectangle.x + dx, - y: rectangle.y + dy, - }; - t += dt; - } - return position; -}; - -// ************************************** -// not used anymore - -// This function returns the futur position of a rectangle, without collision, in direction of already placed rectangles -export const futurPosition = ( - word: Rectangle, - placedRects: Rectangle[], - step: number, - weight: number[] -): Rectangle => { - let isDone = false; - - // Put the word in random place around the parent - let movedRect = placeWordOnOuterCircle(word, placedRects, weight); - let iter = 0; - let displacement = 0; - do { - const moveDirection = getMoveDirection(placedRects, movedRect); - const hypothenus = Math.sqrt(moveDirection.x ** 2 + moveDirection.y ** 2); - const stepX = (step / hypothenus) * moveDirection.x; - const stepY = (step / hypothenus) * moveDirection.y; - const futurRectPosition: Rectangle = { - ...movedRect, - x: movedRect.x + stepX, - y: movedRect.y + stepY, - }; - // Test if the word can be move over the hypotenuse - if (allCollision(futurRectPosition, placedRects)) { - const onlyMoveOverX = { ...futurRectPosition, y: movedRect.y }; - const onlyMoveOverY = { ...futurRectPosition, x: movedRect.x }; - const xColl = allCollision(onlyMoveOverX, placedRects); - const yColl = allCollision(onlyMoveOverY, placedRects); - if (xColl) { - if (yColl) { - // Do not move anymore - isDone = true; - } else { - movedRect = { ...onlyMoveOverY }; - } - } else { - movedRect = { ...onlyMoveOverX }; - } - } else { - movedRect = { ...futurRectPosition }; - } - displacement = Math.abs(stepX) + Math.abs(stepY); - iter++; - } while (!isDone && displacement > 2 && iter < 300); - return movedRect; -}; - -// This function return the cumulative weight of an array : for example [1, 2, 3, 4] become [1, 3, 6, 10] -// source: https://quickref.me/create-an-array-of-cumulative-sum.html -export const cumulativeBins = (bin: number[]): number[] => { - return bin.map( - ( - (sum) => (value) => - (sum += value) - )(0) - ); -}; - -// https://stackoverflow.com/questions/36947847/how-to-generate-range-of-numbers-from-0-to-n-in-es2015-only -// range(0, 9, 2) => [0, 2, 4, 6, 8] -// No negative step -export const rangeWithStep = ( - from: number, - to: number, - step: number -): number[] => { - if (to < from) { - return []; - } - return [...Array(Math.floor((to - from) / step) + 1)].map( - (_, i) => from + i * step - ); -}; - -// This function put the first word in the center of the parent rectangle -export const setFirstWordInCenterOfParent = (w: Word, p: string): Rectangle => { - const parentElement = document.getElementsByTagName("svg").namedItem(p); - const word = getBoundingRect(w.id); - if (parentElement && word) { - const parent = parentElement.getBBox(); - - const newX = (parent.width - word.width) / 2; - const newY = (parent.height - word.height) / 2; - return { x: newX, y: newY, width: word.width, height: word.height }; - } - return { x: 0, y: 0, width: 50, height: 20 }; -}; - -// This function return the distance between a rectangle and a cartesian coordinate -export const getDistance = (point: Coordinate, rect: Rectangle): number => { - return Math.sqrt((point.x - rect.x) ** 2 + (point.y - rect.y) ** 2); -}; - -// This function return the center of mass of multiple rectangle -export const centerOfMass = (passRect: Rectangle[]): Coordinate => { - const centerMass: Coordinate = passRect.reduce( - (acc, word) => { - const sum = { - x: acc.x + word.x, - y: acc.y + word.y, - }; - return sum; - }, - { x: 0, y: 0 } - ); - centerMass.x /= passRect.length; - centerMass.y /= passRect.length; - - return centerMass; -}; - -// This function computes the circle which centre is the center of mass of the rectangle list passed in argument and which radius is equal to the farthest point from the centre -export const getTheCircle = (passRect: Rectangle[]): Circle => { - const centerMass = centerOfMass(passRect); - - const distance: number[] = passRect.map((rect) => - getDistance(centerMass, rect) - ); - - const radius = Math.max( - ...distance, - CONTAINER_HEIGHT / 2, - CONTAINER_WIDTH / 2 - ); - - return { x: centerMass.x, y: centerMass.y, radius }; -}; - -// This function return a random float between min and max -export const randomInterval = (min: number, max: number): number => { - return Math.random() * (max - min) + min; -}; - -// This function puts the word in a random place on a circle -export const placeWordOnOuterCircle = ( - w: Rectangle, - passRect: Rectangle[], - weight: number[] -): Rectangle => { - const maxWeight = Math.max(...weight); - - // Subtract the max to each element to promote other interval - const invertedWeight = weight.map((a) => maxWeight - a); - - // The cumulative weight allows to define intervals to select a random portion of circle to put our current rectangle, with - // different probabilities (here we want to favour portions of the circle with less rectangles already put) - const cumulativeWeight = cumulativeBins(invertedWeight); - - const randomInter = randomInterval( - 0, - cumulativeWeight[cumulativeWeight.length - 1] - ); - - // Calculate the size of each intervals - const ratio = 360 / NUMBER_OF_INTERVALS; - - // create the intervals - const rangeInterval = rangeWithStep(0, 360, ratio); - - const inter = cumulativeWeight.findIndex((el) => el >= randomInter); - - let angleInter; - - if (Number.isInteger(inter)) { - // Add to weights the position that has just been drawn - weight[inter] += 1; - angleInter = { - x: rangeInterval[inter], - y: rangeInterval[inter + 1] - 1, - }; - } else { - angleInter = { x: 0, y: 360 }; - } - - const angle = randomInterval(angleInter.x, angleInter.y); - const circle = getTheCircle(passRect); - const newPosition = { - ...w, - x: circle.radius * Math.cos(angle) + circle.x, - y: circle.radius * Math.sin(angle) + circle.y, - }; - return newPosition; -}; - -// This function allows to obtain the direction of movement of a rectangle in the direction of the rectangles already placed. -export const getMoveDirection = ( - pastWords: Rectangle[], - currentRect: Rectangle -): Coordinate => { - return pastWords.reduce( - (acc, word) => { - const differences = { - x: word.x - currentRect.x, - y: word.y - currentRect.y, - }; - return { x: acc.x + differences.x, y: acc.y + differences.y }; - }, - { x: 0, y: 0 } - ); -}; - -// This function returns the aera of a rectangle -export const getAreaRectangle = (rect: Rectangle): number => { - return rect.height * rect.width; -}; - -// This function places the first word in the centre of the rectangle -export const placeFirstWord = ( - rectToPlace: Rectangle, - centerX: number, - centerY: number -): Rectangle => { - const centeredRect = { - width: rectToPlace.width, - height: rectToPlace.height, - x: centerX, - y: centerY, - }; - - return centeredRect; -}; - -export const getBoundingRect = ( - id: string, - tagName: "svg" | "text" = "text" -): Rectangle => { - const bbox = - ( - document.getElementsByTagName(tagName).namedItem(id) || undefined - )?.getBoundingClientRect() || DEFAULT_RECT; - return { - x: bbox.x, - y: bbox.y, - width: bbox.width + MARGIN_WIDTH, - height: bbox.height + MARGIN_HEIGHT, - }; -}; - -// This function returns the new position of a list of items -// export const getNewPositions = ( -// itemsToPlace: Word[], -// centeredRect: Rectangle, -// step: number -// ): Rectangle[] => { -// // Initialize the weights with the value 1, of the size of the number of intervals -// const weight = new Array(NUMBER_OF_INTERVALS).fill(1); - -// const newPositions = itemsToPlace.slice(1).reduce( -// (placedItems, rect) => { -// const futureItem = futurPosition(rect, placedItems, step, weight); -// return [...placedItems, futureItem]; -// }, -// [centeredRect] -// ); - -// return newPositions; -// }; diff --git a/src/WordCloud/index.ts b/src/WordCloud/index.ts deleted file mode 100644 index 53cad14..0000000 --- a/src/WordCloud/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as default } from "./WordCloud"; -export type { ExplanationData } from "./types"; diff --git a/src/WordCloud/types.ts b/src/WordCloud/types.ts deleted file mode 100644 index 8a0e274..0000000 --- a/src/WordCloud/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CategoryCloud } from "./components/types"; - -export type InputNode = { - id: string; - text: string; - coef: number; -}; - -export type ExplanationData = CategoryCloud[]; diff --git a/src/components/CheckBoxSetting.tsx b/src/components/CheckBoxSetting.tsx deleted file mode 100644 index 08746e0..0000000 --- a/src/components/CheckBoxSetting.tsx +++ /dev/null @@ -1,23 +0,0 @@ -type Props = { - id: string; - value: boolean; - label: string; - onChange: () => void; -}; -const CheckBoxSetting = ({ onChange, id, label, value }: Props) => { - return ( -
- - -
- ); -}; -export default CheckBoxSetting; diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/SettingsWrapper.tsx b/src/components/SettingsWrapper.tsx deleted file mode 100644 index da33c32..0000000 --- a/src/components/SettingsWrapper.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const SettingsWrapper = ({ - children, - title, -}: { - children: JSX.Element | (null | JSX.Element)[]; - title: string; -}) => { - return ( -
- {title} -
{children}
-
- ); -}; -export default SettingsWrapper; diff --git a/src/components/UseCase.tsx b/src/components/UseCase.tsx deleted file mode 100644 index 1c2ace8..0000000 --- a/src/components/UseCase.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useState } from "react"; -import { ExplanationData } from "../WordCloud"; -import { transformGPipelineData } from "./utils/transformations"; - -type Props = { - label: string; - description: string; - nodesFile: string; - categoriesFile: string; - onSubmit: (data: ExplanationData) => void; -}; - -const UseCase = ({ - label, - description, - nodesFile, - categoriesFile, - onSubmit, -}: Props) => { - const [loadingData, setLoadingData] = useState(false); - const onClick = async () => { - setLoadingData(true); - // load the data from the files - const nodesFileData = await fetch(nodesFile); - const nodes = await nodesFileData.json(); - const categoriesFileData = await fetch(categoriesFile); - const categories = await categoriesFileData.json(); - const xData = transformGPipelineData(nodes, categories); - onSubmit(xData); - setLoadingData(false); - }; - - return ( -
- -
{description}
-
- ); -}; -export default UseCase; diff --git a/src/components/types.ts b/src/components/types.ts deleted file mode 100644 index 5c80f1e..0000000 --- a/src/components/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type ExplainabilityNode = { - id: string; - name: string; - score: number; -}; - -export type Category = { - elements: string[]; -} & ExplainabilityNode; diff --git a/src/components/utils/transformations.ts b/src/components/utils/transformations.ts deleted file mode 100644 index fe4fc7a..0000000 --- a/src/components/utils/transformations.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { InputNode } from "../../WordCloud/types"; -import { Category, ExplainabilityNode } from "../types"; - -export const transformGPipelineData = ( - nodes: ExplainabilityNode[], - categories: Category[] -) => - categories?.map((c) => ({ - category: c.name, - words: c.elements.reduce((words, e) => { - const el = nodes.find((n) => n.id === e); - if (el) { - words.push({ id: el.id, text: el.name, coef: el.score }); - } - return words; - }, []), - }));