Merge remote-tracking branch 'jar0s/feature/radar-chart'
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { MouseEventHandler } from "react";
|
||||
import classNames from "classnames";
|
||||
import "./badge.scss";
|
||||
|
||||
type BadgeProps = {
|
||||
onClick?: MouseEventHandler;
|
||||
big?: boolean;
|
||||
|
||||
46
src/components/Chart/Axes.tsx
Normal file
46
src/components/Chart/Axes.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { useRef, useLayoutEffect } from 'react';
|
||||
import * as d3 from "d3";
|
||||
|
||||
export const YAxis: React.FC<{
|
||||
scale: d3.ScaleLinear<number, number>
|
||||
}> = ({ scale }) => {
|
||||
|
||||
const ref = useRef<SVGSVGElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current == null) {
|
||||
return
|
||||
}
|
||||
const axisGenerator = d3.axisLeft(scale).ticks(6);
|
||||
d3.select(ref.current)
|
||||
.attr('class', 'y-axis')
|
||||
.call(axisGenerator)
|
||||
.call(g => g.selectAll('.tick text').remove())
|
||||
.call(g => g.selectAll('.tick line').remove())
|
||||
.call(g => g.selectAll('.domain').remove());
|
||||
}, [scale]);
|
||||
|
||||
return <g ref={ref} />;
|
||||
};
|
||||
|
||||
export const XAxis: React.FC<{
|
||||
scale: d3.ScaleLinear<number, number>
|
||||
}> = ({ scale }) => {
|
||||
|
||||
const ref = useRef<SVGSVGElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current == null) {
|
||||
return
|
||||
}
|
||||
const axisGenerator = d3.axisBottom(scale).ticks(6);
|
||||
d3.select(ref.current)
|
||||
.attr('class', 'x-axis')
|
||||
.call(axisGenerator)
|
||||
.call(g => g.selectAll('.tick text').remove())
|
||||
.call(g => g.selectAll('.tick line').remove())
|
||||
.call(g => g.selectAll('.domain').remove());
|
||||
}, [scale]);
|
||||
|
||||
return <g ref={ref} />;
|
||||
};
|
||||
126
src/components/Chart/BlipPoints.tsx
Normal file
126
src/components/Chart/BlipPoints.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { ScaleLinear } from 'd3';
|
||||
import { FlagType, Item, Blip, Point } from '../../model';
|
||||
import Link from '../Link/Link';
|
||||
import { NewBlip, ChangedBlip, DefaultBlip } from './BlipShapes';
|
||||
import { ConfigData } from '../../config';
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
|
||||
function generateCoordinates(blip: Blip, xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>, config: ConfigData): Point {
|
||||
const pi = Math.PI,
|
||||
ringRadius = config.chartConfig.ringsAttributes[blip.ringPosition].radius,
|
||||
previousRingRadius = blip.ringPosition === 0 ? 0 : config.chartConfig.ringsAttributes[blip.ringPosition - 1].radius,
|
||||
ringPadding = 0.7;
|
||||
|
||||
// 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, so we need to "invert" quadrant positions (i.e. swap 2 with 4)
|
||||
*/
|
||||
const shift = pi * [1, 4, 3, 2][blip.quadrantPosition - 1] / 2;
|
||||
|
||||
return {
|
||||
x: xScale(Math.cos(randomDegree + shift) * radius),
|
||||
y: yScale(Math.sin(randomDegree + shift) * radius)
|
||||
};
|
||||
};
|
||||
|
||||
function randomBetween (min: number, max: number): number {
|
||||
return Math.random() * (max - min) + min;
|
||||
};
|
||||
|
||||
function distanceBetween(point1: Point, point2: Point): number {
|
||||
const a = point2.x - point1.x;
|
||||
const b = point2.y - point1.y;
|
||||
return Math.sqrt((a * a) + (b * b));
|
||||
};
|
||||
|
||||
function renderBlip(blip: Blip, index: number, config: ConfigData): JSX.Element {
|
||||
const props = {
|
||||
blip,
|
||||
className: 'blip',
|
||||
fill: blip.colour,
|
||||
'data-background-color': blip.colour,
|
||||
'data-text-color': blip.txtColour,
|
||||
'data-tip': blip.title,
|
||||
key: index
|
||||
}
|
||||
switch (blip.flag) {
|
||||
case FlagType.new:
|
||||
return <NewBlip {...props} config={config} />;
|
||||
case FlagType.changed:
|
||||
return <ChangedBlip {...props} config={config} />;
|
||||
default:
|
||||
return <DefaultBlip {...props} config={config} />;
|
||||
}
|
||||
};
|
||||
|
||||
const BlipPoints: React.FC<{
|
||||
items: Item[]
|
||||
xScale:ScaleLinear<number, number>
|
||||
yScale:ScaleLinear<number, number>
|
||||
config:ConfigData
|
||||
}> = ({items, xScale, yScale, config}) => {
|
||||
|
||||
const blips: Blip[] = items.reduce((list: Blip[], item: Item) => {
|
||||
if (!item.ring || !item.quadrant) {
|
||||
// skip the blip if it doesn't have a ring or quadrant assigned
|
||||
return list;
|
||||
}
|
||||
const quadrantConfig = config.quadrantsMap[item.quadrant];
|
||||
if (!quadrantConfig) {
|
||||
return list;
|
||||
}
|
||||
|
||||
let blip: Blip = { ...item,
|
||||
quadrantPosition: quadrantConfig.position,
|
||||
ringPosition: config.rings.findIndex(r => r === item.ring),
|
||||
colour: quadrantConfig.colour,
|
||||
txtColour: quadrantConfig.txtColour,
|
||||
coordinates: {x: 0, y: 0},
|
||||
};
|
||||
|
||||
let point: Point;
|
||||
let counter = 1;
|
||||
let distanceBetweenCheck: boolean;
|
||||
do {
|
||||
const localpoint = generateCoordinates(blip, xScale, yScale, config);
|
||||
point = localpoint
|
||||
counter++;
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
distanceBetweenCheck = list.some(b => distanceBetween(localpoint, b.coordinates) < config.chartConfig.blipSize + config.chartConfig.blipSize / 2)
|
||||
} while (counter < 100
|
||||
&& (Math.abs(point.x - xScale(0)) < 15
|
||||
|| Math.abs(point.y - yScale(0)) < 15
|
||||
|| distanceBetweenCheck
|
||||
));
|
||||
|
||||
blip.coordinates = point;
|
||||
|
||||
list.push(blip);
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<g className="blips">
|
||||
{blips.map((blip, index) => (
|
||||
<Link pageName={`${blip.quadrant}/${blip.name}`} key={index}>
|
||||
{renderBlip(blip, index, config)}
|
||||
</Link>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlipPoints;
|
||||
64
src/components/Chart/BlipShapes.tsx
Normal file
64
src/components/Chart/BlipShapes.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { ConfigData } from '../../config';
|
||||
import { Blip } from '../../model';
|
||||
|
||||
type VisualBlipProps = {
|
||||
className: string,
|
||||
fill: string,
|
||||
'data-background-color': string,
|
||||
'data-text-color': string,
|
||||
'data-tip': string,
|
||||
key: number
|
||||
}
|
||||
|
||||
export const ChangedBlip: React.FC<
|
||||
{blip: Blip, config: ConfigData} & VisualBlipProps
|
||||
> = ({blip, config, ...props}) => {
|
||||
const centeredX = blip.coordinates.x - config.chartConfig.blipSize/2,
|
||||
centeredY = blip.coordinates.y - config.chartConfig.blipSize/2;
|
||||
|
||||
return (
|
||||
<rect
|
||||
transform={`rotate(-45 ${centeredX} ${centeredY})`}
|
||||
x={centeredX}
|
||||
y={centeredY}
|
||||
width={config.chartConfig.blipSize}
|
||||
height={config.chartConfig.blipSize}
|
||||
rx="3"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewBlip: React.FC<
|
||||
{blip: Blip, config: ConfigData} & VisualBlipProps
|
||||
> = ({blip, config, ...props}) => {
|
||||
const centeredX = blip.coordinates.x - config.chartConfig.blipSize/2,
|
||||
centeredY = blip.coordinates.y - config.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: React.FC<
|
||||
{blip: Blip, config: ConfigData} & VisualBlipProps
|
||||
> = ({blip, config, ...props}) => {
|
||||
return (
|
||||
<circle
|
||||
r={config.chartConfig.blipSize / 2}
|
||||
cx={blip.coordinates.x}
|
||||
cy={blip.coordinates.y}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
74
src/components/Chart/QuadrantRings.tsx
Normal file
74
src/components/Chart/QuadrantRings.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import { QuadrantConfig } from '../../model';
|
||||
import { ConfigData } from '../../config';
|
||||
|
||||
function arcPath(quadrantPosition: number, ringPosition: number, xScale: d3.ScaleLinear<number, number>, config: ConfigData) {
|
||||
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 = config.chartConfig.ringsAttributes[ringPosition],
|
||||
ringRadiusPx = xScale(arcAttrs.radius) - xScale(0),
|
||||
arc = d3.arc();
|
||||
|
||||
return arc({
|
||||
innerRadius: ringRadiusPx - arcAttrs.arcWidth,
|
||||
outerRadius: ringRadiusPx,
|
||||
startAngle,
|
||||
endAngle
|
||||
}) || undefined;
|
||||
}
|
||||
|
||||
const QuadrantRings: React.FC<{
|
||||
quadrant: QuadrantConfig
|
||||
xScale: d3.ScaleLinear<number, number>
|
||||
config: ConfigData
|
||||
}> = ({ quadrant, xScale, config}) => {
|
||||
// order from top-right clockwise
|
||||
const gradientAttributes = [
|
||||
{x: 0, y: 0, cx: 1, cy: 1, 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}
|
||||
];
|
||||
const gradientId = `${quadrant.position}-radial-gradient`,
|
||||
quadrantSize = config.chartConfig.size / 2;
|
||||
|
||||
return (
|
||||
<g className="quadrant-ring">
|
||||
{/* Definition of the quadrant gradient */}
|
||||
<defs>
|
||||
<radialGradient id={gradientId} {...gradientAttributes[quadrant.position - 1]}>
|
||||
<stop offset="0%" stopColor={quadrant.colour}></stop>
|
||||
<stop offset="100%" stopColor={quadrant.colour} stopOpacity="0"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
{/* Gradient background area */}
|
||||
<rect
|
||||
width={quadrantSize}
|
||||
height={quadrantSize}
|
||||
x={gradientAttributes[quadrant.position - 1].x}
|
||||
y={gradientAttributes[quadrant.position - 1].y}
|
||||
fill={`url(#${gradientId})`}
|
||||
style={{opacity: 0.5}}
|
||||
/>
|
||||
|
||||
{/* Rings' arcs */}
|
||||
{Array.from(config.rings).map((ringPosition, index) => (
|
||||
<path
|
||||
key={index}
|
||||
fill={quadrant.colour}
|
||||
d={arcPath(quadrant.position, index, xScale, config)}
|
||||
style={{transform: `translate(${quadrantSize}px, ${quadrantSize}px)`}}
|
||||
/>
|
||||
))}
|
||||
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuadrantRings;
|
||||
77
src/components/Chart/RadarChart.tsx
Normal file
77
src/components/Chart/RadarChart.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import * as d3 from "d3";
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { Item } from '../../model';
|
||||
import { YAxis, XAxis } from './Axes';
|
||||
import QuadrantRings from './QuadrantRings';
|
||||
import BlipPoints from './BlipPoints';
|
||||
|
||||
import './chart.scss';
|
||||
import { ConfigData } from '../../config';
|
||||
|
||||
const RingLabel: React.FC<{
|
||||
ring: string
|
||||
xScale: d3.ScaleLinear<number, number>
|
||||
yScale: d3.ScaleLinear<number, number>
|
||||
config: ConfigData
|
||||
}> = ({ring, xScale, yScale, config}) => {
|
||||
const ringIndex = config.rings.findIndex(r => r === ring)
|
||||
|
||||
const ringRadius = config.chartConfig.ringsAttributes[ringIndex].radius,
|
||||
previousRingRadius = ringIndex === 0 ? 0 : config.chartConfig.ringsAttributes[ringIndex - 1].radius,
|
||||
|
||||
// middle point in between two ring arcs
|
||||
distanceFromCentre = previousRingRadius + (ringRadius - previousRingRadius) / 2;
|
||||
|
||||
return (
|
||||
<g className="ring-label">
|
||||
{/* Right hand-side label */}
|
||||
<text x={xScale(distanceFromCentre)} y={yScale(0)} textAnchor="middle" dy=".35em">
|
||||
{ring}
|
||||
</text>
|
||||
{/* Left hand-side label */}
|
||||
<text x={xScale(-distanceFromCentre)} y={yScale(0)} textAnchor="middle" dy=".35em">
|
||||
{ring}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const RadarChart: React.FC<{
|
||||
items: Item[]
|
||||
config: ConfigData
|
||||
}> = ({ items, config }) => {
|
||||
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain(config.chartConfig.scale)
|
||||
.range([0, config.chartConfig.size]);
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain(config.chartConfig.scale)
|
||||
.range([config.chartConfig.size, 0]);
|
||||
|
||||
return (
|
||||
<div className="chart" style={{maxWidth: `${config.chartConfig.size}px`}}>
|
||||
<svg viewBox={`0 0 ${config.chartConfig.size} ${config.chartConfig.size}`}>
|
||||
<g transform={`translate(${xScale(0)}, 0)`}>
|
||||
<YAxis scale={yScale}/>
|
||||
</g>
|
||||
<g transform={`translate(0, ${yScale(0)})`}>
|
||||
<XAxis scale={xScale}/>
|
||||
</g>
|
||||
|
||||
{Object.values(config.quadrantsMap).map((value, index) => (
|
||||
<QuadrantRings key={index} quadrant={value} xScale={xScale} config={config} />
|
||||
))}
|
||||
|
||||
{Array.from(config.rings).map((ring: string, index) => (
|
||||
<RingLabel key={index} ring={ring} xScale={xScale} yScale={yScale} config={config} />
|
||||
))}
|
||||
|
||||
<BlipPoints items={items} xScale={xScale} yScale={yScale} config={config} />
|
||||
</svg>
|
||||
<ReactTooltip className="tooltip" offset={{top: -5}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RadarChart;
|
||||
20
src/components/Chart/chart.scss
Normal file
20
src/components/Chart/chart.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.chart {
|
||||
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;
|
||||
}
|
||||
|
||||
.chart .ring-label {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from "react";
|
||||
import { FlagType } from "../../model";
|
||||
import "./flag.scss";
|
||||
|
||||
export type FlagType = "new" | "changed" | "default";
|
||||
|
||||
interface ItemFlag {
|
||||
flag: FlagType;
|
||||
}
|
||||
@@ -16,7 +14,7 @@ export default function Flag({
|
||||
}) {
|
||||
const ucFirst = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
|
||||
if (item.flag !== "default") {
|
||||
if (item.flag !== FlagType.default) {
|
||||
let name = item.flag.toUpperCase();
|
||||
let title = ucFirst(item.flag);
|
||||
if (short === true) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Item } from "../../model";
|
||||
import { FlagType, Item } from "../../model";
|
||||
|
||||
export const item: Item = {
|
||||
flag: "default",
|
||||
flag: FlagType.default,
|
||||
featured: false,
|
||||
revisions: [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import Badge from "../Badge/Badge";
|
||||
import { formatRelease } from "../../date";
|
||||
import { Revision } from "../../model";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { Link as RLink } from "react-router-dom";
|
||||
import "./link.scss";
|
||||
|
||||
type LinkProps = {
|
||||
pageName: string;
|
||||
style?: React.CSSProperties;
|
||||
|
||||
@@ -33,14 +33,13 @@ const PageHelp: React.FC<Props> = ({ leaving, onLeave }) => {
|
||||
<React.Fragment key={headline}>
|
||||
<h3>{headline}</h3>
|
||||
{values.map((element, index) => {
|
||||
const content = sanitizeHtml(element, {
|
||||
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
|
||||
allowedAttributes: {
|
||||
'a': ['href', 'target']
|
||||
},
|
||||
});
|
||||
return (
|
||||
<p key={index} dangerouslySetInnerHTML={sanitize(element)}></p>
|
||||
<p key={index} dangerouslySetInnerHTML={sanitize(element, {
|
||||
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
|
||||
allowedAttributes: {
|
||||
'a': ['href', 'target']
|
||||
},
|
||||
})}></p>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { formatRelease } from "../../date";
|
||||
import { featuredOnly, Item } from "../../model";
|
||||
import { featuredOnly, Item, HomepageOption } from "../../model";
|
||||
import HeroHeadline from "../HeroHeadline/HeroHeadline";
|
||||
import QuadrantGrid from "../QuadrantGrid/QuadrantGrid";
|
||||
import RadarGrid from '../RadarGrid/RadarGrid';
|
||||
import Fadeable from "../Fadeable/Fadeable";
|
||||
import SetTitle from "../SetTitle";
|
||||
import { ConfigData, radarName, radarNameShort } from "../../config";
|
||||
@@ -28,6 +29,8 @@ export default function PageIndex({
|
||||
|
||||
const newestRelease = releases.slice(-1)[0];
|
||||
const numberOfReleases = releases.length;
|
||||
const showChart = config.homepageContent === HomepageOption.chart || config.homepageContent === HomepageOption.both;
|
||||
const showColumns = config.homepageContent === HomepageOption.columns || config.homepageContent === HomepageOption.both;
|
||||
return (
|
||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
||||
<SetTitle title={radarNameShort} />
|
||||
@@ -36,7 +39,12 @@ export default function PageIndex({
|
||||
{radarName}
|
||||
</HeroHeadline>
|
||||
</div>
|
||||
<QuadrantGrid items={featuredOnly(items)} config={config} />
|
||||
{showChart && (
|
||||
<RadarGrid items={featuredOnly(items)} config={config} />
|
||||
)}
|
||||
{showColumns && (
|
||||
<QuadrantGrid items={featuredOnly(items)} config={config} />
|
||||
)}
|
||||
<div className="publish-date">
|
||||
{publishedLabel} {formatRelease(newestRelease)}
|
||||
</div>
|
||||
|
||||
75
src/components/RadarGrid/RadarGrid.tsx
Normal file
75
src/components/RadarGrid/RadarGrid.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import RadarChart from "../Chart/RadarChart";
|
||||
import { ConfigData } from "../../config";
|
||||
import { Item, QuadrantConfig } from "../../model";
|
||||
|
||||
import "./radar-grid.scss";
|
||||
import Link from "../Link/Link";
|
||||
|
||||
const QuadrantLabel: React.FC<{
|
||||
quadrantConfig: QuadrantConfig;
|
||||
quadrantName: string;
|
||||
quadrantLabel: string;
|
||||
}> = ({ quadrantConfig, quadrantName, quadrantLabel }) => {
|
||||
const stylesMap = [
|
||||
{ top: 0, left: 0 },
|
||||
{ top: 0, right: 0 },
|
||||
{ bottom: 0, right: 0 },
|
||||
{ bottom: 0, left: 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="quadrant-label" style={stylesMap[quadrantConfig.position - 1]}>
|
||||
<div className="split">
|
||||
<div className="split__left">
|
||||
<small>Quadrant {quadrantConfig.position}</small>
|
||||
</div>
|
||||
<div className="split__right">
|
||||
<Link className="icon-link" pageName={`${quadrantName}`}>
|
||||
<span className="icon icon--pie icon-link__icon" />
|
||||
Zoom In
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<hr style={{ borderColor: quadrantConfig.colour }} />
|
||||
<h4 className="headline">{quadrantLabel}</h4>
|
||||
<div className="description">{quadrantConfig.description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Legend: React.FC = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const RadarGrid: React.FC<{ items: Item[]; config: ConfigData }> = ({
|
||||
items,
|
||||
config,
|
||||
}) => {
|
||||
return (
|
||||
<div className="radar-grid">
|
||||
<RadarChart items={items} config={config} />
|
||||
{Object.entries(config.quadrantsMap).map(([name, quadrant], index) => (
|
||||
<QuadrantLabel key={index} quadrantConfig={quadrant} quadrantName={name} quadrantLabel={config.quadrants[name]} />
|
||||
))}
|
||||
<Legend />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadarGrid;
|
||||
59
src/components/RadarGrid/radar-grid.scss
Normal file
59
src/components/RadarGrid/radar-grid.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
.radar-grid {
|
||||
position: relative;
|
||||
margin-bottom: 50px;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
.radar-grid {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Item } from "./model";
|
||||
import {Item, HomepageOption, QuadrantConfig} from './model';
|
||||
|
||||
export interface ConfigData {
|
||||
quadrants: { [key: string]: string };
|
||||
rings: string[];
|
||||
showEmptyRings: boolean;
|
||||
quadrantsMap: { [quadrant: string]: QuadrantConfig };
|
||||
chartConfig: {
|
||||
size: number,
|
||||
scale: number[],
|
||||
blipSize: number,
|
||||
ringsAttributes: {radius: number, arcWidth: number}[]
|
||||
};
|
||||
homepageContent: HomepageOption;
|
||||
}
|
||||
|
||||
export const radarName =
|
||||
|
||||
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 |
32
src/model.ts
32
src/model.ts
@@ -1,3 +1,9 @@
|
||||
export enum HomepageOption {
|
||||
chart = "chart",
|
||||
columns = "columns",
|
||||
both = "both"
|
||||
}
|
||||
|
||||
export type ItemAttributes = {
|
||||
name: string;
|
||||
ring: string;
|
||||
@@ -6,7 +12,11 @@ export type ItemAttributes = {
|
||||
featured?: boolean;
|
||||
};
|
||||
|
||||
export type FlagType = "new" | "changed" | "default";
|
||||
export enum FlagType {
|
||||
new = 'new',
|
||||
changed = 'changed',
|
||||
default = 'default'
|
||||
}
|
||||
|
||||
export type Item = ItemAttributes & {
|
||||
featured: boolean;
|
||||
@@ -16,6 +26,14 @@ export type Item = ItemAttributes & {
|
||||
revisions: Revision[];
|
||||
};
|
||||
|
||||
export type Blip = Item & {
|
||||
quadrantPosition: number
|
||||
ringPosition: number
|
||||
colour: string
|
||||
txtColour: string
|
||||
coordinates: Point
|
||||
}
|
||||
|
||||
export type Revision = ItemAttributes & {
|
||||
body: string;
|
||||
fileName: string;
|
||||
@@ -26,6 +44,13 @@ export type Quadrant = {
|
||||
[name: string]: Item[];
|
||||
};
|
||||
|
||||
export type QuadrantConfig = {
|
||||
colour: string,
|
||||
txtColour: string,
|
||||
position: number,
|
||||
description: string
|
||||
}
|
||||
|
||||
export type Radar = {
|
||||
items: Item[];
|
||||
releases: string[];
|
||||
@@ -40,6 +65,11 @@ export const featuredOnly = (items: Item[]) =>
|
||||
export const nonFeaturedOnly = (items: Item[]) =>
|
||||
items.filter((item) => !item.featured);
|
||||
|
||||
export type Point = {
|
||||
x: number,
|
||||
y: number
|
||||
}
|
||||
|
||||
export const groupByQuadrants = (items: Item[]): Group =>
|
||||
items.reduce(
|
||||
(quadrants, item: Item) => ({
|
||||
|
||||
@@ -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