You ship a pie chart that looks fine in the browser. The slices line up, the labels sit where you expect, and the customer demo passes. Two weeks later a teammate opens the file in Safari, and three slices collapse into one wedge. The root cause is almost always the same: someone handed a function an angle in the wrong unit, or used a function that quietly assumed radians while the caller passed degrees. Angle conversion bugs are quiet because they don't throw exceptions — they just produce geometry that's slightly wrong, and only sometimes.
This article is the checklist I wish I had at the start of every project that draws arcs, rotates elements, or animates anything that spins. It focuses on the engineering workflow — where unit mistakes enter the code, how to catch them in review and CI, and what to standardise so the bug class disappears rather than gets patched each time it appears.
The Three Frontend Surfaces Where Angle Units Bite
Frontend code touches angles in three very different surfaces, and each one has its own default. Knowing which is which is half the battle.
Canvas and SVG geometry. The CanvasRenderingContext2D.arc() method takes radians, and so does CanvasRenderingContext2D.rotate(). SVG attribute transform="rotate(...)" also expects degrees, but getPointAtLength() on a path works in the path's user units — which is whatever the path was authored with. Mixing those up is the single most common cause of "the arc starts in the wrong place" tickets. The Canvas 2D API documentation is explicit about radians and is worth bookmarking for any reviewer: CanvasRenderingContext2D.arc() — MDN.
CSS transforms and Web Animations. CSS uses degrees in transform: rotate(45deg) and in rotate individual transform properties. The CSS Values and Units specification defines the <angle> type with both deg and rad accepted in modern browsers, but in practice most authoring tools and design specs use degrees. The CSS Values and Units Level 4 specification is the canonical reference for which units are valid where.
Geometry libraries and game engines. Three.js uses radians for everything — THREE.MathUtils.degToRad exists precisely because so many people get this wrong. So does anything that wraps GL math. If you're importing a DWG floorplan or a DICOM slice, the source unit is whatever the format mandated; the conversion happens once at the boundary.
The QA implication is simple: every one of these surfaces needs its own guard, because a helper that protects Canvas calls won't catch the CSS case, and vice versa.
A Five-Point Review Checklist for Any PR That Draws Arcs
Use this list when reviewing a pull request that adds or modifies drawing code, animation, or layout that depends on rotation. It takes about a minute per file and catches most unit mistakes before they reach main.
-
Search the diff for
Math.PI,* 180,* 360, and/ 57.29. These are the fingerprints of someone converting in one direction and possibly the wrong one. Every occurrence should have an adjacent comment explaining which surface it targets. - Confirm a single source of truth. Either the codebase speaks degrees end-to-end and converts at the Canvas/Three.js boundary, or it speaks radians and converts at the design-import boundary. Pick one and enforce it in lint rules.
-
Check the literal units on every angle constant. A
const SLICE_ANGLE = 0.5236is suspicious because it doesn't say what unit it is. Aconst SLICE_ANGLE_DEG = 30is honest. Bare numbers around geometry are an anti-pattern. - Trace one arc from input to pixels. Pick the most visually central element — usually the first slice — and follow the value from the data source, through any conversion, into the draw call. Every step should be readable by someone who has never seen the file.
-
Render the same scene with deliberately wrong unit inputs. A test that passes
-30instead of30should produce a visually different but still valid result; a test that breaks entirely usually means you accidentally double-converted.
Writing Conversion Helpers That Can't Be Misused
The cheapest defence against unit bugs is making the wrong call impossible to write. Three small habits do most of the work.
Name units into the type. A function called drawSlice(angle) is dangerous because the reader has to remember what unit angle is in. drawSlice(angleRad) and drawSliceDeg(angleDeg) make the unit part of the signature. The compiler can't help you in JavaScript, but your IDE and your reviewers can.
Convert at boundaries, not at call sites. If you receive data in degrees from a backend, convert to radians in the deserialiser or in a single toSceneUnits() call. Don't sprinkle * Math.PI / 180 across twelve draw functions. The fewer conversion sites, the fewer places a bug can live.
Expose the conversion factor as a named constant. const RAD_PER_DEG = Math.PI / 180; at the top of a geometry file is clearer than the literal, and it makes code review faster because the reader doesn't have to mentally compute the conversion to know if a number is plausible. A value like 1.7453292519943295 is meaningless; a value multiplied by RAD_PER_DEG to produce 0.5235987755982988 is recognisable as 30 degrees.
For one-off conversions during a debugging session — "what is 137.5° in radians for this Bezier control point?" — a dedicated tool saves a lot of context switching. The walkthrough at How to Convert Degrees to Radians in Seconds is a good reference when you need the conversion factor handy rather than re-deriving it.
Debugging a "Rotated By a Tiny Bit Wrong" Bug
When the symptom is "everything looks approximately right but slightly off," the investigation is different from a "nothing renders" bug. Here's the workflow I use.
Start by rendering a known reference: a full circle drawn as a single arc, a square rotated by exactly 90°, and a 0° element that should be identical to the unrotated one. If any of those are wrong, the bug is in the unit pipeline, not in the data. If they're correct, the bug is in how a specific value is being passed.
Next, compare the visual offset against the unit size. A 30° slice rendered as roughly 29.14° is suspicious because 30 * π/180 ≈ 0.5236 and 30 * 0.0174 ≈ 0.522 — close, but not identical, and a sign that someone used a degree-to-radian approximation that drifted. A 30° slice rendered as something tiny suggests the value was converted twice. The arithmetic of the offset usually tells you which direction the conversion went wrong.
Finally, log the value at every boundary. Add console.assert(typeof angle === 'number' && angle >= 0 && angle <= 2 * Math.PI) in the draw path. Adding assertion-style logging at the boundary between "user data" and "rendering units" is the most reliable place to catch a leak.
When To Standardise on Radians vs Degrees Across a Team
Most teams end up arguing about this. The argument is usually about taste, but there is a defensible default that depends on what you build.
If your product is design-tool heavy — a Figma plugin, a whiteboard, a chart builder — author and store in degrees. Designers think in degrees, your data sources speak degrees, and the conversion to radians is a one-line adapter at the Canvas boundary. This is also the path of least resistance for any code that has to round-trip with CSS or SVG attributes.
If your product is a game, a simulation, or anything using Three.js, WebGL, or a physics engine, author and store in radians. Every library in that stack already speaks radians, and forcing degrees through the pipeline means every vector math call has a hidden conversion overhead — both cognitive and, sometimes, measurable.
Either choice is fine. Mixing the two inside one codebase is the actual problem. Pick a default, document it in a CONTRIBUTING.md, and add an ESLint rule that flags the other unit appearing in a file. A rule as simple as no-restricted-syntax matching Math.PI outside the geometry module catches the most common mistake for free.
Frequently asked questions
What's the fastest way to verify a suspect angle conversion?
Render three test cases: a 0° rotation (must look identical to no rotation), a 90° rotation (must look identical to a known orthogonal flip), and a 360° rotation (must look identical to 0°). If any of those fail, the conversion is wrong; if they pass and a specific value still looks off, the bug is in how that specific value reached the draw call.
Should I store angles as degrees or radians in my database or JSON payload?
Store the unit that matches your domain. For most B2B products, that's degrees, because that's what your specs, your customers, and your import formats speak. Convert to radians at the rendering boundary, and keep the conversion in exactly one place.
Is Math.PI / 180 ever wrong?
No, but it's easy to type 180 / Math.PI by accident, which is the degrees-to-radians mistake reversed. Both compile and both run, so the only defence is naming the constant — RAD_PER_DEG versus DEG_PER_RAD — and never inlining the conversion. If you find yourself writing the literal division in code, stop and add a named constant instead.
How do I prevent regressions after I fix the bug?
Add a visual regression test that snapshots a few representative frames: a full pie chart, a 90° rotated card, and an animation at its midpoint. Any future unit regression will change those snapshots, and the test will fail loudly rather than letting the bug ship silently.
This article was drafted with AI assistance and reviewed for technical accuracy before publishing.
Top comments (0)