From 8633fca0d1a8cae87362d8247ab8b3a29fa0a190 Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Wed, 15 Mar 2023 12:06:19 +0100 Subject: [PATCH 01/16] feat: random position on circle --- src/App.tsx | 12 +++++++++- src/utils.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7c372e9..2f01499 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,10 +35,20 @@ const Wordcloud = () => { x: CENTER_X, y: CENTER_Y, }; + + // let cumulWeight = [0, 0, 0, 0]; + let weight = [0, 0, 0, 0]; const newPositions = rectsToPlace.slice(1).reduce( (placedElements, rect) => { // move the word - const futureWord = futurPosition(rect, placedElements, 3); + const futurePositionWord = futurPosition( + rect, + placedElements, + 3, + weight + ); + const futureWord = futurePositionWord.rect; + weight = futurePositionWord.weight; return [...placedElements, futureWord]; }, [centeredRect] diff --git a/src/utils.ts b/src/utils.ts index 1006e92..5d15481 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,6 +25,18 @@ export type Circle = { radius: number; }; +export type FuturPosition = { + rect: Rectangle; + weight: number[]; +}; + +export const interval = { + a: { x: 0, y: 89 }, + b: { x: 90, y: 179 }, + c: { x: 180, y: 269 }, + d: { x: 270, y: 360 }, +}; + export const getBoundingRect = ( id: string, tagName: "svg" | "text" = "text" @@ -88,20 +100,54 @@ export const getTheCircle = (passRect: Rectangle[]): Circle => { return { x: centerMass.x, y: centerMass.y, radius }; }; +export const randomInterval = (min: number, max: number): number => { + return Math.floor(Math.random() * (max - min + 1) + min); +}; + +export const cumulativeBins = (bin: number[]): number[] => { + return bin.map( + ( + (sum) => (value) => + (sum += value) + )(0) + ); +}; + // This function put the word in a random place export const placeWordOnOuterCircle = ( w: Rectangle, - passRect: Rectangle[] -): Rectangle => { + passRect: Rectangle[], + weight: number[] +): FuturPosition => { // Chose the parent face - const angle = Math.random() * 360; + const cumulativeWeight = cumulativeBins(weight); + const randomInter = randomInterval(0, Math.max(...cumulativeWeight)); + const inter = cumulativeWeight.findIndex((el) => el > randomInter); + + weight[inter] += 1; + + let angleInter = { x: 0, y: 360 }; + + if (inter === 0) { + angleInter = interval.a; + } else if (inter === 1) { + angleInter = interval.b; + } else if (inter === 2) { + angleInter = interval.c; + } else if (inter === 3) { + angleInter = interval.d; + } + + const angle = randomInterval(angleInter.x, angleInter.y); + + // const angle = Math.random() * 360; 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; + return { rect: newPosition, weight }; }; export const getMoveDirection = ( @@ -123,12 +169,14 @@ export const getMoveDirection = ( export const futurPosition = ( word: Rectangle, passRect: Rectangle[], - step: number -): Rectangle => { + step: number, + weight: number[] +): FuturPosition => { let isCollision = false; + console.log(weight); // put the word in random place around the parent - let movedWord = placeWordOnOuterCircle(word, passRect); + let movedWord = placeWordOnOuterCircle(word, passRect, weight).rect; let iter = 0; let displacement = 0; do { @@ -164,7 +212,7 @@ export const futurPosition = ( displacement = Math.abs(stepX) + Math.abs(stepY); iter++; } while (!isCollision && displacement > 2 && iter < 300); - return movedWord; + return { rect: movedWord, weight }; }; export const areCentersTooClose = ( From 0c7ba1a481f476cdf313b74dbaf62157a1612e3e Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Wed, 15 Mar 2023 12:53:08 +0100 Subject: [PATCH 02/16] fix: update weight in futurePosistion --- src/App.tsx | 1 + src/utils.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2f01499..45fb0f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,7 @@ const Wordcloud = () => { let weight = [0, 0, 0, 0]; const newPositions = rectsToPlace.slice(1).reduce( (placedElements, rect) => { + console.log(weight); // move the word const futurePositionWord = futurPosition( rect, diff --git a/src/utils.ts b/src/utils.ts index 5d15481..ef301e1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -124,7 +124,8 @@ export const placeWordOnOuterCircle = ( const randomInter = randomInterval(0, Math.max(...cumulativeWeight)); const inter = cumulativeWeight.findIndex((el) => el > randomInter); - weight[inter] += 1; + weight[inter] = weight[inter] + 1; + console.log(weight); let angleInter = { x: 0, y: 360 }; @@ -173,10 +174,12 @@ export const futurPosition = ( weight: number[] ): FuturPosition => { let isCollision = false; - console.log(weight); + // console.log(weight); // put the word in random place around the parent - let movedWord = placeWordOnOuterCircle(word, passRect, weight).rect; + const move = placeWordOnOuterCircle(word, passRect, weight); + let movedWord = move.rect; + weight = move.weight; let iter = 0; let displacement = 0; do { From 871502044b1a6e074b6e8731bf6a6f67c2359e3e Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Wed, 15 Mar 2023 14:41:50 +0100 Subject: [PATCH 03/16] fix: weight global reference --- src/App.tsx | 15 +++------------ src/utils.ts | 34 ++++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 45fb0f3..fb131a6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,20 +36,11 @@ const Wordcloud = () => { y: CENTER_Y, }; - // let cumulWeight = [0, 0, 0, 0]; - let weight = [0, 0, 0, 0]; + const weight = [1, 1, 1, 1]; const newPositions = rectsToPlace.slice(1).reduce( (placedElements, rect) => { - console.log(weight); - // move the word - const futurePositionWord = futurPosition( - rect, - placedElements, - 3, - weight - ); - const futureWord = futurePositionWord.rect; - weight = futurePositionWord.weight; + const futureWord = futurPosition(rect, placedElements, 3, weight); + // console.log("weight reduce", weight); return [...placedElements, futureWord]; }, [centeredRect] diff --git a/src/utils.ts b/src/utils.ts index ef301e1..0159b68 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -101,7 +101,7 @@ export const getTheCircle = (passRect: Rectangle[]): Circle => { }; export const randomInterval = (min: number, max: number): number => { - return Math.floor(Math.random() * (max - min + 1) + min); + return Math.floor(Math.random() * (max - min) + min); }; export const cumulativeBins = (bin: number[]): number[] => { @@ -113,19 +113,28 @@ export const cumulativeBins = (bin: number[]): number[] => { ); }; +// export const getWeight = (passRect: Rectangle[]) => { +// const weight = +// }; + // This function put the word in a random place export const placeWordOnOuterCircle = ( w: Rectangle, passRect: Rectangle[], weight: number[] -): FuturPosition => { +): Rectangle => { // Chose the parent face + console.log("weight place", weight); const cumulativeWeight = cumulativeBins(weight); - const randomInter = randomInterval(0, Math.max(...cumulativeWeight)); + console.log("cumul place", cumulativeWeight); + + const randomInter = randomInterval(0, cumulativeWeight.slice(-1)[0]); + console.log("random", randomInter); const inter = cumulativeWeight.findIndex((el) => el > randomInter); - weight[inter] = weight[inter] + 1; - console.log(weight); + console.log("inter", inter); + + weight[inter] += 1; let angleInter = { x: 0, y: 360 }; @@ -139,16 +148,19 @@ export const placeWordOnOuterCircle = ( angleInter = interval.d; } + console.log("angle interval", angleInter); + const angle = randomInterval(angleInter.x, angleInter.y); - // const angle = Math.random() * 360; + console.log("angle", angle); + const circle = getTheCircle(passRect); const newPosition = { ...w, x: circle.radius * Math.cos(angle) + circle.x, y: circle.radius * Math.sin(angle) + circle.y, }; - return { rect: newPosition, weight }; + return newPosition; }; export const getMoveDirection = ( @@ -172,14 +184,12 @@ export const futurPosition = ( passRect: Rectangle[], step: number, weight: number[] -): FuturPosition => { +): Rectangle => { let isCollision = false; // console.log(weight); // put the word in random place around the parent - const move = placeWordOnOuterCircle(word, passRect, weight); - let movedWord = move.rect; - weight = move.weight; + let movedWord = placeWordOnOuterCircle(word, passRect, weight); let iter = 0; let displacement = 0; do { @@ -215,7 +225,7 @@ export const futurPosition = ( displacement = Math.abs(stepX) + Math.abs(stepY); iter++; } while (!isCollision && displacement > 2 && iter < 300); - return { rect: movedWord, weight }; + return movedWord; }; export const areCentersTooClose = ( From 82ce9e7dc6e806b2a62fc6391a3885763a242241 Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Wed, 15 Mar 2023 15:22:46 +0100 Subject: [PATCH 04/16] fix: weight substract max --- src/App.tsx | 1 - src/utils.ts | 23 +++++------------------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index fb131a6..4079e5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,7 +40,6 @@ const Wordcloud = () => { const newPositions = rectsToPlace.slice(1).reduce( (placedElements, rect) => { const futureWord = futurPosition(rect, placedElements, 3, weight); - // console.log("weight reduce", weight); return [...placedElements, futureWord]; }, [centeredRect] diff --git a/src/utils.ts b/src/utils.ts index 0159b68..cbacd80 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -101,9 +101,8 @@ export const getTheCircle = (passRect: Rectangle[]): Circle => { }; export const randomInterval = (min: number, max: number): number => { - return Math.floor(Math.random() * (max - min) + min); + return Math.random() * (max - min) + min; }; - export const cumulativeBins = (bin: number[]): number[] => { return bin.map( ( @@ -113,10 +112,6 @@ export const cumulativeBins = (bin: number[]): number[] => { ); }; -// export const getWeight = (passRect: Rectangle[]) => { -// const weight = -// }; - // This function put the word in a random place export const placeWordOnOuterCircle = ( w: Rectangle, @@ -124,18 +119,16 @@ export const placeWordOnOuterCircle = ( weight: number[] ): Rectangle => { // Chose the parent face - console.log("weight place", weight); const cumulativeWeight = cumulativeBins(weight); - console.log("cumul place", cumulativeWeight); const randomInter = randomInterval(0, cumulativeWeight.slice(-1)[0]); - console.log("random", randomInter); - const inter = cumulativeWeight.findIndex((el) => el > randomInter); - - console.log("inter", inter); + const inter = cumulativeWeight.findIndex((el) => el >= randomInter); weight[inter] += 1; + // substract the max to each element to promote other interval + weight = weight.map((a) => Math.max(...weight) - a); + let angleInter = { x: 0, y: 360 }; if (inter === 0) { @@ -148,12 +141,7 @@ export const placeWordOnOuterCircle = ( angleInter = interval.d; } - console.log("angle interval", angleInter); - const angle = randomInterval(angleInter.x, angleInter.y); - - console.log("angle", angle); - const circle = getTheCircle(passRect); const newPosition = { ...w, @@ -186,7 +174,6 @@ export const futurPosition = ( weight: number[] ): Rectangle => { let isCollision = false; - // console.log(weight); // put the word in random place around the parent let movedWord = placeWordOnOuterCircle(word, passRect, weight); From 468cdd6e94efea77cb5c17fdeb63cdb34d60ba2e Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Wed, 15 Mar 2023 16:04:05 +0100 Subject: [PATCH 05/16] feat: unit test --- src/App.tsx | 6 ++++-- src/constants.ts | 7 +++++++ src/utils.test.ts | 25 +++++++++++++++++++++++++ src/utils.ts | 29 ++++++++++++++--------------- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4079e5f..097d158 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,7 @@ import { CONTAINER_WIDTH, DEFAULT_RECT, } from "./constants"; -import { defaultWords1 } from "./data"; +import { defaultWords1, defaultWords2 } from "./data"; const CUT_OFF = 0.5; @@ -20,7 +20,7 @@ export const MAX_FONT_SIZE = 20; export const MIN_FONT_SIZE = 6; const Wordcloud = () => { - const [words, setWords] = React.useState(defaultWords1); + const [words, setWords] = React.useState(defaultWords2); const updateWords = () => { setWords((prevWords) => { @@ -45,6 +45,8 @@ const Wordcloud = () => { [centeredRect] ); + console.log(weight); + return wordsToPlace.map((word, idx) => ({ ...word, rect: newPositions[idx], diff --git a/src/constants.ts b/src/constants.ts index 0e23997..5711a6a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,3 +4,10 @@ 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 INTERVAL = { + a: { x: 0, y: 89 }, + b: { x: 90, y: 179 }, + c: { x: 180, y: 269 }, + d: { x: 270, y: 360 }, +}; diff --git a/src/utils.test.ts b/src/utils.test.ts index 61cdadc..11beac9 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -7,6 +7,8 @@ import { getMoveDirection, getTheCircle, getDistance, + randomInterval, + cumulativeBins, } from "./utils"; const origin: Coordinate = { @@ -172,3 +174,26 @@ describe("Get the circle", () => { }); }); }); + +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]); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index cbacd80..1a74386 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,9 @@ -import { CONTAINER_HEIGHT, CONTAINER_WIDTH, DEFAULT_RECT } from "./constants"; +import { + CONTAINER_HEIGHT, + CONTAINER_WIDTH, + DEFAULT_RECT, + INTERVAL, +} from "./constants"; export type Word = { id: string; @@ -30,13 +35,6 @@ export type FuturPosition = { weight: number[]; }; -export const interval = { - a: { x: 0, y: 89 }, - b: { x: 90, y: 179 }, - c: { x: 180, y: 269 }, - d: { x: 270, y: 360 }, -}; - export const getBoundingRect = ( id: string, tagName: "svg" | "text" = "text" @@ -53,8 +51,6 @@ export const getBoundingRect = ( }; }; -export const getArea = (rect: Rectangle): number => rect.width * rect.height; - // 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); @@ -121,7 +117,10 @@ export const placeWordOnOuterCircle = ( // Chose the parent face const cumulativeWeight = cumulativeBins(weight); - const randomInter = randomInterval(0, cumulativeWeight.slice(-1)[0]); + const randomInter = randomInterval( + 0, + cumulativeWeight[cumulativeWeight.length - 1] + ); const inter = cumulativeWeight.findIndex((el) => el >= randomInter); weight[inter] += 1; @@ -132,13 +131,13 @@ export const placeWordOnOuterCircle = ( let angleInter = { x: 0, y: 360 }; if (inter === 0) { - angleInter = interval.a; + angleInter = INTERVAL.a; } else if (inter === 1) { - angleInter = interval.b; + angleInter = INTERVAL.b; } else if (inter === 2) { - angleInter = interval.c; + angleInter = INTERVAL.c; } else if (inter === 3) { - angleInter = interval.d; + angleInter = INTERVAL.d; } const angle = randomInterval(angleInter.x, angleInter.y); From 57f6a0a9f56466ca39159a77b5c9cd799fd4339f Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Wed, 15 Mar 2023 17:05:14 +0100 Subject: [PATCH 06/16] feat: move parent right and bottom --- src/App.tsx | 39 ++++++++++++++++++++------------------- src/constants.ts | 19 +++++++++++++------ src/utils.ts | 40 +++++++++++++++++++++++++++++----------- 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 097d158..835208e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,16 +3,12 @@ import { futurPosition, getBoundingRect, placeWordOnOuterCircle, + updateParent, } from "./utils"; import * as React from "react"; -import { - CENTER_X, - CENTER_Y, - CONTAINER_HEIGHT, - CONTAINER_WIDTH, - DEFAULT_RECT, -} from "./constants"; + import { defaultWords1, defaultWords2 } from "./data"; +import { DEFAULT_RECT, PARENT } from "./constants"; const CUT_OFF = 0.5; @@ -32,14 +28,19 @@ const Wordcloud = () => { const centeredRect = { width: firstRect.width, height: firstRect.height, - x: CENTER_X, - y: CENTER_Y, + x: PARENT.centerX, + y: PARENT.centerY, }; + console.log(PARENT.centerX); + console.log(PARENT.centerY); + const weight = [1, 1, 1, 1]; const newPositions = rectsToPlace.slice(1).reduce( (placedElements, rect) => { const futureWord = futurPosition(rect, placedElements, 3, weight); + updateParent(placedElements); + return [...placedElements, futureWord]; }, [centeredRect] @@ -62,8 +63,8 @@ const Wordcloud = () => { {words.map((word) => { @@ -81,28 +82,28 @@ const Wordcloud = () => { fontSize={fontSize} style={{ outline: "1px solid rgba(255, 0, 0, 0.1)" }} id={word.id} - x={(word.rect?.x || CENTER_X).toString()} + x={(word.rect?.x || PARENT.centerX).toString()} // I don't know why I have to add the third of the fontSize to center te word vertically but it works - y={((word.rect?.y || CENTER_Y) + fontSize / 3).toString()} + y={((word.rect?.y || PARENT.centerY) + fontSize / 3).toString()} > {word.text} ); })} { getDistance(centerMass, word) ); - const radius = Math.max( - ...distance, - CONTAINER_HEIGHT / 2, - CONTAINER_WIDTH / 2 - ); + const radius = Math.max(...distance, PARENT.height / 2, PARENT.width / 2); return { x: centerMass.x, y: centerMass.y, radius }; }; @@ -234,3 +225,30 @@ export const allCollision = (word: Rectangle, passRect: Rectangle[]): boolean => ) ) .some((t) => t === true); + +export const updateParent = (passRect: Rectangle[]) => { + const maxXRight = passRect.map((word) => word.x); + const maxDistanceXRight = Math.max(...maxXRight); + + const maxYBottom = passRect.map((word) => word.y); + const maxDistanceYBottom = Math.max(...maxYBottom); + + if ( + maxDistanceYBottom > PARENT.height && + maxDistanceYBottom > maxDistanceXRight + ) { + const coef = maxDistanceYBottom / PARENT.height; + PARENT.height *= coef; + PARENT.width *= coef; + } + + if ( + maxDistanceXRight > PARENT.width && + maxDistanceXRight > maxDistanceYBottom + ) { + const coef = maxDistanceXRight / PARENT.width; + + PARENT.width *= coef; + PARENT.height *= coef; + } +}; From 4705ee481339003fd5db18a81cccb8a26a516e3a Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Thu, 16 Mar 2023 14:30:20 +0100 Subject: [PATCH 07/16] fix: change file name from App to Wordcloud --- src/{App.tsx => Wordcloud.tsx} | 66 +++++++++++-------- src/constants.ts | 17 +++-- src/main.tsx | 18 +++--- src/utils.ts | 113 +++++++++++++++++++++++---------- 4 files changed, 135 insertions(+), 79 deletions(-) rename src/{App.tsx => Wordcloud.tsx} (67%) diff --git a/src/App.tsx b/src/Wordcloud.tsx similarity index 67% rename from src/App.tsx rename to src/Wordcloud.tsx index 835208e..81937cb 100644 --- a/src/App.tsx +++ b/src/Wordcloud.tsx @@ -1,22 +1,33 @@ import "./App.css"; -import { - futurPosition, - getBoundingRect, - placeWordOnOuterCircle, - updateParent, -} from "./utils"; +import { futurPosition, getBoundingRect, Rectangle, Word } from "./utils"; import * as React from "react"; -import { defaultWords1, defaultWords2 } from "./data"; -import { DEFAULT_RECT, PARENT } from "./constants"; +import { CONTAINER_HEIGHT, CONTAINER_WIDTH, DEFAULT_RECT } from "./constants"; const CUT_OFF = 0.5; export const MAX_FONT_SIZE = 20; export const MIN_FONT_SIZE = 6; -const Wordcloud = () => { - const [words, setWords] = React.useState(defaultWords2); +type Props = { + data: Word[]; + width?: number; + height?: number; +}; + +const Wordcloud = ({ + data, + width = CONTAINER_WIDTH, + height = CONTAINER_HEIGHT, +}: Props) => { + const [words, setWords] = React.useState(data); + + const rectParent = { + width: width, + height: height, + centerY: height / 2, + centerX: width / 2, + }; const updateWords = () => { setWords((prevWords) => { @@ -28,26 +39,20 @@ const Wordcloud = () => { const centeredRect = { width: firstRect.width, height: firstRect.height, - x: PARENT.centerX, - y: PARENT.centerY, + x: width / 2, + y: height / 2, }; - console.log(PARENT.centerX); - console.log(PARENT.centerY); - const weight = [1, 1, 1, 1]; const newPositions = rectsToPlace.slice(1).reduce( (placedElements, rect) => { const futureWord = futurPosition(rect, placedElements, 3, weight); - updateParent(placedElements); return [...placedElements, futureWord]; }, [centeredRect] ); - console.log(weight); - return wordsToPlace.map((word, idx) => ({ ...word, rect: newPositions[idx], @@ -55,6 +60,10 @@ const Wordcloud = () => { }); }; + // const parent = updateParent( + // words.filter((w) => Boolean(w.rect)).map((w) => w.rect) as Rectangle[] + // ); + React.useEffect(() => { updateWords(); }, []); @@ -63,9 +72,10 @@ const Wordcloud = () => { {words.map((word) => { const fontSize = @@ -82,28 +92,28 @@ const Wordcloud = () => { fontSize={fontSize} style={{ outline: "1px solid rgba(255, 0, 0, 0.1)" }} id={word.id} - x={(word.rect?.x || PARENT.centerX).toString()} + x={(word.rect?.x || width / 2).toString()} // I don't know why I have to add the third of the fontSize to center te word vertically but it works - y={((word.rect?.y || PARENT.centerY) + fontSize / 3).toString()} + y={((word.rect?.y || height / 2) + fontSize / 3).toString()} > {word.text} ); })} - - , -) + + +); diff --git a/src/utils.ts b/src/utils.ts index 9e49f63..6273a17 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,9 @@ -import { DEFAULT_RECT, INTERVAL, PARENT } from "./constants"; +import { + CONTAINER_HEIGHT, + CONTAINER_WIDTH, + DEFAULT_RECT, + INTERVAL, +} from "./constants"; export type Word = { id: string; @@ -25,11 +30,6 @@ export type Circle = { radius: number; }; -export type FuturPosition = { - rect: Rectangle; - weight: number[]; -}; - export const getBoundingRect = ( id: string, tagName: "svg" | "text" = "text" @@ -64,7 +64,7 @@ export const getDistance = (point: Coordinate, word: Rectangle): number => { return Math.sqrt((point.x - word.x) ** 2 + (point.y - word.y) ** 2); }; -export const getTheCircle = (passRect: Rectangle[]): Circle => { +export const centerOfMass = (passRect: Rectangle[]): Coordinate => { const centerMass: Coordinate = passRect.reduce( (acc, word) => { const sum = { @@ -78,11 +78,21 @@ export const getTheCircle = (passRect: Rectangle[]): Circle => { centerMass.x /= passRect.length; centerMass.y /= passRect.length; + return centerMass; +}; + +export const getTheCircle = (passRect: Rectangle[]): Circle => { + const centerMass = centerOfMass(passRect); + const distance: number[] = passRect.map((word) => getDistance(centerMass, word) ); - const radius = Math.max(...distance, PARENT.height / 2, PARENT.width / 2); + const radius = Math.max( + ...distance, + CONTAINER_HEIGHT / 2, + CONTAINER_WIDTH / 2 + ); return { x: centerMass.x, y: centerMass.y, radius }; }; @@ -226,29 +236,64 @@ export const allCollision = (word: Rectangle, passRect: Rectangle[]): boolean => ) .some((t) => t === true); -export const updateParent = (passRect: Rectangle[]) => { - const maxXRight = passRect.map((word) => word.x); - const maxDistanceXRight = Math.max(...maxXRight); - - const maxYBottom = passRect.map((word) => word.y); - const maxDistanceYBottom = Math.max(...maxYBottom); - - if ( - maxDistanceYBottom > PARENT.height && - maxDistanceYBottom > maxDistanceXRight - ) { - const coef = maxDistanceYBottom / PARENT.height; - PARENT.height *= coef; - PARENT.width *= coef; - } - - if ( - maxDistanceXRight > PARENT.width && - maxDistanceXRight > maxDistanceYBottom - ) { - const coef = maxDistanceXRight / PARENT.width; - - PARENT.width *= coef; - PARENT.height *= coef; - } -}; +// export const updateParent = (passRect: Rectangle[]) => { +// const centerMass = centerOfMass(passRect); + +// rectParent.centerX = centerMass.x; +// rectParent.centerY = centerMass.y; + +// const rectX = passRect.map((word) => word.x); +// const maxX = Math.max(...rectX); +// const minX = Math.min(...rectX); + +// const distanceMaxX = maxX - rectParent.centerX; +// const distanceMinX = rectParent.centerX - minX; + +// if (distanceMaxX > distanceMinX && distanceMaxX > rectParent.width / 2) { +// rectParent.width = rectParent.centerX + (distanceMaxX + 10); +// } else if ( +// distanceMinX > distanceMaxX && +// distanceMinX > rectParent.width / 2 +// ) { +// rectParent.width = rectParent.centerX + (distanceMinX + 10); +// } + +// const rectY = passRect.map((word) => word.y); +// const maxY = Math.max(...rectY); +// const minY = Math.min(...rectY); + +// const distanceMaxY = maxY - rectParent.centerY; +// const distanceMinY = rectParent.centerY - minY; + +// if (distanceMaxY > distanceMinY && distanceMaxY > rectParent.height / 2) { +// rectParent.height = rectParent.centerY + (distanceMaxY + 10); +// } else if ( +// distanceMinY > distanceMaxY && +// distanceMinY > rectParent.height / 2 +// ) { +// rectParent.height = rectParent.centerY + (distanceMinY + 10); +// } + +// return rectParent; + +// rectParent.height = centerMass.y * coef; +// rectParent.width = centerMass.x * coef; + +// if ( +// maxDistanceYBottom > rectParent.height && +// maxDistanceYBottom > maxDistanceXRight +// ) { +// const coef = maxDistanceYBottom / rectParent.height; +// rectParent.height *= coef; +// rectParent.width *= coef; +// } + +// if ( +// maxDistanceXRight > rectParent.width && +// maxDistanceXRight > maxDistanceYBottom +// ) { +// const coef = maxDistanceXRight / rectParent.width; + +// rectParent.width *= coef; +// rectParent.height *= coef; +// } From cb405fb1ea086a54281de2d8304a410d473e79e1 Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Thu, 16 Mar 2023 16:19:03 +0100 Subject: [PATCH 08/16] feat: bound of word cloud --- src/Wordcloud.tsx | 26 +++++++--- src/utils.ts | 121 +++++++++++++++++++++++----------------------- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/Wordcloud.tsx b/src/Wordcloud.tsx index 81937cb..fd80c12 100644 --- a/src/Wordcloud.tsx +++ b/src/Wordcloud.tsx @@ -1,5 +1,11 @@ import "./App.css"; -import { futurPosition, getBoundingRect, Rectangle, Word } from "./utils"; +import { + boundParent, + futurPosition, + getBoundingRect, + Rectangle, + Word, +} from "./utils"; import * as React from "react"; import { CONTAINER_HEIGHT, CONTAINER_WIDTH, DEFAULT_RECT } from "./constants"; @@ -23,10 +29,10 @@ const Wordcloud = ({ const [words, setWords] = React.useState(data); const rectParent = { + x: width / 2, + y: height / 2, width: width, height: height, - centerY: height / 2, - centerX: width / 2, }; const updateWords = () => { @@ -47,7 +53,6 @@ const Wordcloud = ({ const newPositions = rectsToPlace.slice(1).reduce( (placedElements, rect) => { const futureWord = futurPosition(rect, placedElements, 3, weight); - return [...placedElements, futureWord]; }, [centeredRect] @@ -60,9 +65,14 @@ const Wordcloud = ({ }); }; - // const parent = updateParent( - // words.filter((w) => Boolean(w.rect)).map((w) => w.rect) as Rectangle[] - // ); + console.log("before", rectParent); + + const parent = boundParent( + words.filter((w) => Boolean(w.rect)).map((w) => w.rect) as Rectangle[], + rectParent + ); + + console.log("after", parent); React.useEffect(() => { updateWords(); @@ -75,7 +85,7 @@ const Wordcloud = ({ width={width} height={height} style={{ outline: "1px solid green" }} - // viewBox={`${parent.centerX} ${parent.centerY} 1000 1000`} + viewBox={`${parent.x} ${parent.y} ${parent.width} ${parent.height}`} > {words.map((word) => { const fontSize = diff --git a/src/utils.ts b/src/utils.ts index 6273a17..192a81c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,4 @@ +import { R } from "vitest/dist/types-7cd96283"; import { CONTAINER_HEIGHT, CONTAINER_WIDTH, @@ -30,6 +31,13 @@ export type Circle = { radius: number; }; +export type Bound = { + topX: number; + topY: number; + bottomX: number; + bottomY: number; +}; + export const getBoundingRect = ( id: string, tagName: "svg" | "text" = "text" @@ -236,64 +244,55 @@ export const allCollision = (word: Rectangle, passRect: Rectangle[]): boolean => ) .some((t) => t === true); -// export const updateParent = (passRect: Rectangle[]) => { -// const centerMass = centerOfMass(passRect); - -// rectParent.centerX = centerMass.x; -// rectParent.centerY = centerMass.y; - -// const rectX = passRect.map((word) => word.x); -// const maxX = Math.max(...rectX); -// const minX = Math.min(...rectX); - -// const distanceMaxX = maxX - rectParent.centerX; -// const distanceMinX = rectParent.centerX - minX; - -// if (distanceMaxX > distanceMinX && distanceMaxX > rectParent.width / 2) { -// rectParent.width = rectParent.centerX + (distanceMaxX + 10); -// } else if ( -// distanceMinX > distanceMaxX && -// distanceMinX > rectParent.width / 2 -// ) { -// rectParent.width = rectParent.centerX + (distanceMinX + 10); -// } - -// const rectY = passRect.map((word) => word.y); -// const maxY = Math.max(...rectY); -// const minY = Math.min(...rectY); - -// const distanceMaxY = maxY - rectParent.centerY; -// const distanceMinY = rectParent.centerY - minY; - -// if (distanceMaxY > distanceMinY && distanceMaxY > rectParent.height / 2) { -// rectParent.height = rectParent.centerY + (distanceMaxY + 10); -// } else if ( -// distanceMinY > distanceMaxY && -// distanceMinY > rectParent.height / 2 -// ) { -// rectParent.height = rectParent.centerY + (distanceMinY + 10); -// } - -// return rectParent; - -// rectParent.height = centerMass.y * coef; -// rectParent.width = centerMass.x * coef; - -// if ( -// maxDistanceYBottom > rectParent.height && -// maxDistanceYBottom > maxDistanceXRight -// ) { -// const coef = maxDistanceYBottom / rectParent.height; -// rectParent.height *= coef; -// rectParent.width *= coef; -// } - -// if ( -// maxDistanceXRight > rectParent.width && -// maxDistanceXRight > maxDistanceYBottom -// ) { -// const coef = maxDistanceXRight / rectParent.width; - -// rectParent.width *= coef; -// rectParent.height *= coef; -// } +export const boundParent = ( + passRect: Rectangle[], + parent: Rectangle +): Rectangle => { + const newParentBound = passRect.reduce( + (bound, rect) => { + const topLeftRect = { + x: rect.x - rect.width / 2, + y: rect.y - rect.height / 2, + }; + + // console.log(topLeftRect); + + const bottomRightRect = { + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + }; + + // const distX = topLeftRect.x < bound.x; + + // value on left + if (topLeftRect.x < bound.x) { + bound.x = topLeftRect.x; + } + + // value on top + if (topLeftRect.y < bound.y) { + bound.y = topLeftRect.y; + } + // value on right + + if (bottomRightRect.x > bound.width) { + bound.width = bottomRightRect.x; + } + // value on bottom + + if (bottomRightRect.y > bound.height) { + bound.height = bottomRightRect.y; + } + + return bound; + }, + { + x: parent.x - parent.width / 2, + y: parent.y - parent.height / 2, + width: parent.width, + height: parent.height, + } + ); + + return newParentBound; +}; From 3f3af71d9237b9af1199b76cfa64cd78f3943354 Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Fri, 17 Mar 2023 09:56:41 +0100 Subject: [PATCH 09/16] fix: bound of the word cloud --- src/Wordcloud.tsx | 4 ---- src/utils.ts | 8 ++------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Wordcloud.tsx b/src/Wordcloud.tsx index fd80c12..cb9fecc 100644 --- a/src/Wordcloud.tsx +++ b/src/Wordcloud.tsx @@ -65,15 +65,11 @@ const Wordcloud = ({ }); }; - console.log("before", rectParent); - const parent = boundParent( words.filter((w) => Boolean(w.rect)).map((w) => w.rect) as Rectangle[], rectParent ); - console.log("after", parent); - React.useEffect(() => { updateWords(); }, []); diff --git a/src/utils.ts b/src/utils.ts index 192a81c..eb22a48 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -255,15 +255,11 @@ export const boundParent = ( y: rect.y - rect.height / 2, }; - // console.log(topLeftRect); - const bottomRightRect = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, }; - // const distX = topLeftRect.x < bound.x; - // value on left if (topLeftRect.x < bound.x) { bound.x = topLeftRect.x; @@ -273,13 +269,13 @@ export const boundParent = ( if (topLeftRect.y < bound.y) { bound.y = topLeftRect.y; } - // value on right + // value on right if (bottomRightRect.x > bound.width) { bound.width = bottomRightRect.x; } - // value on bottom + // value on bottom if (bottomRightRect.y > bound.height) { bound.height = bottomRightRect.y; } From 4b9b9589301d50a949b51ce5cf5b98a33a763369 Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Fri, 17 Mar 2023 11:18:42 +0100 Subject: [PATCH 10/16] feat: get the slice and slice the word --- src/utils.test.ts | 103 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 31 +++++++++++++- 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 11beac9..5b5b70c 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -9,6 +9,8 @@ import { getDistance, randomInterval, cumulativeBins, + sliceWords, + getSliceOfWords, } from "./utils"; const origin: Coordinate = { @@ -197,3 +199,104 @@ describe("CumulativeBins", () => { expect(cumulativeBins([4, -2, 1, 3])).toEqual([4, 2, 3, 6]); }); }); + +describe("slideWords", () => { + describe("Slide one word", () => { + it("On the bottom", () => { + expect(sliceWords([originRectangle], { x: 0, y: 2 })).toEqual([ + { x: 0, y: 2, width: 1, height: 1 }, + ]); + }); + + it("On the top", () => { + expect( + sliceWords([{ x: 1, y: 4, width: 4, height: 4 }], { x: 0, y: -2 }) + ).toEqual([{ x: 1, y: 2, width: 4, height: 4 }]); + }); + + it("On the right", () => { + expect( + sliceWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: 2, y: 0 }) + ).toEqual([{ x: 4, y: 0, width: 1, height: 1 }]); + }); + + it("On the left", () => { + expect( + sliceWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: -2, y: 0 }) + ).toEqual([{ x: 0, y: 0, width: 1, height: 1 }]); + }); + + it("On the right and bottom", () => { + expect( + sliceWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: 2, y: 4 }) + ).toEqual([{ x: 4, y: 4, width: 1, height: 1 }]); + }); + + it("On the left and top", () => { + expect( + sliceWords([{ x: 2, y: 4, width: 1, height: 1 }], { x: -2, y: -2 }) + ).toEqual([{ x: 0, y: 2, width: 1, height: 1 }]); + }); + }); + + describe("Slide multiple word", () => { + it("On the bottom and right", () => { + expect( + sliceWords( + [ + { x: 1, y: 4, width: 4, height: 4 }, + { x: 6, y: 9, width: 4, height: 4 }, + ], + { x: 1, y: 2 } + ) + ).toEqual([ + { x: 2, y: 6, width: 4, height: 4 }, + { x: 7, y: 11, width: 4, height: 4 }, + ]); + }); + + it("On the top and left", () => { + expect( + sliceWords( + [ + { x: 1, y: 4, width: 4, height: 4 }, + { x: 6, y: 9, width: 4, height: 4 }, + ], + { x: -1, y: -2 } + ) + ).toEqual([ + { x: 0, y: 2, width: 4, height: 4 }, + { x: 5, y: 7, width: 4, height: 4 }, + ]); + }); + }); +}); + +describe("getSliceOfWords", () => { + it("No move", () => { + expect( + getSliceOfWords( + { x: 3, y: 3, width: 4, height: 4 }, + { x: 1, y: 1, width: 4, height: 4 } + ) + ).toEqual({ x: 0, y: 0 }); + }); + + it("Move on the right", () => { + expect( + getSliceOfWords( + { x: 2, y: 3, width: 4, height: 4 }, + { x: 1, y: 1, width: 4, height: 4 } + ) + ).toEqual({ x: 1, y: 0 }); + }); + + it("Move on the left and top", () => { + expect( + getSliceOfWords( + { x: 7, y: 8, width: 4, height: 4 }, + { x: 3, y: 5, width: 4, height: 4 } + ) + ).toEqual({ x: -2, y: -1 }); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index eb22a48..9570901 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { R } from "vitest/dist/types-7cd96283"; +import { p, R } from "vitest/dist/types-7cd96283"; import { CONTAINER_HEIGHT, CONTAINER_WIDTH, @@ -292,3 +292,32 @@ export const boundParent = ( return newParentBound; }; + +export const getSliceOfWords = ( + parent: Rectangle, + bound: Rectangle +): Coordinate => { + const boundCentered: Rectangle = { + x: bound.x + bound.width / 2, + y: bound.y + bound.height / 2, + width: bound.width, + height: bound.height, + }; + + const differenceX = boundCentered.x - parent.x; + const differenceY = boundCentered.y - parent.y; + + return { x: differenceX, y: differenceY }; +}; + +export const sliceWords = ( + words: Rectangle[], + slice: Coordinate +): Rectangle[] => { + words.map((w) => { + w.x = w.x + slice.x; + w.y = w.y + slice.y; + }); + + return words; +}; From ecfdac2c28bbb399cf816c6602e9ca23141df50b Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Fri, 17 Mar 2023 12:06:35 +0100 Subject: [PATCH 11/16] fix: resolve comments on PR --- src/utils.test.ts | 26 +++++++++++++------------- src/utils.ts | 13 +++++++++---- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 5b5b70c..ff34386 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -9,8 +9,8 @@ import { getDistance, randomInterval, cumulativeBins, - sliceWords, - getSliceOfWords, + slideWords, + getWordSlide, } from "./utils"; const origin: Coordinate = { @@ -203,38 +203,38 @@ describe("CumulativeBins", () => { describe("slideWords", () => { describe("Slide one word", () => { it("On the bottom", () => { - expect(sliceWords([originRectangle], { x: 0, y: 2 })).toEqual([ + expect(slideWords([originRectangle], { x: 0, y: 2 })).toEqual([ { x: 0, y: 2, width: 1, height: 1 }, ]); }); it("On the top", () => { expect( - sliceWords([{ x: 1, y: 4, width: 4, height: 4 }], { x: 0, y: -2 }) + slideWords([{ x: 1, y: 4, width: 4, height: 4 }], { x: 0, y: -2 }) ).toEqual([{ x: 1, y: 2, width: 4, height: 4 }]); }); it("On the right", () => { expect( - sliceWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: 2, y: 0 }) + slideWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: 2, y: 0 }) ).toEqual([{ x: 4, y: 0, width: 1, height: 1 }]); }); it("On the left", () => { expect( - sliceWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: -2, y: 0 }) + slideWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: -2, y: 0 }) ).toEqual([{ x: 0, y: 0, width: 1, height: 1 }]); }); it("On the right and bottom", () => { expect( - sliceWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: 2, y: 4 }) + slideWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: 2, y: 4 }) ).toEqual([{ x: 4, y: 4, width: 1, height: 1 }]); }); it("On the left and top", () => { expect( - sliceWords([{ x: 2, y: 4, width: 1, height: 1 }], { x: -2, y: -2 }) + slideWords([{ x: 2, y: 4, width: 1, height: 1 }], { x: -2, y: -2 }) ).toEqual([{ x: 0, y: 2, width: 1, height: 1 }]); }); }); @@ -242,7 +242,7 @@ describe("slideWords", () => { describe("Slide multiple word", () => { it("On the bottom and right", () => { expect( - sliceWords( + slideWords( [ { x: 1, y: 4, width: 4, height: 4 }, { x: 6, y: 9, width: 4, height: 4 }, @@ -257,7 +257,7 @@ describe("slideWords", () => { it("On the top and left", () => { expect( - sliceWords( + slideWords( [ { x: 1, y: 4, width: 4, height: 4 }, { x: 6, y: 9, width: 4, height: 4 }, @@ -275,7 +275,7 @@ describe("slideWords", () => { describe("getSliceOfWords", () => { it("No move", () => { expect( - getSliceOfWords( + getWordSlide( { x: 3, y: 3, width: 4, height: 4 }, { x: 1, y: 1, width: 4, height: 4 } ) @@ -284,7 +284,7 @@ describe("getSliceOfWords", () => { it("Move on the right", () => { expect( - getSliceOfWords( + getWordSlide( { x: 2, y: 3, width: 4, height: 4 }, { x: 1, y: 1, width: 4, height: 4 } ) @@ -293,7 +293,7 @@ describe("getSliceOfWords", () => { it("Move on the left and top", () => { expect( - getSliceOfWords( + getWordSlide( { x: 7, y: 8, width: 4, height: 4 }, { x: 3, y: 5, width: 4, height: 4 } ) diff --git a/src/utils.ts b/src/utils.ts index 9570901..81430f6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,3 @@ -import { p, R } from "vitest/dist/types-7cd96283"; import { CONTAINER_HEIGHT, CONTAINER_WIDTH, @@ -105,9 +104,13 @@ export const getTheCircle = (passRect: Rectangle[]): Circle => { 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 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( ( @@ -134,8 +137,10 @@ export const placeWordOnOuterCircle = ( weight[inter] += 1; + const maxWeight = Math.max(...weight); + // substract the max to each element to promote other interval - weight = weight.map((a) => Math.max(...weight) - a); + weight = weight.map((a) => maxWeight - a); let angleInter = { x: 0, y: 360 }; @@ -293,7 +298,7 @@ export const boundParent = ( return newParentBound; }; -export const getSliceOfWords = ( +export const getWordSlide = ( parent: Rectangle, bound: Rectangle ): Coordinate => { @@ -310,7 +315,7 @@ export const getSliceOfWords = ( return { x: differenceX, y: differenceY }; }; -export const sliceWords = ( +export const slideWords = ( words: Rectangle[], slice: Coordinate ): Rectangle[] => { From 8b772d5ebb08a59d7ab64accd22f3c86c4ac9e4c Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Fri, 17 Mar 2023 14:31:10 +0100 Subject: [PATCH 12/16] fix: test on bounds --- src/Wordcloud.tsx | 2 ++ src/utils.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Wordcloud.tsx b/src/Wordcloud.tsx index cb9fecc..c694e51 100644 --- a/src/Wordcloud.tsx +++ b/src/Wordcloud.tsx @@ -70,6 +70,8 @@ const Wordcloud = ({ rectParent ); + console.log("bound", parent); + React.useEffect(() => { updateWords(); }, []); diff --git a/src/utils.ts b/src/utils.ts index 81430f6..03b84e2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -253,6 +253,7 @@ export const boundParent = ( passRect: Rectangle[], parent: Rectangle ): Rectangle => { + console.log("max", Math.min(...passRect.map((w) => w.x - w.width / 2))); const newParentBound = passRect.reduce( (bound, rect) => { const topLeftRect = { From 818bac832e6c32b5e7b97ddd6a9a7141be8a2041 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Fri, 17 Mar 2023 15:23:07 +0100 Subject: [PATCH 13/16] fix: compute bounding box --- src/Wordcloud.tsx | 32 +++++++++++++++---- src/utils.ts | 78 ++++++++++++++--------------------------------- 2 files changed, 49 insertions(+), 61 deletions(-) diff --git a/src/Wordcloud.tsx b/src/Wordcloud.tsx index c694e51..759fbd4 100644 --- a/src/Wordcloud.tsx +++ b/src/Wordcloud.tsx @@ -9,6 +9,7 @@ import { import * as React from "react"; import { CONTAINER_HEIGHT, CONTAINER_WIDTH, DEFAULT_RECT } from "./constants"; +import { defaultWords1 } from "./data"; const CUT_OFF = 0.5; @@ -65,12 +66,19 @@ const Wordcloud = ({ }); }; - const parent = boundParent( - words.filter((w) => Boolean(w.rect)).map((w) => w.rect) as Rectangle[], - rectParent - ); + // casting is fine here https://codereview.stackexchange.com/questions/135363/filtering-undefined-elements-out-of-an-array + const rects = words.map((w) => w.rect).filter(Boolean) as Rectangle[]; + + const bound = rects.length + ? boundParent(rects) + : { + x: 0, + y: 0, + width: width, + height: height, + }; - console.log("bound", parent); + console.log("bound", bound); React.useEffect(() => { updateWords(); @@ -83,7 +91,7 @@ const Wordcloud = ({ width={width} height={height} style={{ outline: "1px solid green" }} - viewBox={`${parent.x} ${parent.y} ${parent.width} ${parent.height}`} + viewBox={`${bound.x} ${bound.y} ${bound.width} ${bound.height}`} > {words.map((word) => { const fontSize = @@ -126,6 +134,18 @@ const Wordcloud = ({ stroke="orange" strokeWidth="1" /> + + + viewBox + + + ); }; diff --git a/src/utils.ts b/src/utils.ts index 03b84e2..e6c6037 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,19 +24,19 @@ export type Coordinate = { y: number; }; +export type Bound = { + xMin: number; + xMax: number; + yMin: number; + yMax: number; +}; + export type Circle = { x: number; y: number; radius: number; }; -export type Bound = { - topX: number; - topY: number; - bottomX: number; - bottomY: number; -}; - export const getBoundingRect = ( id: string, tagName: "svg" | "text" = "text" @@ -249,54 +249,22 @@ export const allCollision = (word: Rectangle, passRect: Rectangle[]): boolean => ) .some((t) => t === true); -export const boundParent = ( - passRect: Rectangle[], - parent: Rectangle -): Rectangle => { - console.log("max", Math.min(...passRect.map((w) => w.x - w.width / 2))); - const newParentBound = passRect.reduce( - (bound, rect) => { - const topLeftRect = { - x: rect.x - rect.width / 2, - y: rect.y - rect.height / 2, - }; - - const bottomRightRect = { - x: rect.x + rect.width / 2, - y: rect.y + rect.height / 2, - }; - - // value on left - if (topLeftRect.x < bound.x) { - bound.x = topLeftRect.x; - } - - // value on top - if (topLeftRect.y < bound.y) { - bound.y = topLeftRect.y; - } - - // value on right - if (bottomRightRect.x > bound.width) { - bound.width = bottomRightRect.x; - } - - // value on bottom - if (bottomRightRect.y > bound.height) { - bound.height = bottomRightRect.y; - } - - return bound; - }, - { - x: parent.x - parent.width / 2, - y: parent.y - parent.height / 2, - width: parent.width, - height: parent.height, - } - ); - - return newParentBound; +export const boundParent = (rects: Rectangle[]): Rectangle => { + const topLeftPoints: Coordinate[] = rects.map((r) => ({ + x: r.x - r.width / 2, + y: r.y - r.height / 2, + })); + const bottomRightPoints: Coordinate[] = rects.map((r) => ({ + x: r.x + r.width / 2, + y: r.y + r.height / 2, + })); + + 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: xMax - xMin, height: yMax - yMin }; }; export const getWordSlide = ( From 9b83fc77f94ba99d215a4b20011b2464987a96fa Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Fri, 17 Mar 2023 16:33:04 +0100 Subject: [PATCH 14/16] feat: unit test and comments --- src/utils.test.ts | 52 ++++++++++++++++++++++++++++------------------- src/utils.ts | 12 ++++++++++- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index ff34386..f040579 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -11,6 +11,7 @@ import { cumulativeBins, slideWords, getWordSlide, + boundParent, } from "./utils"; const origin: Coordinate = { @@ -202,40 +203,36 @@ describe("CumulativeBins", () => { describe("slideWords", () => { describe("Slide one word", () => { - it("On the bottom", () => { - expect(slideWords([originRectangle], { x: 0, y: 2 })).toEqual([ - { x: 0, y: 2, width: 1, height: 1 }, + it("No move", () => { + expect(slideWords([originRectangle], { x: 0, y: 0 })).toEqual([ + originRectangle, ]); }); - - it("On the top", () => { + it("On one direction", () => { + expect( + slideWords([{ x: 0, y: 0, width: 1, height: 1 }], { x: 0, y: 2 }) + ).toEqual([{ x: 0, y: 2, width: 1, height: 1 }]); expect( slideWords([{ x: 1, y: 4, width: 4, height: 4 }], { x: 0, y: -2 }) ).toEqual([{ x: 1, y: 2, width: 4, height: 4 }]); - }); - - it("On the right", () => { expect( slideWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: 2, y: 0 }) ).toEqual([{ x: 4, y: 0, width: 1, height: 1 }]); - }); - - it("On the left", () => { expect( slideWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: -2, y: 0 }) ).toEqual([{ x: 0, y: 0, width: 1, height: 1 }]); }); - it("On the right and bottom", () => { + it("On multiple direction", () => { expect( slideWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: 2, y: 4 }) ).toEqual([{ x: 4, y: 4, width: 1, height: 1 }]); - }); - - it("On the left and top", () => { expect( slideWords([{ x: 2, y: 4, width: 1, height: 1 }], { x: -2, y: -2 }) ).toEqual([{ x: 0, y: 2, width: 1, height: 1 }]); + expect( + slideWords([{ x: 2, y: 0, width: 1, height: 1 }], { x: -1, y: 4 }) + ).toEqual([{ x: 1, y: 4, width: 1, height: 1 }]); }); }); @@ -253,9 +250,6 @@ describe("slideWords", () => { { x: 2, y: 6, width: 4, height: 4 }, { x: 7, y: 11, width: 4, height: 4 }, ]); - }); - - it("On the top and left", () => { expect( slideWords( [ @@ -289,9 +283,6 @@ describe("getSliceOfWords", () => { { x: 1, y: 1, width: 4, height: 4 } ) ).toEqual({ x: 1, y: 0 }); - }); - - it("Move on the left and top", () => { expect( getWordSlide( { x: 7, y: 8, width: 4, height: 4 }, @@ -300,3 +291,22 @@ describe("getSliceOfWords", () => { ).toEqual({ x: -2, y: -1 }); }); }); + +describe("BoundParent", () => { + it("With one rectangle", () => { + expect(boundParent([{ x: 2, y: 2, width: 2, height: 2 }])).toEqual({ + x: 1, + y: 1, + 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: 1, y: 3, width: 8, height: 7 }); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index e6c6037..bf7f809 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -67,10 +67,12 @@ export const setFirstWordInCenterOfParent = (w: Word, p: string): Rectangle => { 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, word: Rectangle): number => { return Math.sqrt((point.x - word.x) ** 2 + (point.y - word.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) => { @@ -88,6 +90,7 @@ export const centerOfMass = (passRect: Rectangle[]): Coordinate => { return centerMass; }; +// This function get a circle with centre the center of mass and radius the distance the farthest point from the centre export const getTheCircle = (passRect: Rectangle[]): Circle => { const centerMass = centerOfMass(passRect); @@ -120,7 +123,7 @@ export const cumulativeBins = (bin: number[]): number[] => { ); }; -// This function put the word in a random place +// This function put the word in a random place on a circle export const placeWordOnOuterCircle = ( w: Rectangle, passRect: Rectangle[], @@ -164,6 +167,7 @@ export const placeWordOnOuterCircle = ( 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[], currentWord: Rectangle @@ -180,6 +184,7 @@ export const getMoveDirection = ( ); }; +// This function returns the futur position of a rectangle, without collision, in direction of already placed rectangles export const futurPosition = ( word: Rectangle, passRect: Rectangle[], @@ -228,6 +233,7 @@ export const futurPosition = ( return movedWord; }; +// This function indicates whether rectangles are in a collision export const areCentersTooClose = ( centerA: Coordinate, centerB: Coordinate, @@ -237,6 +243,7 @@ export const areCentersTooClose = ( Math.abs(centerA.x - centerB.x) < minX && Math.abs(centerA.y - centerB.y) < minY; +// This function computes the collisions export const allCollision = (word: Rectangle, passRect: Rectangle[]): boolean => passRect .map((rect) => @@ -249,6 +256,7 @@ export const allCollision = (word: Rectangle, passRect: Rectangle[]): boolean => ) .some((t) => t === true); +// This function returns the bound of the word cloud export const boundParent = (rects: Rectangle[]): Rectangle => { const topLeftPoints: Coordinate[] = rects.map((r) => ({ x: r.x - r.width / 2, @@ -267,6 +275,7 @@ export const boundParent = (rects: Rectangle[]): Rectangle => { return { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin }; }; +// This function get the slide of the word cloud export const getWordSlide = ( parent: Rectangle, bound: Rectangle @@ -284,6 +293,7 @@ export const getWordSlide = ( return { x: differenceX, y: differenceY }; }; +// This function slides a rectangle export const slideWords = ( words: Rectangle[], slice: Coordinate From 8707e62acea15278f2f18413e64d4687a3c6f8b7 Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Mon, 20 Mar 2023 09:40:26 +0100 Subject: [PATCH 15/16] fix: review comments --- src/Wordcloud.tsx | 26 ++++++++++---------------- src/utils.ts | 22 +++++++++------------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/Wordcloud.tsx b/src/Wordcloud.tsx index 759fbd4..4b87b51 100644 --- a/src/Wordcloud.tsx +++ b/src/Wordcloud.tsx @@ -29,12 +29,8 @@ const Wordcloud = ({ }: Props) => { const [words, setWords] = React.useState(data); - const rectParent = { - x: width / 2, - y: height / 2, - width: width, - height: height, - }; + const centerX = width / 2; + const centerY = height / 2; const updateWords = () => { setWords((prevWords) => { @@ -46,8 +42,8 @@ const Wordcloud = ({ const centeredRect = { width: firstRect.width, height: firstRect.height, - x: width / 2, - y: height / 2, + x: centerX, + y: centerY, }; const weight = [1, 1, 1, 1]; @@ -78,8 +74,6 @@ const Wordcloud = ({ height: height, }; - console.log("bound", bound); - React.useEffect(() => { updateWords(); }, []); @@ -108,17 +102,17 @@ const Wordcloud = ({ fontSize={fontSize} style={{ outline: "1px solid rgba(255, 0, 0, 0.1)" }} id={word.id} - x={(word.rect?.x || width / 2).toString()} + x={(word.rect?.x || centerX).toString()} // I don't know why I have to add the third of the fontSize to center te word vertically but it works - y={((word.rect?.y || height / 2) + fontSize / 3).toString()} + y={((word.rect?.y || centerY) + fontSize / 3).toString()} > {word.text} ); })} { return centerMass; }; -// This function get a circle with centre the center of mass and radius the distance the farthest point from the centre +// 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); @@ -123,26 +116,29 @@ export const cumulativeBins = (bin: number[]): number[] => { ); }; -// This function put the word in a random place on a circle +// This function puts the word in a random place on a circle export const placeWordOnOuterCircle = ( w: Rectangle, passRect: Rectangle[], weight: number[] ): Rectangle => { - // Chose the parent face + // 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(weight); const randomInter = randomInterval( 0, cumulativeWeight[cumulativeWeight.length - 1] ); + const inter = cumulativeWeight.findIndex((el) => el >= randomInter); + // Add to weights the position that has just been drawn weight[inter] += 1; const maxWeight = Math.max(...weight); - // substract the max to each element to promote other interval + // Substract the max to each element to promote other interval weight = weight.map((a) => maxWeight - a); let angleInter = { x: 0, y: 360 }; @@ -275,7 +271,7 @@ export const boundParent = (rects: Rectangle[]): Rectangle => { return { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin }; }; -// This function get the slide of the word cloud +// This function gets the slide of the word cloud export const getWordSlide = ( parent: Rectangle, bound: Rectangle @@ -293,7 +289,7 @@ export const getWordSlide = ( return { x: differenceX, y: differenceY }; }; -// This function slides a rectangle +// This function slides an array of rectangles export const slideWords = ( words: Rectangle[], slice: Coordinate From c9e39760468613f9117eb189d6c82fdd100886ba Mon Sep 17 00:00:00 2001 From: RoxaneBurri Date: Mon, 20 Mar 2023 12:03:02 +0100 Subject: [PATCH 16/16] fix: inverted weights --- src/pseudocode.txt | 229 --------------------------------------------- src/utils.ts | 18 ++-- 2 files changed, 9 insertions(+), 238 deletions(-) delete mode 100644 src/pseudocode.txt diff --git a/src/pseudocode.txt b/src/pseudocode.txt deleted file mode 100644 index f3e1afa..0000000 --- a/src/pseudocode.txt +++ /dev/null @@ -1,229 +0,0 @@ -// triangle : th, tx, ty, th hypotenus - - -// Data -const defaultWords = [ - { id: "1234", text: "test", x: 10, y: 100 }, - { id: "2345", text: "hello", x: 50, y: 10 }, - { id: "3456", text: "haha", x: 130, y: 40 }, -]; - -// sort the dataset -function sortData(data){ - return data.x.sort() -} - -function centre(w){ - x = x + w/2 - y = y + h/2 - - return x, y -} - -function distance(p1, p2){ - return sqrt((p2.x - p1.x)^2 + (p2.y-p1.y)^2) -} - -// force résultante entre un rectangle (en son centre) et tout les centres des autres rectangles -// input: current point, list[center of all already done rect] -// output: a line (by equation or two points) - -function netForce(point, passRect){ - - distance = [] - Force = [] - - xForce = [] - yForce = [] - - for(rect in passRect){ - - // calculate distance between each rectangles - d = distance(point, rect) - distance.push(d) - - - // calculate the force of gravity between the object - F = G * 1 / d^2 - - Force.push(F) - - // calculate the x anf y components of the forces - - angle = arctan((rect.y - point.y)/(rect.x - point.x)) - - Fx = F * cos(angle) - Fy = F * sin(angle) - - xForce.push(Fx) - yForce.push(Fy) - - sumXForce = xForce.sum() - sumYForce = yForce.sum() - - coordinateF = sumXForce * i + sumYForce * j - - - - return [sumXForce, sumYForce] - - } -} - -function netForceMethod1(point, passRect){ - -} - -// fonction qui retourne un triangle rectangle avec comme hypoténus la droite de la force résultante - -function getTriangleFromNetForce(netForce){ - return th, tx, ty - -} - -// fonction qui donne la nouvelle valeur de x, suivant la droite de la force résultante, pour un déplacement d'une valeur step - -function stepX(x, th, tx, step){ - val = (step/th) * tx - return x + val -} - -// fonction qui donne la nouvelle valeur de y, suivant la droite de la force résultante, pour un déplacement d'une valeur step - -function stepY(x, th, ty, step){ - val = (step/th) * ty - return y + val -} - - - -function futurPosition(rect, passRect, step){ - - netForce = netForce(rect, passRect) - th, tx, ty = getTriangleFromNetForce(netForce) - - xFutur = stepX(rect.x, th, tx, step) - yFutur = stepY(rect.y, th, ty, step) - - tmp.x = xFutur - tmp.y = yFutur - - - if (allCollision(tmp, passRect)){ - - collisionDirection = [0, 0] - - case 1: - tmp.x = rect.x - if (allCollision(tmp, passRect)){ - // there is a collision on y - collisionDirection[1] = 1 - } - - case 2: - tmp.y = rect.y - if (allCollision(tmp, passRect)){ - // there is a collision on x - collisionDirection[0] = 1 - } - - if (collisionDirection == [1, 1]){ - // collison on both direction - // return position initial de rect - return rect - - } else { - if (collisionDirection == [1, 0]){ - // collision on x - - rect.y += xFutur - futurPosition(rect, passRect, step) - } - - if (collisionDirection == [0, 1]){ - // collision on y - - rect.x += yFutur - futurPosition(rect, passRect, step) - } - } - } else { - futurPosition(tmp, passRect, step) - } - -} - - - -// fonction qui retourne true s'il y a une collision - -function collision(rect1, rect2){ - const x1 = word1.x; - const y1 = word1.y; - const w1 = document.getElementById(word1.id).getBoundingClientRect().width; - const h1 = document.getElementById(word1.id).getBoundingClientRect().height; - - const x2 = word2.x; - const y2 = word2.y; - const w2 = document.getElementById(word2.id).getBoundingClientRect().width; - const h2 = document.getElementById(word2.id).getBoundingClientRect().height; - - if (x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && h1 + y1 > y2) { - console.log("collision"); - return collision; - } else { - return not collision; - } -} - -// fonction qui retourne true s'il y a une collision entre le rectangle et les rectangles déjà posé - -function allCollision(rect, passRect) - wordArray.for((w: { id: string; text: string; x: number; y: number }) => { - if (word.id != w.id) { - if (collision(word, w)) { - console.log("collision"); - return true; - } else { - console.log("No collision"); - - return false; - } - } - return false; - }); - - - - function main(){ - - passRect = [] - - defaultWords = sortData(defaultWords) - - // iterate over each word, already put word in passWord array - defaultWords.forEach(w => - - if (passRect == empty){ - w.x = center - w.y = center - - passRect.push(w) - } else { - - w = futurPosition(w, passWord, step) - passRect.push(w) - - } - - return w; - - ) - - } - - function finish(passRect){ - if (passRect == passWord){ - // the word cloud is finish - } - } \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 401a5cf..0c6ad35 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -122,9 +122,14 @@ export const placeWordOnOuterCircle = ( passRect: Rectangle[], weight: number[] ): Rectangle => { + const maxWeight = Math.max(...weight); + + // Substract 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(weight); + const cumulativeWeight = cumulativeBins(invertedWeight); const randomInter = randomInterval( 0, @@ -136,11 +141,6 @@ export const placeWordOnOuterCircle = ( // Add to weights the position that has just been drawn weight[inter] += 1; - const maxWeight = Math.max(...weight); - - // Substract the max to each element to promote other interval - weight = weight.map((a) => maxWeight - a); - let angleInter = { x: 0, y: 360 }; if (inter === 0) { @@ -189,7 +189,7 @@ export const futurPosition = ( ): Rectangle => { let isCollision = false; - // put the word in random place around the parent + // Put the word in random place around the parent let movedWord = placeWordOnOuterCircle(word, passRect, weight); let iter = 0; let displacement = 0; @@ -204,7 +204,7 @@ export const futurPosition = ( y: movedWord.y + (Math.abs(stepY) > 0.01 ? stepY : 0), }; - // test if the word can be move over the hypotenuse + // Test if the word can be move over the hypotenuse if (allCollision(futurPosition, passRect)) { const onlyMoveOverX = { ...futurPosition, y: movedWord.y }; const onlyMoveOverY = { ...futurPosition, x: movedWord.x }; @@ -212,7 +212,7 @@ export const futurPosition = ( const yColl = allCollision(onlyMoveOverY, passRect); if (xColl) { if (yColl) { - // do not move anymore + // Do not move anymore isCollision = true; } else { movedWord = { ...onlyMoveOverY };