99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
import MaterialIcon from "@mui/material/Icon";
|
|
import * as d3 from "d3";
|
|
import { useEffect, useRef } from "react";
|
|
import { colorScheme, config } from "../config";
|
|
|
|
import { CategoryMetadata } from "../lib/metadata";
|
|
|
|
import "./Chart.css";
|
|
|
|
interface QuestionGroupChartProps {
|
|
question: string;
|
|
metadata?: CategoryMetadata;
|
|
groupData: { response: string }[];
|
|
responses: string[];
|
|
xScale: d3.ScaleBand<string>;
|
|
}
|
|
|
|
function makeDot(
|
|
g: d3.Selection<SVGGElement, unknown, null, undefined>,
|
|
dotX: number,
|
|
dotY: number
|
|
) {
|
|
const shape = config.dotShape;
|
|
if (shape === "circle") {
|
|
return g
|
|
.append("circle")
|
|
.attr("class", "dot")
|
|
.attr("cx", dotX)
|
|
.attr("cy", dotY)
|
|
.attr("r", config.dotRadius);
|
|
} else if (shape === "rect") {
|
|
return g
|
|
.append("rect")
|
|
.attr("class", "dot")
|
|
.attr("x", dotX - config.dotRadius)
|
|
.attr("y", dotY - config.dotRadius)
|
|
.attr("width", config.dotRadius * 2)
|
|
.attr("height", config.dotRadius * 2);
|
|
}
|
|
throw new Error(`Unsupported shape: ${shape}`);
|
|
}
|
|
|
|
export function QuestionGroupChart({
|
|
question,
|
|
metadata,
|
|
groupData,
|
|
responses,
|
|
xScale,
|
|
}: QuestionGroupChartProps) {
|
|
const svgRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
// Clear SVG
|
|
d3.select(svgRef.current).selectAll("*").remove();
|
|
|
|
// Group responses by category (within this group only)
|
|
const responseGroups = d3.group(groupData, (d) => d.response);
|
|
const chartHeight = config.chartHeight;
|
|
const chartWidth = xScale.range()[1];
|
|
|
|
const svg = d3
|
|
.select(svgRef.current)
|
|
.attr("width", chartWidth)
|
|
.attr("height", chartHeight);
|
|
const g = svg.append("g");
|
|
|
|
// Dots
|
|
responses.forEach((response) => {
|
|
const responseData = responseGroups.get(response) || [];
|
|
const x = xScale(response) || 0;
|
|
responseData.forEach((_, index) => {
|
|
const row = Math.floor(index / config.columnsPerGroup);
|
|
const col = index % config.columnsPerGroup;
|
|
const dotX =
|
|
x +
|
|
xScale.bandwidth() / 2 +
|
|
(col - (config.columnsPerGroup - 1) / 2) *
|
|
(config.dotRadius * 2 + config.dotSpacing);
|
|
const dotY =
|
|
chartHeight - (row + 1) * (config.dotRadius * 2 + config.dotSpacing);
|
|
|
|
makeDot(g, dotX, dotY).attr("fill", colorScheme[response] || "#666");
|
|
});
|
|
});
|
|
}, [groupData, responses, xScale, question]);
|
|
return (
|
|
<div className="question-group">
|
|
<svg ref={svgRef}></svg>
|
|
<div className="question-title">
|
|
{metadata?.icon && <MaterialIcon>{metadata.icon}</MaterialIcon>}
|
|
{question}
|
|
</div>
|
|
<p>{metadata?.description || question}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default QuestionGroupChart;
|