diff --git a/plugins/tech-radar/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/tech-radar/src/components/ExampleComponent/ExampleComponent.tsx
deleted file mode 100644
index a01859993e..0000000000
--- a/plugins/tech-radar/src/components/ExampleComponent/ExampleComponent.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * 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 React, { FC } from 'react';
-import { Typography, Grid } from '@material-ui/core';
-import {
- InfoCard,
- Header,
- Page,
- pageTheme,
- Content,
- ContentHeader,
- HeaderLabel,
- SupportButton,
-} from '@backstage/core';
-import ExampleFetchComponent from '../ExampleFetchComponent';
-
-const ExampleComponent: FC<{}> = () => (
-
-
-
-
-
-
-
- A description of your plugin goes here.
-
-
-
-
-
- All content should be wrapped in a card like this.
-
-
-
-
-
-
-
-
-
-
-
-);
-
-export default ExampleComponent;
diff --git a/plugins/tech-radar/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/tech-radar/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx
deleted file mode 100644
index a198bfcc3f..0000000000
--- a/plugins/tech-radar/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * 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 React, { FC } from 'react';
-import { makeStyles } from '@material-ui/core/styles';
-import Table from '@material-ui/core/Table';
-import TableBody from '@material-ui/core/TableBody';
-import TableCell from '@material-ui/core/TableCell';
-import TableContainer from '@material-ui/core/TableContainer';
-import TableHead from '@material-ui/core/TableHead';
-import TableRow from '@material-ui/core/TableRow';
-import Alert from '@material-ui/lab/Alert';
-import { useAsync } from 'react-use';
-import { Progress } from '@backstage/core';
-
-const useStyles = makeStyles({
- table: {
- minWidth: 650,
- },
- avatar: {
- height: 32,
- width: 32,
- borderRadius: '50%',
- },
-});
-
-type User = {
- gender: string; // "male"
- name: {
- title: string; // "Mr",
- first: string; // "Duane",
- last: string; // "Reed"
- };
- location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…}
- email: string; // "duane.reed@example.com"
- login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…}
- dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37}
- registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14}
- phone: string; // "07-2154-5651"
- cell: string; // "0405-592-879"
- id: {
- name: string; // "TFN",
- value: string; // "796260432"
- };
- picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…}
- nat: string; // "AU"
-};
-
-type DenseTableProps = {
- users: User[];
-};
-
-export const DenseTable: FC = ({ users }) => {
- const classes = useStyles();
-
- return (
-
-
+ );
+ }
+
+ static _getSegment(segmented, quadrant, ring, ringOffset = 0) {
+ return (segmented[quadrant.idx] || {})[ring.idx + ringOffset] || [];
+ }
+
+ render() {
+ const {
+ quadrants,
+ rings,
+ entries,
+ onEntryMouseEnter,
+ onEntryMouseLeave,
+ onEntryClick,
+ } = this.props;
+
+ const segments = {};
+
+ for (const entry of entries) {
+ const qidx = entry.quadrant.idx;
+ const ridx = entry.ring.idx;
+ const quadrantData = segments[qidx] || (segments[qidx] = {});
+ const ringData = quadrantData[ridx] || (quadrantData[ridx] = []);
+ ringData.push(entry);
+ }
+
+ return (
+
+ {quadrants.map(quadrant =>
+ RadarLegend._renderQuadrant(
+ segments,
+ quadrant,
+ rings,
+ onEntryMouseEnter,
+ onEntryMouseLeave,
+ onEntryClick,
+ ),
+ )}
+
+ );
+ }
+}
+
+RadarLegend.propTypes = {
+ quadrants: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.QUADRANT))
+ .isRequired,
+ rings: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.RING)).isRequired,
+ entries: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.ENTRY)).isRequired,
+ onEntryMouseEnter: PropTypes.func,
+ onEntryMouseLeave: PropTypes.func,
+ onEntryClick: PropTypes.func,
+};
diff --git a/plugins/tech-radar/src/components/RadarLegend/index.ts b/plugins/tech-radar/src/components/RadarLegend/index.ts
new file mode 100644
index 0000000000..7214fc7929
--- /dev/null
+++ b/plugins/tech-radar/src/components/RadarLegend/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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.
+ */
+
+export { default } from './RadarLegend';
diff --git a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.js b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.js
new file mode 100644
index 0000000000..bf31c3e6fa
--- /dev/null
+++ b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.js
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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 React from 'react';
+import PropTypes from 'prop-types';
+import * as CommonPropTypes from '../../utils/prop-types';
+
+import RadarGrid from '../RadarGrid';
+import RadarEntry from '../RadarEntry';
+import RadarBubble from '../RadarBubble';
+import RadarFooter from '../RadarFooter';
+import RadarLegend from '../RadarLegend';
+
+// A component that draws the radar circle.
+export default class RadarPlot extends React.PureComponent {
+ render() {
+ const {
+ width,
+ height,
+ radius,
+ quadrants,
+ rings,
+ entries,
+ activeEntry,
+ onEntryMouseEnter,
+ onEntryMouseLeave,
+ onEntryClick,
+ } = this.props;
+
+ return (
+
+ onEntryMouseEnter(entry))
+ }
+ onEntryMouseLeave={
+ onEntryMouseLeave && (entry => onEntryMouseLeave(entry))
+ }
+ onEntryClick={onEntryClick && (entry => onEntryClick(entry))}
+ />
+
+
+
+ {entries.map(entry => (
+ onEntryMouseEnter(entry))
+ }
+ onMouseLeave={
+ onEntryMouseLeave && (() => onEntryMouseLeave(entry))
+ }
+ onClick={onEntryClick && (() => onEntryClick(entry))}
+ />
+ ))}
+
+
+
+ );
+ }
+}
+
+RadarPlot.propTypes = {
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+ radius: PropTypes.number.isRequired,
+ rings: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.RING)).isRequired,
+ quadrants: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.QUADRANT))
+ .isRequired,
+ entries: PropTypes.arrayOf(PropTypes.shape(CommonPropTypes.ENTRY)).isRequired,
+ activeEntry: PropTypes.object,
+ onEntryMouseEnter: PropTypes.func,
+ onEntryMouseLeave: PropTypes.func,
+ onEntryClick: PropTypes.func,
+};
diff --git a/plugins/tech-radar/src/components/RadarPlot/index.ts b/plugins/tech-radar/src/components/RadarPlot/index.ts
new file mode 100644
index 0000000000..61ea1900d4
--- /dev/null
+++ b/plugins/tech-radar/src/components/RadarPlot/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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.
+ */
+
+export { default } from './RadarPlot';
diff --git a/plugins/tech-radar/src/utils/prop-types.js b/plugins/tech-radar/src/utils/prop-types.js
new file mode 100644
index 0000000000..d5491435b6
--- /dev/null
+++ b/plugins/tech-radar/src/utils/prop-types.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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 PropTypes from 'prop-types';
+
+// Parameters for a ring; its index in an array determines how close to the center this ring is.
+export const RING = {
+ id: PropTypes.string.isRequired,
+ idx: PropTypes.number,
+ name: PropTypes.string.isRequired,
+ color: PropTypes.string.isRequired,
+};
+
+// Parameters for a quadrant (there should be exactly 4 of course)
+export const QUADRANT = {
+ id: PropTypes.string.isRequired,
+ idx: PropTypes.number,
+ name: PropTypes.string.isRequired,
+};
+
+export const ENTRY = {
+ id: PropTypes.string.isRequired,
+ idx: PropTypes.number,
+ // The quadrant where this entry belongs
+ quadrant: PropTypes.shape(QUADRANT).isRequired,
+ // The ring where this entry belongs
+ ring: PropTypes.shape(RING).isRequired,
+ // The label that's shown in the legend and on hover
+ title: PropTypes.string.isRequired,
+ // An URL to a longer description as to why this entry is where it is
+ url: PropTypes.string,
+ // How this entry has recently moved; -1 for "down", +1 for "up", 0 for not moved
+ moved: PropTypes.number,
+};
+
+// The same as ENTRY except quadrant/ring are declared by their string ID instead of being the actual objects
+export const DECLARED_ENTRY = Object.assign({}, ENTRY, {
+ quadrant: PropTypes.string.isRequired,
+ ring: PropTypes.string.isRequired,
+});
diff --git a/plugins/tech-radar/src/utils/segment.js b/plugins/tech-radar/src/utils/segment.js
new file mode 100644
index 0000000000..a4cb921c48
--- /dev/null
+++ b/plugins/tech-radar/src/utils/segment.js
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * 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.
+ */
+
+export default class Segment {
+ constructor(quadrant, ring, radius, nextSeed) {
+ this.nextSeed = nextSeed;
+ this.polarMin = {
+ t: quadrant.radialMin,
+ r: ring.innerRadius,
+ };
+ this.polarMax = {
+ t: quadrant.radialMax,
+ r: ring.outerRadius,
+ };
+ this.cartesianMin = {
+ x: 15 * quadrant.offsetX,
+ y: 15 * quadrant.offsetY,
+ };
+ this.cartesianMax = {
+ x: radius * quadrant.offsetX,
+ y: radius * quadrant.offsetY,
+ };
+ }
+
+ clipx(d) {
+ const c = boundedBox(d, this.cartesianMin, this.cartesianMax);
+ const p = boundedRing(polar(c), this.polarMin.r + 15, this.polarMax.r - 15);
+ d.x = cartesian(p).x;
+ return d.x;
+ }
+
+ clipy(d) {
+ const c = boundedBox(d, this.cartesianMin, this.cartesianMax);
+ const p = boundedRing(polar(c), this.polarMin.r + 15, this.polarMax.r - 15);
+ d.y = cartesian(p).y;
+ return d.y;
+ }
+
+ random() {
+ return cartesian({
+ t: this._randomBetween(this.polarMin.t, this.polarMax.t),
+ r: this._normalBetween(this.polarMin.r, this.polarMax.r),
+ });
+ }
+
+ // custom random number generator, to make random sequence reproducible
+ // source: https://stackoverflow.com/questions/521295
+ _random() {
+ const x = Math.sin(this.nextSeed()) * 10000;
+ return x - Math.floor(x);
+ }
+
+ _randomBetween(min, max) {
+ return min + this._random() * (max - min);
+ }
+
+ _normalBetween(min, max) {
+ return min + (this._random() + this._random()) * 0.5 * (max - min);
+ }
+}
+
+function polar({ x, y }) {
+ return {
+ t: Math.atan2(y, x),
+ r: Math.sqrt(x * x + y * y),
+ };
+}
+
+function cartesian({ r, t }) {
+ return {
+ x: r * Math.cos(t),
+ y: r * Math.sin(t),
+ };
+}
+
+function boundedInterval(value, min, max) {
+ const low = Math.min(min, max);
+ const high = Math.max(min, max);
+ return Math.min(Math.max(value, low), high);
+}
+
+function boundedRing(polarValue, rMin, rMax) {
+ return {
+ t: polarValue.t,
+ r: boundedInterval(polarValue.r, rMin, rMax),
+ };
+}
+
+function boundedBox(point, min, max) {
+ return {
+ x: boundedInterval(point.x, min.x, max.x),
+ y: boundedInterval(point.y, min.y, max.y),
+ };
+}