174 lines
4.2 KiB
Go
174 lines
4.2 KiB
Go
|
package graph
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"math"
|
||
|
|
||
|
"reichard.io/bbank/database"
|
||
|
)
|
||
|
|
||
|
type SVGGraphPoint struct {
|
||
|
X int
|
||
|
Y int
|
||
|
Size int
|
||
|
}
|
||
|
|
||
|
type SVGGraphData struct {
|
||
|
Height int
|
||
|
Width int
|
||
|
Offset int
|
||
|
LinePoints []SVGGraphPoint
|
||
|
BarPoints []SVGGraphPoint
|
||
|
BezierPath string
|
||
|
BezierFill string
|
||
|
}
|
||
|
|
||
|
type SVGBezierOpposedLine struct {
|
||
|
Length int
|
||
|
Angle int
|
||
|
}
|
||
|
|
||
|
func GetSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int) SVGGraphData {
|
||
|
// Static Padding
|
||
|
var padding int = 5
|
||
|
|
||
|
// Derive Height
|
||
|
var maxHeight int = 0
|
||
|
for _, item := range inputData {
|
||
|
if int(item.MinutesRead) > maxHeight {
|
||
|
maxHeight = int(item.MinutesRead)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Derive Block Offsets & Transformed Coordinates (Line & Bar)
|
||
|
var blockOffset int = int(math.Floor(float64(svgWidth) / float64(len(inputData))))
|
||
|
|
||
|
// Line & Bar Points
|
||
|
linePoints := []SVGGraphPoint{}
|
||
|
barPoints := []SVGGraphPoint{}
|
||
|
|
||
|
// Bezier Fill Coordinates (Max X, Min X, Max Y)
|
||
|
var maxBX int = 0
|
||
|
var maxBY int = 0
|
||
|
var minBX int = 0
|
||
|
for idx, item := range inputData {
|
||
|
itemSize := int(item.MinutesRead)
|
||
|
itemY := (maxHeight + padding) - itemSize
|
||
|
barPoints = append(barPoints, SVGGraphPoint{
|
||
|
X: (idx * blockOffset) + (blockOffset / 2),
|
||
|
Y: itemY,
|
||
|
Size: itemSize + padding,
|
||
|
})
|
||
|
|
||
|
lineX := (idx + 1) * blockOffset
|
||
|
linePoints = append(linePoints, SVGGraphPoint{
|
||
|
X: lineX,
|
||
|
Y: itemY,
|
||
|
Size: itemSize + padding,
|
||
|
})
|
||
|
|
||
|
if lineX > maxBX {
|
||
|
maxBX = lineX
|
||
|
}
|
||
|
|
||
|
if lineX < minBX {
|
||
|
minBX = lineX
|
||
|
}
|
||
|
|
||
|
if itemY > maxBY {
|
||
|
maxBY = itemY
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Return Data
|
||
|
return SVGGraphData{
|
||
|
Width: svgWidth + padding*2,
|
||
|
Height: maxHeight + padding*2,
|
||
|
Offset: blockOffset,
|
||
|
LinePoints: linePoints,
|
||
|
BarPoints: barPoints,
|
||
|
BezierPath: getSVGBezierPath(linePoints),
|
||
|
BezierFill: fmt.Sprintf("L %d,%d L %d,%d Z", maxBX, maxBY+padding, minBX, maxBY+padding),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func getSVGBezierOpposedLine(pointA SVGGraphPoint, pointB SVGGraphPoint) SVGBezierOpposedLine {
|
||
|
lengthX := float64(pointB.X - pointA.X)
|
||
|
lengthY := float64(pointB.Y - pointA.Y)
|
||
|
|
||
|
return SVGBezierOpposedLine{
|
||
|
Length: int(math.Sqrt(math.Pow(lengthX, 2) + math.Pow(lengthY, 2))),
|
||
|
Angle: int(math.Atan2(lengthY, lengthX)),
|
||
|
}
|
||
|
|
||
|
// length = Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
|
||
|
// angle = Math.atan2(lengthY, lengthX)
|
||
|
}
|
||
|
|
||
|
func getSVGBezierControlPoint(currentPoint *SVGGraphPoint, prevPoint *SVGGraphPoint, nextPoint *SVGGraphPoint, isReverse bool) SVGGraphPoint {
|
||
|
// First / Last Point
|
||
|
if prevPoint == nil {
|
||
|
prevPoint = currentPoint
|
||
|
}
|
||
|
if nextPoint == nil {
|
||
|
nextPoint = currentPoint
|
||
|
}
|
||
|
|
||
|
// Modifiers
|
||
|
var smoothingRatio float64 = 0.2
|
||
|
var directionModifier float64 = 0
|
||
|
if isReverse == true {
|
||
|
directionModifier = math.Pi
|
||
|
}
|
||
|
|
||
|
opposingLine := getSVGBezierOpposedLine(*prevPoint, *nextPoint)
|
||
|
var lineAngle float64 = float64(opposingLine.Angle) + directionModifier
|
||
|
var lineLength float64 = float64(opposingLine.Length) * smoothingRatio
|
||
|
|
||
|
// Calculate Control Point
|
||
|
return SVGGraphPoint{
|
||
|
X: currentPoint.X + int(math.Cos(float64(lineAngle))*lineLength),
|
||
|
Y: currentPoint.Y + int(math.Sin(float64(lineAngle))*lineLength),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func getSVGBezierCurve(point SVGGraphPoint, index int, allPoints []SVGGraphPoint) []SVGGraphPoint {
|
||
|
var pointMinusTwo *SVGGraphPoint
|
||
|
var pointMinusOne *SVGGraphPoint
|
||
|
var pointPlusOne *SVGGraphPoint
|
||
|
|
||
|
if index-2 >= 0 && index-2 < len(allPoints) {
|
||
|
pointMinusTwo = &allPoints[index-2]
|
||
|
}
|
||
|
if index-1 >= 0 && index-1 < len(allPoints) {
|
||
|
pointMinusOne = &allPoints[index-1]
|
||
|
}
|
||
|
if index+1 >= 0 && index+1 < len(allPoints) {
|
||
|
pointPlusOne = &allPoints[index+1]
|
||
|
}
|
||
|
|
||
|
startControlPoint := getSVGBezierControlPoint(pointMinusOne, pointMinusTwo, &point, false)
|
||
|
endControlPoint := getSVGBezierControlPoint(&point, pointMinusOne, pointPlusOne, true)
|
||
|
|
||
|
return []SVGGraphPoint{
|
||
|
startControlPoint,
|
||
|
endControlPoint,
|
||
|
point,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func getSVGBezierPath(allPoints []SVGGraphPoint) string {
|
||
|
var bezierSVGPath string = ""
|
||
|
|
||
|
for index, point := range allPoints {
|
||
|
if index == 0 {
|
||
|
bezierSVGPath += fmt.Sprintf("M %d,%d", point.X, point.Y)
|
||
|
} else {
|
||
|
newPoints := getSVGBezierCurve(point, index, allPoints)
|
||
|
bezierSVGPath += fmt.Sprintf(" C%d,%d %d,%d %d,%d", newPoints[0].X, newPoints[0].Y, newPoints[1].X, newPoints[1].Y, newPoints[2].X, newPoints[2].Y)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return bezierSVGPath
|
||
|
}
|