import { GradeDistribution, ObservatoryResult } from "../types";
import { formatMinus } from "../utils";
import "./index.scss";
export default function GradeSVG({
gradeDistribution,
result,
}: {
gradeDistribution: GradeDistribution[];
result: ObservatoryResult;
}) {
const width = 1200;
const height = 380;
const leftSpace = 100; // left edge to left edge of first bar
const rightSpace = 80; // right edge to tight edge of last bar
const bottomSpace = 60; // bottom edge to bottom edge of bars
const topSpace = 60; // top padding
const itemCount = gradeDistribution.length;
const barWidth = 60;
// The x-axis has the different grades from "A+" to "D-".
const xTickIncr =
(width - leftSpace - rightSpace - barWidth) / (itemCount - 1);
const xTickOffset = leftSpace + xTickIncr / 2;
// The y-axis has ticks according to the maximum value of all grades.
const yMarks = calculateTicks(gradeDistribution);
const yTickOffset = height - bottomSpace;
const yTickIncr = (height - bottomSpace - topSpace) / (yMarks.length - 1);
const yTickMax = Math.max(...yMarks);
return (
<>
Number of sites by grade
| Grade |
Sites |
{gradeDistribution.map((item, index) => (
|
{formatMinus(item.grade)}
{item.grade === result.scan.grade ? " (Current grade)" : ""}
|
{item.count} sites |
))}
>
);
}
/**
* Calculate
* @param {GradeDistribution[]} gradeDistribution
* @returns {number[]}
*/
function calculateTicks(gradeDistribution: GradeDistribution[]): number[] {
const maxValue = Math.max(...gradeDistribution.map((item) => item.count));
const tickTargetCount = 7; // Target number of ticks between 5 and 10
const range = rangeForValue(maxValue, false); // Get a nice range
const tickInterval = rangeForValue(range / tickTargetCount, true); // Determine a nice tick interval
const niceMaxValue = Math.ceil(maxValue / tickInterval) * tickInterval; // Adjust max value to a nice number
const tickCount = Math.ceil(niceMaxValue / tickInterval) + 1; // Calculate the number of ticks
const ticks: number[] = [];
for (let i = 0; i < tickCount; i++) {
ticks.push(i * tickInterval);
}
return ticks;
}
/**
* This returns values to construct proper axis measurements in
* diagrams. The returned value is 1|2|5 * 10^x.
*
* If `round` is `true`, the returned value can be also rounded down,
* useful for calculating ticks on an axis.
*
* Examples:
*
* |range |rounded=false|rounded=true|
* |---------|-------------|------------|
* | 1 | 1 | 1 |
* | 2 | 2 | 2 |
* | 3 | 5 | 5 |
* | 4 | 5 | 5 |
* | 5 | 5 | 5 |
* | 6 | 10 | 5 |
* | 7 | 10 | 10 |
* | 8 | 10 | 10 |
* | 9 | 10 | 10 |
* | 10 | 10 | 10 |
* | 34 | 50 | 50 |
* | 450 | 500 | 500 |
* | 560 | 1000 | 500 |
* | 6780 | 10000 | 5000 |
* | 10 | 10 | 10 |
* | 100 | 100 | 100 |
* | 1000 | 1000 | 1000 |
* | 10000 | 10000 | 10000 |
*
* @param {number} range The input value
* @param {boolean} round If false, the returned value will always be greater than `range`, otherwise it can be rounded off
* @returns {number} a number according to `1|2|5 * 10^x`, where x is derived from `range` to be in the same order of magnitude
*/
function rangeForValue(range: number, round: boolean): number {
const exponent = Math.floor(Math.log10(range));
const fraction = range / Math.pow(10, exponent);
let niceFraction: number;
if (round) {
if (fraction < 1.5) {
niceFraction = 1;
} else if (fraction < 3) {
niceFraction = 2;
} else if (fraction < 7) {
niceFraction = 5;
} else {
niceFraction = 10;
}
} else {
if (fraction <= 1) {
niceFraction = 1;
} else if (fraction <= 2) {
niceFraction = 2;
} else if (fraction <= 5) {
niceFraction = 5;
} else {
niceFraction = 10;
}
}
return niceFraction * Math.pow(10, exponent);
}