import d3TagCloud from 'd3-cloud' ; import React from 'react' ; import { connect } from 'react-redux' ; import { bindActionCreators , Dispatch } from 'redux' ; import { ScreenReaderSummary } from '../../../../components/accessibility' ; import { SettingsSpec , WordCloudElementEvent } from '../../../../specs/settings' ; import { onChartRendered } from '../../../../state/actions/chart' ; import { GlobalChartState } from '../../../../state/chart_state' ; import { A11ySettings , DEFAULT_A11Y_SETTINGS , getA11ySettingsSelector , } from '../../../../state/selectors/get_accessibility_config' ; import { getInternalIsInitializedSelector , InitStatus } from '../../../../state/selectors/get_internal_is_intialized' ; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs' ; import { Size } from '../../../../utils/dimensions' ; import { nullShapeViewModel , ShapeViewModel , Word , WordcloudViewModel } from '../../layout/types/viewmodel_types' ; import { geometries } from '../../state/selectors/geometries' ; function getFont ( d ) { return d . fontFamily ; } function getFontStyle ( d ) { return d . style ; } function getFontWeight ( d ) { return d . fontWeight ; } function getFontSize ( d ) { return d . size ; } function hashWithinRange ( str , max ) { str = JSON . stringify ( str ) ; let hash = 0 ; for ( const ch of str ) { hash = ( hash * 31 + ch . charCodeAt ( 0 ) ) % max ; } return Math . abs ( hash ) % max ; } function getRotation ( startAngle , endAngle , count , text ) { const angleRange = endAngle - startAngle ; const angleCount = count ?? 360 ; const interval = count - 1 ; const angleStep = interval === 0 ? 0 : angleRange / interval ; const index = hashWithinRange ( text , angleCount ) ; return index * angleStep + startAngle ; } function exponential ( minFontSize , maxFontSize , exponent , weight ) { return minFontSize + ( maxFontSize - minFontSize ) * weight ** exponent ; } function linear ( minFontSize , maxFontSize , _exponent , weight ) { return minFontSize + ( maxFontSize - minFontSize ) * weight ; } function squareRoot ( minFontSize , maxFontSize , _exponent , weight ) { return minFontSize + ( maxFontSize - minFontSize ) * Math . sqrt ( weight ) ; } function log ( minFontSize , maxFontSize , _exponent , weight ) { return minFontSize + ( maxFontSize - minFontSize ) * Math . log2 ( weight + 1 ) ; } const weightFnLookup = { linear , exponential , log , squareRoot } ; function layoutMaker ( { data , ... viewModel } , chartSize ) { const { height , width } = chartSize ; const words = data . map < Word > ( ( d ) => { const weightFn = weightFnLookup [ viewModel . weightFn ] ; return { datum : d , text : d . text , color : d . color , fontFamily : viewModel . fontFamily , style : viewModel . fontStyle , fontWeight : viewModel . fontWeight , size : weightFn ( viewModel . minFontSize , viewModel . maxFontSize , viewModel . exponent , d . weight ) , } ; } ) ; return d3TagCloud < Word > ( ) . random ( ( ) => 0.5 ) . size ( [ width , height ] ) . words ( words ) . spiral ( viewModel . spiral ?? 'archimedean' ) . padding ( viewModel . padding ?? 5 ) . rotate ( ( d ) => getRotation ( viewModel . startAngle , viewModel . endAngle , viewModel . angleCount , d . text ) ) . font ( getFont ) . fontStyle ( getFontStyle ) . fontWeight ( getFontWeight ) . fontSize ( getFontSize ) ; } const View = ( { words , size : { height , width } , actions : { onElementClick , onElementOver , onElementOut } , specId , } ) => { return ( < svg width = { width } height = { height } role = "presentation" > < g transform = { `translate(${ width / 2 }, ${ height / 2 })` } > { words . map ( ( d , i ) => { const elements = [ [ d . datum , { specId , key : specId } ] ] ; const actions = { ... ( onElementClick && { onClick : ( ) => { onElementClick ( elements ) ; } , } ) , ... ( onElementOver && { onMouseOver : ( ) => { onElementOver ( elements ) ; } , } ) , ... ( onElementOut && { onMouseOut : ( ) => { onElementOut ( ) ; } , } ) , } ; return ( < text key = { String ( i ) } style = { { fontSize : getFontSize ( d ) , fontStyle : getFontStyle ( d ) , fontFamily : getFont ( d ) , fontWeight : getFontWeight ( d ) , fill : d . color , } } textAnchor = "middle" transform = { `translate(${ d . x }, ${ d . y }) rotate(${ d . rotate })` } { ... actions } > { d . text } < /text>); } ) } < /g> < /svg>); } ; interface ReactiveChartStateProps { initialized : boolean ; geometries : ShapeViewModel ; chartSize : Size ; a11ySettings : A11ySettings ; onElementClick ? : SettingsSpec [ 'onElementClick' ] ; onElementOver ? : SettingsSpec [ 'onElementOver' ] ; onElementOut ? : SettingsSpec [ 'onElementOut' ] ; } interface ReactiveChartDispatchProps { onChartRendered : typeof onChartRendered ; } type Props = ReactiveChartStateProps & ReactiveChartDispatchProps ; class Component extends React . Component < Props > { static displayName = 'Wordcloud' ; componentDidMount ( ) { if ( this . props . initialized ) { this . props . onChartRendered ( ) ; } } componentDidUpdate ( ) { if ( this . props . initialized ) { this . props . onChartRendered ( ) ; } } render ( ) { const { initialized , chartSize , geometries : { wordcloudViewModel , specId } , a11ySettings , onElementClick , onElementOver , onElementOut , } = this . props ; if ( ! initialized || chartSize . width === 0 || chartSize . height === 0 ) { return null ; } const layout = layoutMaker ( wordcloudViewModel , chartSize ) ; let renderedWordObjects = [ ] ; layout . on ( 'end' , ( w ) => ( renderedWordObjects = w ) ) . start ( ) ; const wordCount = wordcloudViewModel . data . length ; const renderedWordCount = renderedWordObjects . length ; const notAllWordsFit = wordCount !== renderedWordCount ; if ( notAllWordsFit && wordcloudViewModel . outOfRoomCallback instanceof Function ) { wordcloudViewModel . outOfRoomCallback ( wordCount , renderedWordCount , renderedWordObjects . map ( ( word ) => word . text ) ) ; } return ( < figure aria - labelledby = { a11ySettings . labelId } aria - describedby = { a11ySettings . descriptionId } > < View words = { renderedWordObjects } size = { chartSize } actions = { { onElementClick , onElementOut , onElementOver } } specId = { specId } / > < ScreenReaderSummary /> < /figure>); } } const mapDispatchToProps = ( dispatch ) : ReactiveChartDispatchProps => bindActionCreators ( { onChartRendered , } , dispatch ) ; const DEFAULT_PROPS = { initialized : false , geometries : nullShapeViewModel ( ) , chartSize : { width : 0 , height : 0 , } , a11ySettings : DEFAULT_A11Y_SETTINGS , } ; const mapStateToProps = ( state ) : ReactiveChartStateProps => { if ( getInternalIsInitializedSelector ( state ) !== InitStatus . Initialized ) { return DEFAULT_PROPS ; } return { initialized : true , geometries : geometries ( state ) , chartSize : state . parentDimensions , a11ySettings : getA11ySettingsSelector ( state ) , onElementClick : getSettingsSpecSelector ( state ) . onElementClick , onElementOver : getSettingsSpecSelector ( state ) . onElementOver , onElementOut : getSettingsSpecSelector ( state ) . onElementOut , } ; } ; internal export const Wordcloud = connect ( mapStateToProps , mapDispatchToProps ) ( Component ) ;