commit c239a3998e2a80e0694dcc0fef868ff878c11454
parent 1d5b7631571ea6d474bb48d99df9c4a507439362
Author: hrryt <harry@mayesfield.uk>
Date: Fri, 7 Aug 2026 20:53:42 +0100
Add SVG graphing
Diffstat:
10 files changed, 152 insertions(+), 29 deletions(-)
diff --git a/TODO.md b/TODO.md
@@ -0,0 +1,9 @@
+# General
+- Use KaTeX_Math font outside MathML
+
+# Questions
+- Improve existing questions
+ - Negative b toggle
+- Add more questions
+ - Add graphing questions
+- Drop-down for finer controls
diff --git a/src/components/Equation.jsx b/src/components/Equation.jsx
@@ -8,5 +8,5 @@ export default function Equation({ equation, displayMode = false }) {
render(equation, ref.current, { displayMode: displayMode });
}, [equation]);
- return <div ref={ref} >{equation}</div>;
+ return <div ref={ref}>{equation}</div>;
}
diff --git a/src/components/Graph.jsx b/src/components/Graph.jsx
@@ -1,13 +0,0 @@
-import { useRef, useEffect } from 'preact/hooks';
-import Plane from '../scripts/Plane.js';
-
-export default function Graph({ procedure }) {
- const ref = useRef(null);
-
- useEffect(() => {
- const plane = new Plane(ref.current);
- procedure(plane)
- }, [procedure]);
-
- return <canvas ref={ref} width="150" height="150"/>;
-}
diff --git a/src/components/graphing/Graph.jsx b/src/components/graphing/Graph.jsx
@@ -0,0 +1,66 @@
+import Label from './Label.jsx';
+
+function Grid({ viewBox }) {
+ const [ minX, minY, width, height ] = viewBox;
+ const d = `M ${minX} ${minY} h ${width} ${`m -${width} 1 h ${width}`.repeat(height)} v -${height} ${`m -1 ${height} v -${height}`.repeat(width)}`;
+ return <path id="grid" d={d} />;
+}
+
+function Axes({ viewBox }) {
+ const [ minX, minY, width, height ] = viewBox;
+ return (
+ <g>
+ <path id="axes" d={`M ${minX} 0 h ${width-.1}`} marker-end="url(#arrow)"/>
+ <path id="axes" d={`M 0 ${minY} v ${height-.1}`} marker-end="url(#arrow)"/>
+ </g>
+ );
+}
+
+function AxisTicks({ viewBox }) {
+ const [ minX, minY, width, height ] = viewBox;
+ return (
+ <g>
+ <g>
+ {Array.from({ length: width - 1 }).map((_, i) => {
+ const x = i + minX + 1;
+ if (x == 0) { return; }
+ return <Label x={x} y="0" position="bottom" d={.1}>{x}</Label>;
+ })}
+ </g>
+ <g>
+ {Array.from({ length: height - 1 }).map((_, i) => {
+ const y = i + minY + 1;
+ if (y == 0) { return; }
+ return <Label x="0" y={y} position="left" d={.1}>{y}</Label>;
+ })}
+ </g>
+ <Label x="0" y="0" position="bottom-left" d={.1}>0</Label>
+ </g>
+ )
+}
+
+export default function Graph({ children, width="500", height="500", viewBox=[-5, -5, 10, 10] }) {
+ return (
+ <svg
+ width={width} height={height} viewBox={viewBox.join(' ')}
+ transform="scale(1 -1)" xmlns="http://www.w3.org/2000/svg"
+ >
+ <defs>
+ <marker
+ id='arrow'
+ viewBox="0 0 4 4"
+ refX="3" refY="2"
+ markerWidth="4" markerHeight="4"
+ markerUnits="strokeWidth"
+ orient="auto"
+ >
+ <path d="M 0 0 L 4 2 L 0 4 z" id='arrowhead' />
+ </marker>
+ </defs>
+ <Grid viewBox={viewBox} />
+ <Axes viewBox={viewBox} />
+ {children}
+ <AxisTicks viewBox={viewBox} />
+ </svg>
+ );
+}
diff --git a/src/components/graphing/Label.jsx b/src/components/graphing/Label.jsx
@@ -0,0 +1,27 @@
+function getPosition(position) {
+ switch (position) {
+ case 'left':
+ return ['central', 'end', -1, 0];
+ case 'right':
+ return ['central', 'start', 1, 0];
+ case 'top':
+ return ['auto', 'middle', 0, 1];
+ case 'bottom':
+ return ['hanging', 'middle', 0, -1];
+ case 'bottom-left':
+ return ['hanging', 'end', -1, -1];
+ }
+}
+
+export default function Label({ x, y, position, d=.15, children }) {
+ const [dominantBaseline, textAnchor, dx, dy] = getPosition(position);
+ return (
+ <text
+ x={x} y={-y} dx={d*dx} dy={-d*dy} transform="scale(1 -1)"
+ dominant-baseline={dominantBaseline}
+ text-anchor={textAnchor}
+ >
+ {children}
+ </text>
+ );
+}
diff --git a/src/components/graphing/Quadratic.jsx b/src/components/graphing/Quadratic.jsx
@@ -0,0 +1,3 @@
+export default function Quadratic() {
+ return <path d="M -3 9 Q 0 -9 3 9" />;
+}
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
@@ -1,12 +1,21 @@
import Window from '../components/Window.jsx';
-import Graph from '../components/Graph.jsx';
+import Graph from '../components/graphing/Graph.jsx';
+import Quadratic from '../components/graphing/Quadratic.jsx';
+import Label from '../components/graphing/Label.jsx';
export default function Home() {
return (
<main>
<Window title="Home">
<p>Hello World!</p>
- <Graph procedure={p => p.draw_test()}/>
+ <div class="field-border kern">
+ <Graph viewBox={[-5, -1, 7, 6]}>
+ <Quadratic />
+ <Label x="-2" y="4" position="left">x²</Label>
+ <polygon points="1,2 -1,1, -4,3" />
+ <Label x="-4" y="3" position="top">(-4, 3)</Label>
+ </Graph>
+ </div>
</Window>
</main>
);
diff --git a/src/scripts/Plane.js b/src/scripts/Plane.js
@@ -1,13 +0,0 @@
-export default class Plane {
- constructor(canvas) {
- this.ctx = canvas.getContext("2d");
- }
-
- draw_test() {
- this.ctx.fillStyle = "rgb(200 0 0)";
- this.ctx.fillRect(10, 10, 50, 50);
-
- this.ctx.fillStyle = "rgb(0 0 200 / 50%)";
- this.ctx.fillRect(30, 30, 50, 50);
- }
-}
diff --git a/src/styles/index.css b/src/styles/index.css
@@ -1,3 +1,4 @@
@import 'kern-typ/kern.css';
@import '98.css';
@import './main.css';
+@import './svg.css';
diff --git a/src/styles/svg.css b/src/styles/svg.css
@@ -0,0 +1,34 @@
+path,polygon,circle,ellipse,line,polyline {
+ stroke: #066;
+ stroke-width: 0.06;
+ stroke-linejoin: round;
+ stroke-linecap: round;
+ fill: transparent;
+}
+
+polygon {
+ fill: #0668;
+}
+
+path#grid {
+ stroke: black;
+ stroke-width: 0.01;
+}
+
+path#axes {
+ stroke: black;
+ stroke-width: 0.08;
+ stroke-linecap: butt;
+}
+
+path#arrowhead {
+ stroke: black;
+ fill: black;
+}
+
+text {
+ font-size: 0.02em;
+ stroke: white;
+ stroke-width: 0.02;
+ paint-order: stroke;
+}