Full Radar grid with chart
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// TODO remove faux-dom and start using the React hook approach
|
||||
import ReactFauxDOM from 'react-faux-dom';
|
||||
import * as d3 from 'd3';
|
||||
import * as d3 from "d3";
|
||||
|
||||
export const LeftAxis = ({ scale }) => {
|
||||
export const YAxis = ({ scale }) => {
|
||||
const el = ReactFauxDOM.createElement('g');
|
||||
|
||||
const axisGenerator = d3.axisLeft(scale).ticks(6);
|
||||
@@ -14,7 +15,7 @@ export const LeftAxis = ({ scale }) => {
|
||||
return el.toReact();
|
||||
};
|
||||
|
||||
export const BottomAxis = ({ scale }) => {
|
||||
export const XAxis = ({ scale }) => {
|
||||
const el = ReactFauxDOM.createElement('g');
|
||||
|
||||
const axisGenerator = d3.axisBottom(scale).ticks(6);
|
||||
|
||||
@@ -1,66 +1,112 @@
|
||||
import ReactFauxDOM from 'react-faux-dom';
|
||||
import * as d3 from "d3";
|
||||
import { quadrantsMap, ringsMap } from '../../config';
|
||||
import React from 'react';
|
||||
import { quadrantsMap, ringsMap, chartConfig, blipFlags } from '../..//config';
|
||||
import { NewBlip, ChangedBlip, DefaultBlip } from './BlipShapes';
|
||||
|
||||
/*
|
||||
See https://medium.com/create-code/build-a-radar-diagram-with-d3-js-9db6458a9248
|
||||
for a good explanation of formulas used to calculate various things in this component
|
||||
*/
|
||||
|
||||
const generateCoordinates = (enrichedBlip, xScale, yScale) => {
|
||||
const pi = Math.PI;
|
||||
// radian between 5 and 85
|
||||
const randomDegree = ((Math.random() * 80 + 5) * pi) / 180;
|
||||
const radius = enrichedBlip.ringPosition - 0.2;
|
||||
const r = Math.random() * 0.6 + (radius - 0.6);
|
||||
// multiples of PI/2
|
||||
const shift = pi * [1, 4, 3, 2][enrichedBlip.quadrantPosition - 1] / 2;
|
||||
const pi = Math.PI,
|
||||
ringRadius = chartConfig.ringsAttributes[enrichedBlip.ringPosition - 1].radius,
|
||||
previousRingRadius = enrichedBlip.ringPosition == 1 ? 0 : chartConfig.ringsAttributes[enrichedBlip.ringPosition - 2].radius,
|
||||
ringPadding = 0.6;
|
||||
|
||||
// radian between 0 and 90 degrees
|
||||
const randomDegree = ((Math.random() * 90) * pi) / 180;
|
||||
// random distance from the centre of the radar, but within given ring. Also, with some "padding" so the points don't touch ring borders.
|
||||
const radius = randomBetween(previousRingRadius + ringPadding, ringRadius - ringPadding);
|
||||
/*
|
||||
Multiples of PI/2. To apply the calculated position to the specific quadrant.
|
||||
Order here is counter-clockwise and starts at the top left, so we need to "invert" quadrant positions
|
||||
*/
|
||||
const shift = pi * [4, 3, 2, 1][enrichedBlip.quadrantPosition - 1] / 2;
|
||||
|
||||
return {
|
||||
x: xScale(Math.cos(randomDegree + shift) * r),
|
||||
y: yScale(Math.sin(randomDegree + shift) * r)
|
||||
x: xScale(Math.cos(randomDegree + shift) * radius),
|
||||
y: yScale(Math.sin(randomDegree + shift) * radius)
|
||||
};
|
||||
};
|
||||
|
||||
const distanceBetweenPoints = (point1, point2) => {
|
||||
const randomBetween = (min, max) => {
|
||||
return Math.random() * (max - min) + min;
|
||||
};
|
||||
|
||||
const distanceBetween = (point1, point2) => {
|
||||
const a = point2.x - point1.x;
|
||||
const b = point2.y - point1.y;
|
||||
return Math.sqrt((a * a) + (b * b));
|
||||
};
|
||||
};
|
||||
|
||||
export default function BlipPoints({blips, xScale, yScale, navigate}) {
|
||||
|
||||
export default function BlipPoints({blips, xScale, yScale}) {
|
||||
const enrichedBlips = blips.reduce((list, blip) => {
|
||||
if (!blip.ring || !blip.quadrant) {
|
||||
// skip the blip if it doesn't have a ring or quadrant assigned
|
||||
return list;
|
||||
}
|
||||
blip.ringPosition = ringsMap[blip.ring].position;
|
||||
blip.quadrantPosition = quadrantsMap[blip.quadrant].position;
|
||||
blip.colour = quadrantsMap[blip.quadrant].colour;
|
||||
let enrichedBlip = { ...blip,
|
||||
ringPosition: ringsMap[blip.ring].position,
|
||||
quadrantPosition: quadrantsMap[blip.quadrant].position,
|
||||
colour: quadrantsMap[blip.quadrant].colour,
|
||||
txtColour: quadrantsMap[blip.quadrant].txtColour
|
||||
};
|
||||
|
||||
let point;
|
||||
let counter = 1;
|
||||
do {
|
||||
point = generateCoordinates(blip, xScale, yScale);
|
||||
point = generateCoordinates(enrichedBlip, xScale, yScale);
|
||||
counter++;
|
||||
// generate position of the new blip until it has a satisfactory distance to every other blip
|
||||
// this feels pretty inefficient, but good enough for now
|
||||
} while (list.some(item => distanceBetweenPoints(point, item) < 8) || counter > 100);
|
||||
/*
|
||||
Generate position of the new blip until it has a satisfactory distance to every other blip (so that they don't touch each other)
|
||||
and quadrant borders (so that they don't overlap quadrants)
|
||||
This feels pretty inefficient, but good enough for now.
|
||||
*/
|
||||
} while (counter < 100
|
||||
&& (Math.abs(point.x - xScale(0)) < 15
|
||||
|| Math.abs(point.y - yScale(0)) < 15
|
||||
|| list.some(item => distanceBetween(point, item) < chartConfig.blipSize + chartConfig.blipSize / 2)
|
||||
));
|
||||
|
||||
blip.x = point.x;
|
||||
blip.y = point.y;
|
||||
enrichedBlip.x = point.x;
|
||||
enrichedBlip.y = point.y;
|
||||
|
||||
list.push(blip);
|
||||
list.push(enrichedBlip);
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
const el = ReactFauxDOM.createElement('g');
|
||||
const handleClick = (pageName, event) => {
|
||||
event.preventDefault();
|
||||
navigate(pageName);
|
||||
}
|
||||
|
||||
d3.select(el)
|
||||
.attr('class', 'circles')
|
||||
.selectAll('circle')
|
||||
.data(enrichedBlips)
|
||||
.enter().append('circle')
|
||||
.attr('fill', blip => blip.colour)
|
||||
.attr('r', 3)
|
||||
.attr('data-value', blip => blip.title)
|
||||
.attr('cx', blip => blip.x)
|
||||
.attr('cy', blip => blip.y)
|
||||
const renderBlip = (blip, index) => {
|
||||
const props = {
|
||||
blip,
|
||||
className: 'blip',
|
||||
fill: blip.colour,
|
||||
'data-background-color': blip.colour,
|
||||
'data-text-color': blip.txtColour,
|
||||
'data-tip': blip.title,
|
||||
onClick: handleClick.bind(this, `${blip.quadrant}/${blip.name}`),
|
||||
key: index
|
||||
}
|
||||
switch (blip.flag) {
|
||||
case blipFlags.new.name:
|
||||
return <NewBlip {...props} />;
|
||||
case blipFlags.changed.name:
|
||||
return <ChangedBlip {...props} />;
|
||||
default:
|
||||
return <DefaultBlip {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
return el.toReact();
|
||||
}
|
||||
return (
|
||||
<g className="blips">
|
||||
{enrichedBlips.map((blip, index) => (
|
||||
renderBlip(blip, index)
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
48
src/components/Chart/BlipShapes.tsx
Normal file
48
src/components/Chart/BlipShapes.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { chartConfig } from '../../config';
|
||||
|
||||
export const ChangedBlip = ({blip, ...props}) => {
|
||||
const centeredX = blip.x - chartConfig.blipSize/2,
|
||||
centeredY = blip.y - chartConfig.blipSize/2;
|
||||
|
||||
return (
|
||||
<rect
|
||||
transform={`rotate(-45 ${centeredX} ${centeredY})`}
|
||||
x={centeredX}
|
||||
y={centeredY}
|
||||
width={chartConfig.blipSize}
|
||||
height={chartConfig.blipSize}
|
||||
rx="3"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewBlip = ({blip, ...props}) => {
|
||||
const centeredX = blip.x - chartConfig.blipSize/2,
|
||||
centeredY = blip.y - chartConfig.blipSize/2;
|
||||
|
||||
/*
|
||||
The below is a predefined path of a triangle with rounded corners.
|
||||
I didn't find any more human friendly way of doing this as all examples I found have tons of lines of code
|
||||
e.g. https://observablehq.com/@perlmonger42/interactive-rounded-corners-on-svg-polygons-using-d3-js
|
||||
*/
|
||||
return (
|
||||
<path
|
||||
transform={`translate(${centeredX}, ${centeredY})`}
|
||||
d="M.247 10.212l5.02-8.697a2 2 0 013.465 0l5.021 8.697a2 2 0 01-1.732 3H1.98a2 2 0 01-1.732-3z"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultBlip = ({blip, ...props}) => {
|
||||
return (
|
||||
<circle
|
||||
r={chartConfig.blipSize / 2}
|
||||
cx={blip.x}
|
||||
cy={blip.y}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,45 +2,34 @@ import React from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import { chartConfig } from '../../config';
|
||||
|
||||
const size = chartConfig.canvasSize / 2;
|
||||
const arcPath = (quadrantPosition, ringPosition, xScale) => {
|
||||
const startAngle = (quadrantPosition - 1) * Math.PI / 2,
|
||||
endAngle = quadrantPosition * Math.PI / 2,
|
||||
arcAttrs = chartConfig.ringsAttributes[ringPosition - 1],
|
||||
ringRadiusPx = xScale(arcAttrs.radius) - xScale(0),
|
||||
arc = d3.arc();
|
||||
|
||||
const arcPath = (quadrantPosition, ringPosition) => {
|
||||
// order from the centre outwards
|
||||
const arcAttributes = [
|
||||
{radius: size / 4, width: 6},
|
||||
{radius: size / 2, width: 4},
|
||||
{radius: (size / 4 * 3), width: 2},
|
||||
{radius: size, width: 2}
|
||||
]
|
||||
const startAngle = quadrantPosition == 1 ?
|
||||
3 * Math.PI / 2
|
||||
: (quadrantPosition - 2) * Math.PI / 2
|
||||
const endAngle = quadrantPosition == 1 ?
|
||||
4 * Math.PI / 2
|
||||
: (quadrantPosition -1) * Math.PI / 2
|
||||
const arcAttrs = arcAttributes[ringPosition - 1];
|
||||
|
||||
const arc = d3.arc();
|
||||
return arc({
|
||||
innerRadius: arcAttrs.radius + (arcAttrs.width / 2),
|
||||
outerRadius: arcAttrs.radius - (arcAttrs.width / 2),
|
||||
innerRadius: ringRadiusPx - arcAttrs.arcWidth,
|
||||
outerRadius: ringRadiusPx,
|
||||
startAngle,
|
||||
endAngle
|
||||
});
|
||||
}
|
||||
|
||||
export default function QuadrantRings ({ quadrant }) {
|
||||
// order from top left clockwise
|
||||
export default function QuadrantRings ({ quadrant, xScale}) {
|
||||
// order from top-right clockwise
|
||||
const gradientAttributes = [
|
||||
{x: 0, y: 0, cx: 1, cy: 1, r: 1},
|
||||
{x: size, y: 0, cx: 0, cy: 1, r: 1},
|
||||
{x: size, y: size, cx: 0, cy: 0, r: 1},
|
||||
{x: 0, y: size, cx: 1, cy: 0, r: 1}
|
||||
{x: xScale(0), y: 0, cx: 0, cy: 1, r: 1},
|
||||
{x: xScale(0), y: xScale(0), cx: 0, cy: 0, r: 1},
|
||||
{x: 0, y: xScale(0), cx: 1, cy: 0, r: 1},
|
||||
{x: 0, y: 0, cx: 1, cy: 1, r: 1}
|
||||
];
|
||||
const gradientId = `${quadrant.position}-radial-gradient`;
|
||||
const gradientId = `${quadrant.position}-radial-gradient`,
|
||||
quadrantSize = chartConfig.size / 2;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<g className="quadrant-ring">
|
||||
{/* Definition of the quadrant gradient */}
|
||||
<defs>
|
||||
<radialGradient id={gradientId} {...gradientAttributes[quadrant.position - 1]}>
|
||||
@@ -51,8 +40,8 @@ export default function QuadrantRings ({ quadrant }) {
|
||||
|
||||
{/* Gradient background area */}
|
||||
<rect
|
||||
width={size}
|
||||
height={size}
|
||||
width={quadrantSize}
|
||||
height={quadrantSize}
|
||||
x={gradientAttributes[quadrant.position - 1].x}
|
||||
y={gradientAttributes[quadrant.position - 1].y}
|
||||
fill={`url(#${gradientId})`}
|
||||
@@ -64,8 +53,8 @@ export default function QuadrantRings ({ quadrant }) {
|
||||
<path
|
||||
key={index}
|
||||
fill={quadrant.colour}
|
||||
d={arcPath(quadrant.position, ringPosition)}
|
||||
style={{transform: `translate(${size}px, ${size}px)`}}
|
||||
d={arcPath(quadrant.position, ringPosition, xScale)}
|
||||
style={{transform: `translate(${quadrantSize}px, ${quadrantSize}px)`}}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -1,60 +1,63 @@
|
||||
import React from 'react';
|
||||
import * as d3 from "d3";
|
||||
import './chart.scss';
|
||||
import { blipFlags, chartConfig, quadrantsMap, ringsMap } from '../../config';
|
||||
import { LeftAxis, BottomAxis } from './Axes';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { chartConfig, quadrantsMap, ringsMap } from '../../config';
|
||||
import { YAxis, XAxis } from './Axes';
|
||||
import QuadrantRings from './QuadrantRings';
|
||||
import BlipPoints from './BlipPoints';
|
||||
|
||||
const RingLabel = ({ring}) => {
|
||||
const middlePoint = chartConfig.canvasSize / 2;
|
||||
const shift = (ring.position - 1) * chartConfig.canvasSize / 8 + chartConfig.canvasSize / 16;
|
||||
import './chart.scss';
|
||||
|
||||
const RingLabel = ({ring, xScale, yScale}) => {
|
||||
const ringRadius = chartConfig.ringsAttributes[ring.position - 1].radius,
|
||||
previousRingRadius = ring.position == 1 ? 0 : chartConfig.ringsAttributes[ring.position - 2].radius,
|
||||
|
||||
// middle point in between two ring arcs
|
||||
distanceFromCentre = previousRingRadius + (ringRadius - previousRingRadius) / 2;
|
||||
|
||||
return (
|
||||
<g>
|
||||
{/* Right hand-side label */}
|
||||
<text x={middlePoint + shift} y={middlePoint} textAnchor="middle" dy=".35em">
|
||||
{ring.displayName}
|
||||
</text>
|
||||
{/* Left hand-side label */}
|
||||
<text x={middlePoint - shift} y={middlePoint} textAnchor="middle" dy=".35em">
|
||||
{ring.displayName}
|
||||
</text>
|
||||
<g className="ring-label">
|
||||
{/* Right hand-side label */}
|
||||
<text x={xScale(distanceFromCentre)} y={yScale(0)} textAnchor="middle" dy=".35em">
|
||||
{ring.displayName}
|
||||
</text>
|
||||
{/* Left hand-side label */}
|
||||
<text x={xScale(-distanceFromCentre)} y={yScale(0)} textAnchor="middle" dy=".35em">
|
||||
{ring.displayName}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default function RadarChart({ blips }) {
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([-4, 4])
|
||||
.range([0, chartConfig.canvasSize]);
|
||||
.domain(chartConfig.scale)
|
||||
.range([0, chartConfig.size]);
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([-4, 4])
|
||||
.range([chartConfig.canvasSize, 0]);
|
||||
.domain(chartConfig.scale)
|
||||
.range([chartConfig.size, 0]);
|
||||
|
||||
return (
|
||||
<div className="chart">
|
||||
<div className="chart" style={{maxWidth: `${chartConfig.size}px`}}>
|
||||
<svg viewBox={`0 0 ${chartConfig.size} ${chartConfig.size}`}>
|
||||
<g transform={`translate(${chartConfig.margin}, ${chartConfig.margin})`}>
|
||||
<g transform={`translate(${xScale(0)}, 0)`}>
|
||||
<YAxis scale={yScale}/>
|
||||
</g>
|
||||
<g transform={`translate(0, ${yScale(0)})`}>
|
||||
<XAxis scale={xScale}/>
|
||||
</g>
|
||||
|
||||
<g transform={`translate(${xScale.range()[1] / 2}, 0)`}>
|
||||
<LeftAxis scale={yScale}/>
|
||||
</g>
|
||||
<g transform={`translate(0, ${yScale.range()[0] / 2})`}>
|
||||
<BottomAxis scale={xScale}/>
|
||||
</g>
|
||||
{Object.keys(quadrantsMap).map((id, index) => (
|
||||
<QuadrantRings key={index} quadrant={quadrantsMap[id]} xScale={xScale} />
|
||||
))}
|
||||
|
||||
{Object.keys(quadrantsMap).map((id, index) => (
|
||||
<QuadrantRings key={index} quadrant={quadrantsMap[id]} />
|
||||
))}
|
||||
{Object.keys(ringsMap).map((id, index) => (
|
||||
<RingLabel key={index} ring={ringsMap[id]} xScale={xScale} yScale={yScale} />
|
||||
))}
|
||||
|
||||
{Object.keys(ringsMap).map((id, index) => (
|
||||
<RingLabel key={index} ring={ringsMap[id]} />
|
||||
))}
|
||||
|
||||
<BlipPoints blips={blips} xScale={xScale} yScale={yScale}/>
|
||||
</g>
|
||||
<BlipPoints blips={blips} xScale={xScale} yScale={yScale}/>
|
||||
</svg>
|
||||
<ReactTooltip className="tooltip" offset={{top: -5}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,4 +2,15 @@
|
||||
fill: white;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chart .blip:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chart .tooltip {
|
||||
padding: 5px 10px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Branding from '../Branding/Branding';
|
||||
import Link from '../Link/Link';
|
||||
import IconLink from '../IconLink/IconLink';
|
||||
import LogoLink from '../LogoLink/LogoLink';
|
||||
import Search from '../Search/Search';
|
||||
import { radarNameShort } from '../../config';
|
||||
@@ -50,14 +50,10 @@ export default function Header({ pageName }: { pageName: string }) {
|
||||
<Branding logoContent={<LogoLink small={smallLogo} />}>
|
||||
<div className='nav'>
|
||||
<div className='nav__item'>
|
||||
<Link pageName='help-and-about-tech-radar' className='icon-link'>
|
||||
<span className='icon icon--question icon-link__icon'/>How to Use {radarNameShort}?
|
||||
</Link>
|
||||
<IconLink pageName="help-and-about-tech-radar" icon="question" text={`How to Use ${radarNameShort}?`} />
|
||||
</div>
|
||||
<div className='nav__item'>
|
||||
<Link pageName='overview' className='icon-link'>
|
||||
<span className='icon icon--overview icon-link__icon'/>Technologies Overview
|
||||
</Link>
|
||||
<IconLink pageName="overview" icon="overview" text="All Technologies" />
|
||||
</div>
|
||||
<div className='nav__item'>
|
||||
<button className='icon-link' onClick={handleOpenClick}>
|
||||
|
||||
11
src/components/IconLink/IconLink.tsx
Normal file
11
src/components/IconLink/IconLink.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import Link from '../Link/Link';
|
||||
|
||||
export default function IconLink({icon, pageName, text}) {
|
||||
return (
|
||||
<Link className="icon-link" pageName={pageName}>
|
||||
<span className={`icon icon--${icon} icon-link__icon`} />
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { formatRelease } from '../../date';
|
||||
import { featuredOnly, Item } from '../../model';
|
||||
import HeroHeadline from '../HeroHeadline/HeroHeadline';
|
||||
import QuadrantGrid from '../QuadrantGrid/QuadrantGrid';
|
||||
import RadarChart from '../Chart/RadarChart';
|
||||
import RadarGrid from '../RadarGrid/RadarGrid';
|
||||
import Fadeable from '../Fadeable/Fadeable';
|
||||
import SetTitle from '../SetTitle';
|
||||
import { radarName, radarNameShort } from '../../config';
|
||||
@@ -25,8 +25,10 @@ export default function PageIndex({ leaving, onLeave, items, releases }: PageInd
|
||||
<div className='headline-group'>
|
||||
<HeroHeadline alt={`Version #${numberOfReleases}`}>{radarName}</HeroHeadline>
|
||||
</div>
|
||||
<RadarChart blips={featuredOnly(items)} />
|
||||
<RadarGrid blips={featuredOnly(items)} />
|
||||
{/*
|
||||
<QuadrantGrid items={featuredOnly(items)} />
|
||||
*/}
|
||||
<div className='publish-date'>Published {formatRelease(newestRelease)}</div>
|
||||
</Fadeable>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import Badge from '../Badge/Badge';
|
||||
import ItemList from '../ItemList/ItemList';
|
||||
import Link from '../Link/Link';
|
||||
import IconLink from '../IconLink/IconLink';
|
||||
import FooterEnd from '../FooterEnd/FooterEnd';
|
||||
import SetTitle from '../SetTitle';
|
||||
import ItemRevisions from '../ItemRevisions/ItemRevisions';
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
createAnimationRunner
|
||||
} from '../../animation';
|
||||
import './item-page.scss';
|
||||
import {translate} from '../../config';
|
||||
import {quadrantsMap} from '../../config';
|
||||
import {groupByQuadrants, Item} from '../../model';
|
||||
|
||||
const getItem = (pageName: string, items: Item[]) => {
|
||||
@@ -191,7 +191,7 @@ export default function PageItem({pageName, items, leaving, onLeave}: PageItemPr
|
||||
<div className='item-page__nav'>
|
||||
<div className='item-page__nav__inner'>
|
||||
<div className='item-page__header' style={getAnimationState('navHeader')}>
|
||||
<h3 className='headline'>{translate(item.quadrant)}</h3>
|
||||
<h3 className='headline'>{quadrantsMap[item.quadrant].displayName}</h3>
|
||||
</div>
|
||||
|
||||
<ItemList items={itemsInRing} activeItem={item} headerStyle={getAnimationState('navHeader')}
|
||||
@@ -203,10 +203,7 @@ export default function PageItem({pageName, items, leaving, onLeave}: PageItemPr
|
||||
</Badge>
|
||||
</div>
|
||||
<div className='split__right'>
|
||||
<Link className='icon-link' pageName={item.quadrant}>
|
||||
<span className='icon icon--pie icon-link__icon'/>
|
||||
Quadrant Overview
|
||||
</Link>
|
||||
<IconLink pageName={item.quadrant} icon="pie" text="Quadrant Overview" />
|
||||
</div>
|
||||
</div>
|
||||
</ItemList>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import Badge from '../Badge/Badge';
|
||||
import ItemList from '../ItemList/ItemList';
|
||||
import Link from '../Link/Link';
|
||||
import IconLink from '../IconLink/IconLink';
|
||||
import Fadeable from '../Fadeable/Fadeable';
|
||||
import SetTitle from '../SetTitle';
|
||||
import ItemRevisions from '../ItemRevisions/ItemRevisions';
|
||||
|
||||
import { translate } from '../../config';
|
||||
import { quadrantsMap } from '../../config';
|
||||
import { groupByQuadrants, Item } from '../../model';
|
||||
|
||||
type PageItemMobileProps = {
|
||||
@@ -40,7 +40,7 @@ export default function PageItemMobile({ pageName, items, leaving, onLeave }: Pa
|
||||
<div className='mobile-item-page__header'>
|
||||
<div className='split'>
|
||||
<div className='split__left'>
|
||||
<h3 className='headline'>{translate(item.quadrant)}</h3>
|
||||
<h3 className='headline'>{quadrantsMap[item.quadrant].displayName}</h3>
|
||||
<h1 className='hero-headline hero-headline--inverse'>{item.title}</h1>
|
||||
</div>
|
||||
<div className='split__right'>
|
||||
@@ -59,12 +59,10 @@ export default function PageItemMobile({ pageName, items, leaving, onLeave }: Pa
|
||||
<ItemList items={itemsInRing} activeItem={item}>
|
||||
<div className='split'>
|
||||
<div className='split__left'>
|
||||
<h3 className='headline'>{translate(item.quadrant)}</h3>
|
||||
<h3 className='headline'>{quadrantsMap[item.quadrant].displayName}</h3>
|
||||
</div>
|
||||
<div className='split__right'>
|
||||
<Link className='icon-link' pageName={item.quadrant}>
|
||||
<span className='icon icon--pie icon-link__icon'></span>Zoom In
|
||||
</Link>
|
||||
<IconLink pageName={item.quadrant} icon="pie" text="Zoom in" />
|
||||
</div>
|
||||
</div>
|
||||
</ItemList>
|
||||
|
||||
@@ -8,7 +8,7 @@ import Fadeable from '../Fadeable/Fadeable';
|
||||
import SetTitle from '../SetTitle';
|
||||
import Flag from '../Flag/Flag';
|
||||
import { groupByFirstLetter, Item } from '../../model';
|
||||
import { translate, Ring } from '../../config';
|
||||
import { quadrantsMap, Ring } from '../../config';
|
||||
|
||||
const containsSearchTerm = (text = '', term = '') => {
|
||||
// TODO search refinement
|
||||
@@ -109,7 +109,7 @@ export default function PageOverview({ rings, search: searchProp, items, leaving
|
||||
</div>
|
||||
<div className='split__right'>
|
||||
<div className='nav nav--relations'>
|
||||
<div className='nav__item'>{translate(item.quadrant)}</div>
|
||||
<div className='nav__item'>{quadrantsMap[item.quadrant].displayName}</div>
|
||||
<div className='nav__item'>
|
||||
<Badge type={item.ring}>{item.ring}</Badge>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import QuadrantSection from '../QuadrantSection/QuadrantSection';
|
||||
import Fadeable from '../Fadeable/Fadeable';
|
||||
import SetTitle from '../SetTitle';
|
||||
|
||||
import { translate } from '../../config';
|
||||
import { quadrantsMap } from '../../config';
|
||||
import { featuredOnly, groupByQuadrants, Item } from '../../model';
|
||||
|
||||
type PageQuadrantProps = {
|
||||
@@ -19,11 +19,11 @@ export default function PageQuadrant({ leaving, onLeave, pageName, items }: Page
|
||||
const groups = groupByQuadrants(featuredOnly(items));
|
||||
return (
|
||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
||||
<SetTitle title={translate(pageName)} />
|
||||
<SetTitle title={quadrantsMap[pageName].displayName} />
|
||||
<HeadlineGroup>
|
||||
<HeroHeadline>{translate(pageName)}</HeroHeadline>
|
||||
<HeroHeadline>{quadrantsMap[pageName].displayName}</HeroHeadline>
|
||||
</HeadlineGroup>
|
||||
<QuadrantSection groups={groups} quadrantName={pageName} big />
|
||||
<QuadrantSection groups={groups} quadrantName={pageName} big showTitle={false} />
|
||||
</Fadeable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { translate, rings, Ring, showEmptyRings } from '../../config';
|
||||
import { rings, quadrantsMap, Ring, showEmptyRings } from '../../config';
|
||||
import Badge from '../Badge/Badge';
|
||||
import Link from '../Link/Link';
|
||||
import IconLink from '../IconLink/IconLink';
|
||||
import ItemList from '../ItemList/ItemList';
|
||||
import Flag from '../Flag/Flag';
|
||||
import { Group } from '../../model';
|
||||
@@ -47,20 +48,19 @@ const renderRing = (ringName: Ring, quadrantName: string, groups: Group, big: bo
|
||||
);
|
||||
};
|
||||
|
||||
export default function QuadrantSection({ quadrantName, groups, big = false }: { quadrantName: string; groups: Group; big?: boolean }) {
|
||||
export default function QuadrantSection({ quadrantName, groups, big = false, showTitle = true}: { quadrantName: string; groups: Group; big?: boolean; showTitle?: boolean }) {
|
||||
return (
|
||||
<div className='quadrant-section'>
|
||||
<div className='quadrant-section__header'>
|
||||
<div className='split'>
|
||||
<div className='split__left'>
|
||||
<h4 className='headline'>{translate(quadrantName)}</h4>
|
||||
</div>
|
||||
{showTitle && (
|
||||
<div className="split__left">
|
||||
<h4 className="headline">{quadrantsMap[quadrantName].displayName}</h4>
|
||||
</div>
|
||||
)}
|
||||
{!big && (
|
||||
<div className='split__right'>
|
||||
<Link className='icon-link' pageName={`${quadrantName}`}>
|
||||
<span className='icon icon--pie icon-link__icon' />
|
||||
Zoom In
|
||||
</Link>
|
||||
<IconLink pageName={quadrantName} icon="pie" text="Zoom In" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
63
src/components/RadarGrid/RadarGrid.tsx
Normal file
63
src/components/RadarGrid/RadarGrid.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import RadarChart from '../Chart/RadarChart';
|
||||
import IconLink from '../IconLink/IconLink';
|
||||
import { quadrantsMap } from '../../config';
|
||||
|
||||
import './radar-grid.scss';
|
||||
|
||||
const QuadrantLabel = ({quadrant}) => {
|
||||
const stylesMap = [
|
||||
{top: 0, right: 0},
|
||||
{bottom: 0, right: 0},
|
||||
{bottom: 0, left: 0},
|
||||
{top: 0, left: 0},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="quadrant-label" style={stylesMap[quadrant.position - 1]}>
|
||||
<div className="split">
|
||||
<div className="split__left">
|
||||
<small>Quadrant {quadrant.position}</small>
|
||||
</div>
|
||||
<div className="split__right">
|
||||
<IconLink icon="pie" pageName={`${quadrant.id}`} text="Zoom In" />
|
||||
</div>
|
||||
</div>
|
||||
<hr style={{borderColor: quadrant.colour}}/>
|
||||
<h4 className="headline">{quadrant.displayName}</h4>
|
||||
<div className="description">{quadrant.description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Legend = () => {
|
||||
return (
|
||||
<div className="radar-legend">
|
||||
<div className="wrapper">
|
||||
<span className="icon icon--blip_new"></span>
|
||||
New in this version
|
||||
</div>
|
||||
<div className="wrapper">
|
||||
<span className="icon icon--blip_changed"></span>
|
||||
Recently changed
|
||||
</div>
|
||||
<div className="wrapper">
|
||||
<span className="icon icon--blip_default"></span>
|
||||
Unchanged
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RadarGrid({ blips }) {
|
||||
|
||||
return (
|
||||
<div className="radar-grid">
|
||||
<RadarChart blips={blips} />
|
||||
{Object.keys(quadrantsMap).map((id, index) => (
|
||||
<QuadrantLabel key={index} quadrant={quadrantsMap[id]} />
|
||||
))}
|
||||
<Legend />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
src/components/RadarGrid/radar-grid.scss
Normal file
52
src/components/RadarGrid/radar-grid.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
.radar-grid {
|
||||
position: relative;
|
||||
margin-bottom: 50px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.radar-grid .quadrant-label {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.quadrant-label .split {
|
||||
font-size: 12 px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.quadrant-label hr {
|
||||
width: 100%;
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
.quadrant-label .description {
|
||||
font-size: 14px;
|
||||
color: #a6b1bb
|
||||
}
|
||||
|
||||
.quadrant-label .icon-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.quadrant-label .icon-link .icon {
|
||||
background-size: 18px 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.radar-grid .radar-legend {
|
||||
position: absolute;
|
||||
width: 15%;
|
||||
right: 0;
|
||||
top: 45%;
|
||||
}
|
||||
|
||||
.radar-legend .wrapper {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.radar-legend .icon {
|
||||
|
||||
background-position: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import PageHelp from './PageHelp/PageHelp';
|
||||
import PageQuadrant from './PageQuadrant/PageQuadrant';
|
||||
import PageItem from './PageItem/PageItem';
|
||||
import PageItemMobile from './PageItemMobile/PageItemMobile';
|
||||
import {quadrants, getItemPageNames, isMobileViewport, rings} from '../config';
|
||||
import {quadrantsMap, getItemPageNames, isMobileViewport, rings} from '../config';
|
||||
import {Item} from '../model';
|
||||
|
||||
type RouterProps = {
|
||||
@@ -35,7 +35,7 @@ const getPageByName = (items: Item[], pageName: string): page => {
|
||||
if (pageName === 'help-and-about-tech-radar') {
|
||||
return page.help;
|
||||
}
|
||||
if (quadrants.includes(pageName)) {
|
||||
if (Object.keys(quadrantsMap).includes(pageName)) {
|
||||
return page.quadrant;
|
||||
}
|
||||
if (getItemPageNames(items).includes(pageName)) {
|
||||
|
||||
128
src/config.ts
128
src/config.ts
@@ -3,43 +3,52 @@ import {Item} from './model';
|
||||
export const radarName = process.env.RADAR_NAME || 'AOE Technology Radar'
|
||||
export const radarNameShort = radarName;
|
||||
|
||||
export const quadrants = [
|
||||
'languages-and-frameworks',
|
||||
'methods-and-patterns',
|
||||
'platforms-and-aoe-services',
|
||||
'tools',
|
||||
];
|
||||
// Quadrants positions start from the top right and go clockwise
|
||||
export const quadrantsMap = {
|
||||
'methods-and-patterns': {
|
||||
id: 'methods-and-patterns',
|
||||
displayName: 'Methods & Patterns',
|
||||
colour: '#248EA6',
|
||||
txtColour: 'white',
|
||||
position: 1,
|
||||
description: 'Optional description goes here'
|
||||
},
|
||||
'platforms-and-aoe-services': {
|
||||
id: 'platforms-and-aoe-services',
|
||||
displayName: 'Platforms and Operations',
|
||||
colour: '#F25244',
|
||||
txtColour: '#444444',
|
||||
position: 2,
|
||||
description: 'Optional description goes here'
|
||||
},
|
||||
'tools': {
|
||||
id: 'tools',
|
||||
displayName: 'Tools',
|
||||
colour: '#F2A25C',
|
||||
txtColour: 'white',
|
||||
position: 3,
|
||||
description: 'Optional descrption goes here'
|
||||
},
|
||||
'languages-and-frameworks': {
|
||||
id: 'languages-and-frameworks',
|
||||
displayName: 'Languages & Frameworks',
|
||||
colour: '#84BFA4',
|
||||
txtColour: '#444444',
|
||||
position: 4,
|
||||
description: 'Optional description goes here'
|
||||
},
|
||||
};
|
||||
|
||||
// Quadrants positions start from the top left and go clockwise
|
||||
export const quadrantsMap = {
|
||||
'languages-and-frameworks': {
|
||||
displayName: 'Languages & Frameworks',
|
||||
colour: '#84BFA4',
|
||||
position: 1
|
||||
},
|
||||
'methods-and-patterns': {
|
||||
displayName: 'Methods & Patterns',
|
||||
colour: '#248EA6',
|
||||
position: 2
|
||||
},
|
||||
'platforms-and-aoe-services': {
|
||||
displayName: 'Platforms and Operations',
|
||||
colour: '#F25244',
|
||||
position: 3
|
||||
},
|
||||
'tools': {
|
||||
displayName: 'Tools',
|
||||
colour: '#F2A25C',
|
||||
position: 4
|
||||
},
|
||||
};
|
||||
|
||||
const chartMargin = 20,
|
||||
chartSize = 900;
|
||||
export const chartConfig = {
|
||||
margin: chartMargin,
|
||||
size: chartSize,
|
||||
canvasSize: chartSize - chartMargin * 2
|
||||
size: 800, //in px
|
||||
scale: [-16, 16],
|
||||
blipSize: 12, // in px, be careful when increasing this value as it may cause a lot of calculations during placing the blips on the chart
|
||||
ringsAttributes: [ // order from the centre outwards
|
||||
{ radius: 8, arcWidth: 6 }, // radius values are based on the scale (not px!)
|
||||
{ radius: 11, arcWidth: 4 },
|
||||
{ radius: 14, arcWidth: 2 },
|
||||
{ radius: 16, arcWidth: 2 }
|
||||
]
|
||||
};
|
||||
|
||||
export const rings = [
|
||||
@@ -52,29 +61,29 @@ export const rings = [
|
||||
|
||||
// rings positions start at the centre and go outwards
|
||||
export const ringsMap = {
|
||||
'adopt': {
|
||||
displayName: 'ADOPT',
|
||||
position: 1
|
||||
},
|
||||
'trial': {
|
||||
displayName: 'TRIAL',
|
||||
position: 2
|
||||
},
|
||||
'assess': {
|
||||
displayName: 'ASSESS',
|
||||
position: 3
|
||||
},
|
||||
'hold': {
|
||||
displayName: 'HOLD',
|
||||
position: 4
|
||||
}
|
||||
};
|
||||
'adopt': {
|
||||
displayName: 'ADOPT',
|
||||
position: 1
|
||||
},
|
||||
'trial': {
|
||||
displayName: 'EXPLORE',
|
||||
position: 2
|
||||
},
|
||||
'assess': {
|
||||
displayName: 'ENDURE',
|
||||
position: 3
|
||||
},
|
||||
'hold': {
|
||||
displayName: 'RETIRE',
|
||||
position: 4
|
||||
}
|
||||
};
|
||||
|
||||
// TODO replace with TS enum
|
||||
export const blipFlags = {
|
||||
new: { name: 'new', short: 'N' },
|
||||
changed: { name: 'changed', short: 'C' },
|
||||
default: { name: 'default', short: '' }
|
||||
new: { name: 'new', short: 'N' },
|
||||
changed: { name: 'changed', short: 'C' },
|
||||
default: { name: 'default', short: '' }
|
||||
}
|
||||
|
||||
export type Ring = typeof rings[number]
|
||||
@@ -83,15 +92,6 @@ export const getItemPageNames = (items: Item[]) => items.map(item => `${item.qua
|
||||
|
||||
export const showEmptyRings = false;
|
||||
|
||||
const messages: { [k: string]: string } = {
|
||||
'languages-and-frameworks': 'Languages & Frameworks',
|
||||
'methods-and-patterns': 'Methods & Patterns',
|
||||
'platforms-and-aoe-services': 'Platforms and Operations',
|
||||
'tools': 'Tools',
|
||||
};
|
||||
|
||||
export const translate = (key: string) => (messages[key] || '-');
|
||||
|
||||
export function isMobileViewport() {
|
||||
// return false for server side rendering
|
||||
if (typeof window == 'undefined') return false;
|
||||
|
||||
4
src/icons/blip_changed.svg
Normal file
4
src/icons/blip_changed.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect transform="rotate(-45 8 8)" x="2" y="2" width="12" height="12" rx="3" fill="#a6b1bb"></rect>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 319 B |
4
src/icons/blip_default.svg
Normal file
4
src/icons/blip_default.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="6" fill="#a6b1bb"></circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
4
src/icons/blip_new.svg
Normal file
4
src/icons/blip_new.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg viewbox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#a6b1bb" d="M.247 10.212l5.02-8.697a2 2 0 013.465 0l5.021 8.697a2 2 0 01-1.732 3H1.98a2 2 0 01-1.732-3z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -30,4 +30,25 @@
|
||||
&--close {
|
||||
background-image: url('../../icons/close.svg');
|
||||
}
|
||||
|
||||
&--blip_new {
|
||||
background-image: url('../../icons/blip_new.svg');
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: 18px;
|
||||
}
|
||||
|
||||
&--blip_changed {
|
||||
background-image: url('../../icons/blip_changed.svg');
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: 18px;
|
||||
}
|
||||
|
||||
&--blip_default {
|
||||
background-image: url('../../icons/blip_default.svg');
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: 18px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user