import React, { useRef } from 'react'; import ReactFauxDom from 'react-faux-dom'; import cloud from 'd3-cloud'; import isDeepEqual from 'react-fast-compare'; import { BaseType, ValueFn, select } from 'd3-selection'; import { scaleOrdinal } from 'd3-scale'; import { schemeCategory10 } from 'd3-scale-chromatic'; interface Datum { text: string; value: number; } export interface Word extends cloud.Word { text: string; value: number; } type WordCloudProps = { data: Datum[]; width?: number; height?: number; font?: string | ((word: Word, index: number) => string); fontStyle?: string | ((word: Word, index: number) => string); fontWeight?: | string | number | ((word: Word, index: number) => string | number); fontSize?: number | ((word: Word, index: number) => number); rotate?: number | ((word: Word, index: number) => number); spiral?: | 'archimedean' | 'rectangular' | ((size: [number, number]) => (t: number) => [number, number]); padding?: number | ((word: Word, index: number) => number); random?: () => number; fill?: ValueFn; onWordClick?: (this: BaseType, event: any, d: Word) => void; onWordMouseOver?: (this: BaseType, event: any, d: Word) => void; onWordMouseOut?: (this: BaseType, event: any, d: Word) => void; }; const defaultScaleOrdinal = scaleOrdinal(schemeCategory10); function WordCloud({ data, width = 700, height = 600, font = 'serif', fontStyle = 'normal', fontWeight = 'normal', fontSize = (d) => Math.sqrt(d.value), // eslint-disable-next-line no-bitwise rotate = () => (~~(Math.random() * 6) - 3) * 30, spiral = 'archimedean', padding = 1, random = Math.random, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore The ordinal function should accept number fill = (_, i) => defaultScaleOrdinal(i), onWordClick, onWordMouseOver, onWordMouseOut, }: WordCloudProps) { const elementRef = useRef(); if (!elementRef.current) { elementRef.current = ReactFauxDom.createElement('div'); } const el = elementRef.current; // clear old words select(el).selectAll('*').remove(); // render based on new data const layout = cloud() .words(data) .size([width, height]) .font(font) .fontStyle(fontStyle) .fontWeight(fontWeight) .fontSize(fontSize) .rotate(rotate) .spiral(spiral) .padding(padding) .random(random) .on('end', (words) => { const [w, h] = layout.size(); const texts = select(el) .append('svg') .attr('viewBox', `0 0 ${w} ${h}`) .attr('preserveAspectRatio', 'xMinYMin meet') .append('g') .attr('transform', `translate(${w / 2},${h / 2})`) .selectAll('text') .data(words) .enter() .append('text') .style( 'font-family', ((d) => d.font) as ValueFn ) .style( 'font-style', ((d) => d.style) as ValueFn ) .style( 'font-weight', ((d) => d.weight) as ValueFn ) .style( 'font-size', ((d) => `${d.size}px`) as ValueFn ) .style('fill', fill) .attr('text-anchor', 'middle') .attr('transform', (d) => `translate(${[d.x, d.y]})rotate(${d.rotate})`) .text((d) => d.text); if (onWordClick) { texts.on('click', onWordClick); } if (onWordMouseOver) { texts.on('mouseover', onWordMouseOver); } if (onWordMouseOut) { texts.on('mouseout', onWordMouseOut); } }); layout.start(); return el.toReact(); } export default React.memo(WordCloud, isDeepEqual);