Files
ternary-graph-generator/src/Main.purs
2025-07-20 16:48:39 -07:00

167 lines
6.5 KiB
Plaintext

module Main where
import Prelude ( discard, class Monoid, class Semigroup, Unit, ($), (<<<), (<>), (*), (+), (-), (/))
import Data.Array ((!!), concat, cons, mapWithIndex, range)
import Data.Functor (map)
import Data.Int (toNumber)
import Data.Maybe
import Data.Number (cos, pi, sin)
import Data.Number.Format (toString)
import Data.List (fold)
import Data.Ord ((<))
import Data.Ring (negate)
import Data.Tuple.Nested (Tuple3, tuple3, get1, get2, get3)
import Effect (Effect)
import Effect.Console (log)
type Point =
{ x :: Number
, y :: Number
}
type Line =
{ start :: Point
, end :: Point
}
-- TODO: How to do more normal math expressions?
rotate :: Number -> Point -> Point
rotate angle { x: x, y: y} = {
x: x * (cos angle) - y * (sin angle),
y: x * (sin angle) + y * (cos angle)
}
transform :: Number -> Number -> Number -> Point -> Point
transform scale xOffset yOffset { x: x, y: y} = {
x: scale * x + xOffset,
y: scale * y + yOffset
}
rotateLine :: Number -> Line -> Line
rotateLine angle { start: p1, end: p2} = {
start: rotate angle p1,
end: rotate angle p2
}
transformLine :: Number -> Number -> Number -> Line -> Line
transformLine scale xOffset yOffset { start: start, end: end } = {
start : t start,
end : t end
}
where t = transform scale xOffset yOffset
-- How to handle the different coordinate systems?
-- These things should be generated in the basic coordinate system
-- * ticks via generateTicks
-- * axis labels
-- * axis lines
-- There should be a transform function that these should be passed through directly after generation
newtype XMLFragment = XMLFragment String
derive newtype instance xmlFragmentSemiGroup :: Semigroup XMLFragment
derive newtype instance xmlFragmentMonoid :: Monoid XMLFragment
-- TODO: Add style configuration
svgLine :: Line -> XMLFragment
svgLine { start: {x: x1, y: y1}, end: {x: x2, y: y2} } =
XMLFragment $ """<path style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
""" <> "d=\"M " <> (toString x1) <> "," <> (toString y1) <> " " <> (toString x2) <> "," <> (toString y2) <> "\"/>"
-- TODO: position at center of text
svgText :: String -> Number -> Point -> Number -> XMLFragment
svgText text fontSize { x: x, y: y } angle =
XMLFragment $ "<text xml:space=\"preserve\" style=\"font-size:" <> (toString fontSize) <> "px;line-height:131%;font-family:'Liberation sans';font-variant-position:super;fill:#000000;stroke:#000000;stroke-width:0.0999998;stroke-linecap:round;stop-color:#000000;fill-opacity:1\" "
<> "transform=\"translate(" <> (toString x) <> ", " <> (toString y) <> ") rotate(" <> (toString angle) <> ")\">" <> text <> "</text>"
-- TODO: Make axis tick size a parameter
getTick :: Number -> Int -> Int -> Line
getTick scale numTicks tickI =
{start: {x: x, y: -(0.5 + 5.0 / scale)}, end: {x: x, y: y}}
where
x = 2.0 * (sin (pi / 3.0)) * (toNumber tickI) / (toNumber numTicks) - (sin (pi / 3.0))
y = if tickI < numTicks / 2
then 1.0 + x * 1.5 / (sin (pi / 3.0))
else 1.0 - x * 1.5 / (sin (pi / 3.0))
getTicks :: Number -> Number -> Int -> Tuple3 (Array Line) (Array Line) (Array Line)
getTicks scale angle numTicks =
tuple3 axis1Lines axis2Lines axis3Lines
where
foo = map (getTick scale numTicks) (range 0 numTicks)
axis1Lines = map (rotateLine angle) foo
axis2Lines = map (rotateLine (2.0 * pi / 3.0)) axis1Lines
axis3Lines = map (rotateLine (2.0 * pi / 3.0)) axis2Lines
-- There's probably a better way to do this
unfragment :: XMLFragment -> String
unfragment (XMLFragment frag) = frag
ternaryGraphSvg :: Array XMLFragment -> String
ternaryGraphSvg fragments = """<?xml version="1.0" encoding="UTF-8"?>
<svg
width="150mm"
height="120mm"
viewBox="-50 -50 200 200"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:svg="http://www.w3.org/2000/svg">
""" <> unfragment (fold fragments) <> "</svg>"
axesPoints :: Number -> Tuple3 Point Point Point
axesPoints angle = tuple3
(rotate angle { x: 0.0, y: 1.0 })
(rotate angle { x: -(sin (2.0 * pi / 3.0)), y : -0.5})
(rotate angle { x: (sin (2.0 * pi / 3.0)), y: -0.5 })
axesPath :: Point -> Point -> Point -> XMLFragment
axesPath p1 p2 p3 = XMLFragment $ """<path style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:bevel;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1"
""" <> "d=\"M " <> (toString p1.x) <> "," <> (toString p1.y) <> " " <> (toString p2.x) <> "," <> (toString p2.y) <> " " <> (toString p3.x) <> "," <> (toString p3.y) <> " Z\"/>"
ternaryGraph :: Number -> Number -> Number -> Int -> String
ternaryGraph scale xOffset yOffset numTicks = ternaryGraphSvg fragments
where
axisTickLines = getTicks scale pi numTicks
axis1TickLines = map (transformLine scale xOffset yOffset) (get1 axisTickLines)
axis2TickLines = map (transformLine scale xOffset yOffset) (get2 axisTickLines)
axis3TickLines = map (transformLine scale xOffset yOffset) (get3 axisTickLines)
tickLines = concat [axis1TickLines, axis2TickLines, axis3TickLines]
tickLinesSvg = fold $ map svgLine tickLines
axesPointsPi = axesPoints pi
axisPoint1 = transform scale xOffset yOffset (get1 axesPointsPi)
axisPoint2 = transform scale xOffset yOffset (get2 axesPointsPi)
axisPoint3 = transform scale xOffset yOffset (get3 axesPointsPi)
axesPathSvg = axesPath axisPoint1 axisPoint2 axisPoint3
-- TODO: axis text size
axisTitlesSvg = [
svgText "axis 1" 12.0 (transform scale xOffset yOffset {x: 0.0, y: 1.2}) 0.0,
svgText "axis 2" 12.0 (transform scale xOffset yOffset {x: -(sin (2.0 * pi / 3.0)), y: -0.7}) (-60.0),
svgText "axis 3" 12.0 (transform scale xOffset yOffset {x: (sin (2.8 * pi / 3.0)), y: -0.7}) 60.0
]
axisTickStarts = map (\line -> line.start)
-- TODO: tick label size
-- TODO: axis tick label rotation
-- TODO: axis tick start number
axisTickLabels = \rotation -> mapWithIndex (\i point -> svgText ("E" <> (toString (toNumber i))) 10.0 point 0.0)
axis1TickLabels = axisTickLabels 0 $ axisTickStarts axis1TickLines
axis2TickLabels = axisTickLabels 0 $ axisTickStarts axis2TickLines
axis3TickLabels = axisTickLabels 0 $ axisTickStarts axis3TickLines
tickLabelsSvg = concat [
axis1TickLabels,
axis2TickLabels,
axis3TickLabels
]
fragments = concat [[tickLinesSvg, axesPathSvg], axisTitlesSvg, tickLabelsSvg]
main :: Effect Unit
main = do
log $ ternaryGraph 100.0 50.0 50.0 10