First stab at the radar chart

This commit is contained in:
Jarosław Marek
2021-04-28 00:33:49 +12:00
parent e8381eb332
commit ad4c8475f5
13 changed files with 745 additions and 12 deletions

View File

@@ -0,0 +1,28 @@
import ReactFauxDOM from 'react-faux-dom';
import * as d3 from 'd3';
export const LeftAxis = ({ scale }) => {
const el = ReactFauxDOM.createElement('g');
const axisGenerator = d3.axisLeft(scale).ticks(6);
d3.select(el).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());
return el.toReact();
};
export const BottomAxis = ({ scale }) => {
const el = ReactFauxDOM.createElement('g');
const axisGenerator = d3.axisBottom(scale).ticks(6);
d3.select(el).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());
return el.toReact();
};

View File

@@ -0,0 +1,66 @@
import ReactFauxDOM from 'react-faux-dom';
import * as d3 from "d3";
import { quadrantsMap, ringsMap } from '../../config';
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;
return {
x: xScale(Math.cos(randomDegree + shift) * r),
y: yScale(Math.sin(randomDegree + shift) * r)
};
};
const distanceBetweenPoints = (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}) {
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 point;
let counter = 1;
do {
point = generateCoordinates(blip, 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);
blip.x = point.x;
blip.y = point.y;
list.push(blip);
return list;
}, []);
const el = ReactFauxDOM.createElement('g');
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)
return el.toReact();
}

View File

@@ -0,0 +1,74 @@
import React from 'react';
import * as d3 from 'd3';
import { chartConfig } from '../../config';
const size = chartConfig.canvasSize / 2;
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),
startAngle,
endAngle
});
}
export default function QuadrantRings ({ quadrant }) {
// order from top left 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}
];
const gradientId = `${quadrant.position}-radial-gradient`;
return (
<g>
{/* 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={size}
height={size}
x={gradientAttributes[quadrant.position - 1].x}
y={gradientAttributes[quadrant.position - 1].y}
fill={`url(#${gradientId})`}
style={{opacity: 0.5}}
/>
{/* Rings' arcs */}
{[1, 2, 3, 4].map((ringPosition, index) => (
<path
key={index}
fill={quadrant.colour}
d={arcPath(quadrant.position, ringPosition)}
style={{transform: `translate(${size}px, ${size}px)`}}
/>
))}
</g>
);
}

View File

@@ -0,0 +1,60 @@
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 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;
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>
);
};
export default function RadarChart({ blips }) {
const xScale = d3.scaleLinear()
.domain([-4, 4])
.range([0, chartConfig.canvasSize]);
const yScale = d3.scaleLinear()
.domain([-4, 4])
.range([chartConfig.canvasSize, 0]);
return (
<div className="chart">
<svg viewBox={`0 0 ${chartConfig.size} ${chartConfig.size}`}>
<g transform={`translate(${chartConfig.margin}, ${chartConfig.margin})`}>
<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]} />
))}
{Object.keys(ringsMap).map((id, index) => (
<RingLabel key={index} ring={ringsMap[id]} />
))}
<BlipPoints blips={blips} xScale={xScale} yScale={yScale}/>
</g>
</svg>
</div>
);
}

View File

@@ -0,0 +1,5 @@
.chart {
fill: white;
font-size: 12px;
text-align: center;
}