diff --git a/plugins/tech-radar/src/components/Radar/Radar.tsx b/plugins/tech-radar/src/components/Radar/Radar.tsx index e6ae6d385d..b2d4ed22e2 100644 --- a/plugins/tech-radar/src/components/Radar/Radar.tsx +++ b/plugins/tech-radar/src/components/Radar/Radar.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import React, { useState, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import type { Entry, Quadrant, Ring } from '../../utils/types'; import RadarPlot from '../RadarPlot'; -import type { Ring, Quadrant, Entry } from '../../utils/types'; -import { adjustQuadrants, adjustRings, adjustEntries } from './utils'; +import { adjustEntries, adjustQuadrants, adjustRings } from './utils'; export type Props = { width: number; @@ -28,17 +28,27 @@ export type Props = { svgProps?: object; }; -const Radar = (props: Props): JSX.Element => { - const { width, height, quadrants, rings, entries } = props; +const Radar = ({ width, height, quadrants, rings, entries, ...props }: Props): JSX.Element => { + const [adjustedQuadrants, setAdjustedQuadrants] = useState>(quadrants); + const [adjustedRings, setAdjustedRings] = useState>(rings); + const [adjustedEntries, setAdjustedEntries] = useState>(entries); + const radius = Math.min(width, height) / 2; const [activeEntry, setActiveEntry] = useState(); const node = useRef(null); - // TODO(dflemstr): most of this can be heavily memoized if performance becomes a problem - adjustQuadrants(quadrants, radius, width, height); - adjustRings(rings, radius); - adjustEntries(entries, quadrants, rings, radius, activeEntry); + useEffect(() => { + setAdjustedQuadrants(adjustQuadrants(quadrants, radius, width, height)); + }, [quadrants, radius, width, height]) + + useEffect(() => { + setAdjustedRings(adjustRings(rings, radius)); + }, [radius, rings]) + + useEffect(() => { + setAdjustedEntries(adjustEntries(entries, adjustedQuadrants, adjustedRings, radius, activeEntry)); + }, [entries, adjustedQuadrants, adjustedRings, radius, activeEntry]) return ( @@ -46,9 +56,9 @@ const Radar = (props: Props): JSX.Element => { width={width} height={height} radius={radius} - entries={entries} - quadrants={quadrants} - rings={rings} + entries={adjustedEntries} + quadrants={adjustedQuadrants} + rings={adjustedRings} activeEntry={activeEntry} onEntryMouseEnter={entry => setActiveEntry(entry)} onEntryMouseLeave={() => setActiveEntry(undefined)} diff --git a/plugins/tech-radar/src/components/Radar/utils.ts b/plugins/tech-radar/src/components/Radar/utils.ts index e41b0a27ab..19db2a0cd1 100644 --- a/plugins/tech-radar/src/components/Radar/utils.ts +++ b/plugins/tech-radar/src/components/Radar/utils.ts @@ -17,7 +17,7 @@ import color from 'color'; import { forceCollide, forceSimulation } from 'd3-force'; import Segment from '../../utils/segment'; -import type { Ring, Quadrant, Entry } from '../../utils/types'; +import type { Entry, Quadrant, Ring } from '../../utils/types'; export const adjustQuadrants = ( quadrants: Quadrant[], @@ -81,30 +81,33 @@ export const adjustQuadrants = ( }, ]; - quadrants.forEach((quadrant, index) => { + return quadrants.slice().map((quadrant, index) => { const legendParam = legendParams[index % 4]; - quadrant.index = index; - quadrant.radialMin = (index * Math.PI) / 2; - quadrant.radialMax = ((index + 1) * Math.PI) / 2; - quadrant.offsetX = index % 4 === 0 || index % 4 === 3 ? 1 : -1; - quadrant.offsetY = index % 4 === 0 || index % 4 === 1 ? 1 : -1; - quadrant.legendX = legendParam.x; - quadrant.legendY = legendParam.y; - quadrant.legendWidth = legendParam.width; - quadrant.legendHeight = legendParam.height; + return ({ + ...quadrant, + index, + radialMin: (index * Math.PI) / 2, + radialMax: ((index + 1) * Math.PI) / 2, + offsetX: index % 4 === 0 || index % 4 === 3 ? 1 : -1, + offsetY: index % 4 === 0 || index % 4 === 1 ? 1 : -1, + legendX: legendParam.x, + legendY: legendParam.y, + legendWidth: legendParam.width, + legendHeight: legendParam.height + }) }); }; export const adjustEntries = ( - entries: Entry[], + _entries: Entry[], quadrants: Quadrant[], rings: Ring[], radius: number, activeEntry?: Entry, ) => { let seed = 42; - entries.forEach((entry, index) => { + const entries = _entries.map((entry, index) => { const quadrant = quadrants.find(q => { const match = typeof entry.quadrant === 'object' ? entry.quadrant.id : entry.quadrant; @@ -123,18 +126,21 @@ export const adjustEntries = ( if (!ring) { throw new Error(`Unknown ring ${entry.ring} for entry ${entry.id}!`); } + const segment = new Segment(quadrant, ring, radius, () => seed++) + const point = segment?.random(); - entry.index = index; - entry.quadrant = quadrant; - entry.ring = ring; - entry.segment = new Segment(quadrant, ring, radius, () => seed++); - const point = entry.segment.random(); - entry.x = point.x; - entry.y = point.y; - entry.active = activeEntry ? entry.id === activeEntry.id : false; - entry.color = entry.active - ? entry.ring.color - : color(entry.ring.color).desaturate(0.5).lighten(0.1).string(); + return ({ + ...entry, + index: index, + quadrant: quadrant, + ring: ring, + segment, + x: point.x, + y: point.y, + color: activeEntry && entry.id === activeEntry?.id + ? entry.ring.color + : color(entry.ring.color).desaturate(0.5).lighten(0.1).string(), + }) }); const simulation = forceSimulation() @@ -145,9 +151,9 @@ export const adjustEntries = ( for ( let i = 0, - n = Math.ceil( - Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()), - ); + n = Math.ceil( + Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()), + ); i < n; ++i ) { @@ -160,13 +166,13 @@ export const adjustEntries = ( } } } + + return entries }; -export const adjustRings = (rings: Ring[], radius: number) => { - rings.forEach((ring, index) => { - ring.index = index; - ring.outerRadius = ((index + 2) / (rings.length + 1)) * radius; - ring.innerRadius = - ((index === 0 ? 0 : index + 1) / (rings.length + 1)) * radius; - }); -}; +export const adjustRings = (rings: Ring[], radius: number) => rings.slice().map((ring, index) => ({ + ...ring, + index, + outerRadius: ((index + 2) / (rings.length + 1)) * radius, + innerRadius: ((index === 0 ? 0 : index + 1) / (rings.length + 1)) * radius +})) diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.test.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.test.tsx index 1f74efe650..e0b5f09a63 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.test.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.test.tsx @@ -14,15 +14,16 @@ * limitations under the License. */ -import React from 'react'; -import { render } from '@testing-library/react'; -import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import { render } from '@testing-library/react'; +import React from 'react'; import GetBBoxPolyfill from '../../utils/polyfills/getBBox'; -import RadarLegend, { Props } from './RadarLegend'; +import RadarLegend from './RadarLegend'; +import { RadarLegendProps } from './types'; -const minProps: Props = { +const minProps: RadarLegendProps = { quadrants: [{ id: 'languages', name: 'Languages' }], rings: [{ id: 'use', name: 'USE', color: '#93c47d' }], entries: [ diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx index a398113b98..9a4b861b56 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2022 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,21 +15,10 @@ */ import { makeStyles, Theme } from '@material-ui/core'; import React from 'react'; -import { WithLink } from '../../utils/components'; -import type { Entry, Quadrant, Ring } from '../../utils/types'; -import { RadarDescription } from '../RadarDescription'; +import { RadarLegendQuadrant } from './RadarLegendQuadrant'; +import { RadarLegendProps } from './types'; +import { setupSegments } from './utils'; -type Segments = { - [k: number]: { [k: number]: Entry[] }; -}; - -export type Props = { - quadrants: Quadrant[]; - rings: Ring[]; - entries: Entry[]; - onEntryMouseEnter?: (entry: Entry) => void; - onEntryMouseLeave?: (entry: Entry) => void; -}; const useStyles = makeStyles(theme => ({ quadrant: { @@ -85,209 +74,26 @@ const useStyles = makeStyles(theme => ({ }, })); -const RadarLegend = (props: Props): JSX.Element => { +const RadarLegend = ({ + quadrants, + rings, + entries, + onEntryMouseEnter, + onEntryMouseLeave, + ...props +}: RadarLegendProps +): JSX.Element => { const classes = useStyles(props); - const getSegment = ( - segmented: Segments, - quadrant: Quadrant, - ring: Ring, - ringOffset = 0, - ) => { - const quadrantIndex = quadrant.index; - const ringIndex = ring.index; - const segmentedData = - quadrantIndex === undefined ? {} : segmented[quadrantIndex] || {}; - return ringIndex === undefined - ? [] - : segmentedData[ringIndex + ringOffset] || []; - }; - - type RadarLegendRingProps = { - ring: Ring; - entries: Entry[]; - onEntryMouseEnter?: Props['onEntryMouseEnter']; - onEntryMouseLeave?: Props['onEntryMouseEnter']; - }; - - type RadarLegendLinkProps = { - url?: string; - description?: string; - title?: string; - }; - - const RadarLegendLink = ({ - url, - description, - title, - }: RadarLegendLinkProps) => { - const [open, setOpen] = React.useState(false); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - const toggle = () => { - setOpen(!open); - }; - - if (description) { - return ( - <> - - {title} - - {open && ( - - )} - - ); - } - return ( - - {title} - - ); - }; - - const RadarLegendRing = ({ - ring, - entries, - onEntryMouseEnter, - onEntryMouseLeave, - }: RadarLegendRingProps) => { - return ( -
-

{ring.name}

- {entries.length === 0 ? ( -

(empty)

- ) : ( -
    - {entries.map(entry => ( -
  1. onEntryMouseEnter(entry)) - } - onMouseLeave={ - onEntryMouseLeave && (() => onEntryMouseLeave(entry)) - } - > - -
  2. - ))} -
- )} -
- ); - }; - - type RadarLegendQuadrantProps = { - segments: Segments; - quadrant: Quadrant; - rings: Ring[]; - onEntryMouseEnter: Props['onEntryMouseEnter']; - onEntryMouseLeave: Props['onEntryMouseLeave']; - }; - - const RadarLegendQuadrant = ({ - segments, - quadrant, - rings, - onEntryMouseEnter, - onEntryMouseLeave, - }: RadarLegendQuadrantProps) => { - return ( - -
-

{quadrant.name}

-
- {rings.map(ring => ( - - ))} -
-
-
- ); - }; - - const setupSegments = (entries: Entry[]) => { - const segments: Segments = {}; - - for (const entry of entries) { - const quadrantIndex = entry.quadrant.index; - const ringIndex = entry.ring.index; - let quadrantData: { [k: number]: Entry[] } = {}; - if (quadrantIndex !== undefined) { - if (segments[quadrantIndex] === undefined) { - segments[quadrantIndex] = {}; - } - - quadrantData = segments[quadrantIndex]; - } - - let ringData = []; - if (ringIndex !== undefined) { - if (quadrantData[ringIndex] === undefined) { - quadrantData[ringIndex] = []; - } - - ringData = quadrantData[ringIndex]; - } - - ringData.push(entry); - } - - return segments; - }; - - const { quadrants, rings, entries, onEntryMouseEnter, onEntryMouseLeave } = - props; - - const segments: Segments = setupSegments(entries); - return ( {quadrants.map(quadrant => ( diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx new file mode 100644 index 0000000000..f6626aa0d2 --- /dev/null +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClassNameMap } from "@material-ui/core/styles/withStyles"; +import React from "react"; +import { WithLink } from "../../utils/components"; +import { RadarDescription } from "../RadarDescription"; + +type RadarLegendLinkProps = { + url?: string; + description?: string; + title?: string; + classes: ClassNameMap +}; + +export const RadarLegendLink = ({ + url, + description, + title, + classes +}: RadarLegendLinkProps) => { + const [open, setOpen] = React.useState(false); + + const handleClickOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const toggle = () => { + setOpen(!open); + }; + + if (description) { + return ( + <> + + {title} + + {open && ( + + )} + + ); + } + return ( + + {title} + + ); +}; diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendQuadrant.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendQuadrant.tsx new file mode 100644 index 0000000000..af79230dfc --- /dev/null +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendQuadrant.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ClassNameMap } from "@material-ui/core/styles/withStyles"; +import React from "react"; +import { Quadrant, Ring } from "../../utils/types"; +import { RadarLegendRing } from "./RadarLegendRing"; +import { RadarLegendProps, Segments } from "./types"; +import { getSegment } from "./utils"; + + + +type RadarLegendQuadrantProps = { + segments: Segments; + quadrant: Quadrant; + rings: Ring[]; + classes: ClassNameMap, + onEntryMouseEnter: RadarLegendProps['onEntryMouseEnter']; + onEntryMouseLeave: RadarLegendProps['onEntryMouseLeave']; +}; + +export const RadarLegendQuadrant = ({ + segments, + quadrant, + rings, + classes, + onEntryMouseEnter, + onEntryMouseLeave, +}: RadarLegendQuadrantProps) => { + return ( + +
+

{quadrant.name}

+
+ {rings.map(ring => ( + + ))} +
+
+
+ ); +}; \ No newline at end of file diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx new file mode 100644 index 0000000000..c9070ba0dd --- /dev/null +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClassNameMap } from "@material-ui/core/styles/withStyles"; +import React from "react"; +import { Entry, Ring } from "../../utils/types"; +import { RadarLegendLink } from "./RadarLegendLink"; +import { RadarLegendProps } from "./types"; + +type RadarLegendRingProps = { + ring: Ring; + entries: Entry[]; + classes: ClassNameMap; + onEntryMouseEnter?: RadarLegendProps['onEntryMouseEnter']; + onEntryMouseLeave?: RadarLegendProps['onEntryMouseEnter']; +}; + +export const RadarLegendRing = ({ + ring, + entries, + classes, + onEntryMouseEnter, + onEntryMouseLeave, +}: RadarLegendRingProps) => { + return ( +
+

{ring.name}

+ {entries.length === 0 ? ( +

(empty)

+ ) : ( +
    + {entries.map(entry => ( +
  1. onEntryMouseEnter(entry)) + } + onMouseLeave={ + onEntryMouseLeave && (() => onEntryMouseLeave(entry)) + } + > + +
  2. + ))} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/plugins/tech-radar/src/components/RadarLegend/types.ts b/plugins/tech-radar/src/components/RadarLegend/types.ts new file mode 100644 index 0000000000..2165646afc --- /dev/null +++ b/plugins/tech-radar/src/components/RadarLegend/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entry, Quadrant, Ring } from "../../utils/types"; + +export type Segments = { + [k: number]: { [k: number]: Entry[] }; +}; + +export type RadarLegendProps = { + quadrants: Quadrant[]; + rings: Ring[]; + entries: Entry[]; + onEntryMouseEnter?: (entry: Entry) => void; + onEntryMouseLeave?: (entry: Entry) => void; +}; \ No newline at end of file diff --git a/plugins/tech-radar/src/components/RadarLegend/utils.ts b/plugins/tech-radar/src/components/RadarLegend/utils.ts new file mode 100644 index 0000000000..036388ece9 --- /dev/null +++ b/plugins/tech-radar/src/components/RadarLegend/utils.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entry, Quadrant, Ring } from "../../utils/types"; +import { Segments } from "./types"; + +export const setupSegments = (entries: Entry[]) => { + const segments: Segments = {}; + + for (const entry of entries) { + const quadrantIndex = entry.quadrant.index; + const ringIndex = entry.ring.index; + let quadrantData: { [k: number]: Entry[] } = {}; + if (quadrantIndex !== undefined) { + if (segments[quadrantIndex] === undefined) { + segments[quadrantIndex] = {}; + } + + quadrantData = segments[quadrantIndex]; + } + + let ringData = []; + if (ringIndex !== undefined) { + if (quadrantData[ringIndex] === undefined) { + quadrantData[ringIndex] = []; + } + + ringData = quadrantData[ringIndex]; + } + + ringData.push(entry); + } + + return segments; +}; + +export const getSegment = ( + segmented: Segments, + quadrant: Quadrant, + ring: Ring, + ringOffset = 0, +) => { + const quadrantIndex = quadrant.index; + const ringIndex = ring.index; + const segmentedData = + quadrantIndex === undefined ? {} : segmented[quadrantIndex] || {}; + return ringIndex === undefined + ? [] + : segmentedData[ringIndex + ringOffset] || []; +}; \ No newline at end of file