Parses graphs and other data from camelcamelcamel.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

166 lines
5.2 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. package main
  2. import (
  3. "fmt"
  4. "image"
  5. "math"
  6. "time"
  7. )
  8. func main() {
  9. // color checks for graphs generated by camelcamelcamel. Currently only supports
  10. // Amazon prices (green line).
  11. var (
  12. isLineColor = colorMatcher(99, 168, 94)
  13. isMaxLineColor = colorMatcher(194, 68, 68)
  14. isMinLineColor = colorMatcher(119, 195, 107) // MUST be different from line color or will get problems tracing
  15. isGridColor = colorMatcher(215, 215, 214)
  16. isBgColor = colorMatcher(255, 255, 254)
  17. )
  18. maxPrice, minPrice := 59.99, 9.99
  19. startDate, endDate := time.Date(2015, 10, 29, 0, 0, 0, 0, time.Local), time.Date(2019, 05, 15, 0, 0, 0, 0, time.Local)
  20. _, _, _, _, _, _, _, _, _ = isLineColor, isMaxLineColor, isMinLineColor, isGridColor, isBgColor, maxPrice, minPrice, startDate, endDate
  21. img := must(loadPNG("graph.png")).(image.Image)
  22. graphBounds := *getGraphBounds(img, isGridColor)
  23. priceExtrapolator := getPriceExtrapolator(img, graphBounds, maxPrice, minPrice, isMaxLineColor, isMinLineColor)
  24. dateExtrapolator := getDateExtrapolator(img, graphBounds, startDate, endDate)
  25. fmt.Println(graphBounds) // (54,16)-(15945,7951)
  26. fmt.Println(priceExtrapolator(3127)) // 39.99
  27. fmt.Println(priceExtrapolator(16)) // 59.99
  28. fmt.Println(priceExtrapolator(4682)) // 30.00
  29. fmt.Println(priceExtrapolator(6862)) // 15.99
  30. fmt.Println(priceExtrapolator(7795)) // 9.99
  31. fmt.Println(dateExtrapolator(55)) // Oct 29, 2015
  32. fmt.Println(dateExtrapolator(1636)) // Mar 6, 2016
  33. fmt.Println(dateExtrapolator(3230)) // Jul 14, 2016
  34. fmt.Println(dateExtrapolator(4824)) // Nov 20, 2016
  35. fmt.Println(dateExtrapolator(6418)) // Mar 30, 2017
  36. fmt.Println(dateExtrapolator(14387)) // Jan 8, 2019
  37. var curPrice float64
  38. var curStock bool
  39. if err := traceLine(img, graphBounds, isLineColor, func(x, y int, isDotted bool) {
  40. inStock, date, price := !isDotted, dateExtrapolator(x), priceExtrapolator(y)
  41. if curPrice == price && curStock == inStock {
  42. return
  43. }
  44. curPrice, curStock = price, inStock
  45. is := 1
  46. if inStock {
  47. is = 0
  48. }
  49. fmt.Printf("%d,%s,%.2f\n", is, date.Format("2006-01-02"), curPrice)
  50. }); err != nil {
  51. panic(err)
  52. }
  53. }
  54. // The following functions are calibrated for camelcamelcamel graphs with a plot
  55. // size of 2, and marker lines of 1. The graph should also have a border enclosing
  56. // the grid area. Dashed and dotted lines should have intervals of at most 2.
  57. // traceLine traces the grid line.
  58. func traceLine(img image.Image, graphBounds image.Rectangle, line colorMatcherFunc, cb func(x, y int, isDotted bool)) error {
  59. curDotted := false
  60. for x := graphBounds.Min.X; x < graphBounds.Max.X; x++ {
  61. curY := -1
  62. for y := graphBounds.Min.Y; y < graphBounds.Max.Y; y++ {
  63. onLine := line(img.At(x, y))
  64. if x > 2 && x < graphBounds.Max.X-2 {
  65. if line(img.At(x-2, y)) && line(img.At(x+2, y)) {
  66. if onLine {
  67. curDotted = false
  68. } else {
  69. onLine = true
  70. curDotted = true
  71. }
  72. }
  73. }
  74. if onLine {
  75. curY = y
  76. break
  77. }
  78. }
  79. if curY == -1 {
  80. if x < graphBounds.Max.X {
  81. return fmt.Errorf("line ended early at %d", x)
  82. }
  83. break
  84. }
  85. cb(x, curY, curDotted)
  86. }
  87. return nil
  88. }
  89. // getGraphBounds returns the detected graph bounds (based on lines of the grid
  90. // color at least 75% of the image width).
  91. func getGraphBounds(img image.Image, fn colorMatcherFunc) *image.Rectangle {
  92. var yTop, yBottom, xLeft, xRight int
  93. for y := 0; y < img.Bounds().Max.Y; y++ {
  94. if pctColor(img, fn, y, false) > 0.75 {
  95. yTop = y
  96. break
  97. }
  98. }
  99. for y := img.Bounds().Max.Y; y > 0; y-- {
  100. if pctColor(img, fn, y, false) > 0.75 {
  101. yBottom = y
  102. break
  103. }
  104. }
  105. for x := 0; x < img.Bounds().Max.X; x++ {
  106. if pctColor(img, fn, x, true) > 0.75 {
  107. xLeft = x
  108. break
  109. }
  110. }
  111. for x := img.Bounds().Max.X; x > 0; x-- {
  112. if pctColor(img, fn, x, true) > 0.75 {
  113. xRight = x
  114. break
  115. }
  116. }
  117. if yTop == 0 || yBottom == 0 || xLeft == 0 || xRight == 0 {
  118. return nil
  119. }
  120. r := image.Rect(xLeft, yTop, xRight, yBottom)
  121. return &r
  122. }
  123. // getPriceExtrapolator gets an extrapolator to extract the price from the image
  124. // based on a y value relative to the image (NOT the grid).
  125. func getPriceExtrapolator(img image.Image, graphBounds image.Rectangle, maxPrice, minPrice float64, maxColor, minColor colorMatcherFunc) func(y int) float64 {
  126. // dashed line, so threshold is 0.4; the line is on bottom of plotted line, so subtract 1.
  127. var maxY, minY int
  128. for y := graphBounds.Min.Y; y < graphBounds.Max.Y; y++ {
  129. if maxY == 0 && pctColor(img, maxColor, y, false) > 0.4 {
  130. maxY = y - 1
  131. } else if minY == 0 && pctColor(img, minColor, y, false) > 0.4 {
  132. minY = y - 1
  133. }
  134. }
  135. if minY < maxY {
  136. return nil
  137. }
  138. return func(y int) float64 {
  139. return math.Round(extrapolate(maxY, minY, maxPrice, minPrice, y)*100) / 100
  140. }
  141. }
  142. // getDateExtrapolator gets an extrapolator to extract the date from the image
  143. // based on an x value relative to the image (NOT the grid). It allows extrapolating
  144. // up to 100 days before or after the known range.
  145. func getDateExtrapolator(img image.Image, gridBounds image.Rectangle, startDate, endDate time.Time) func(x int) time.Time {
  146. startIndex, dates, endIndex := getDaysBetween(startDate, endDate, 100)
  147. return func(x int) time.Time {
  148. return dates[int(math.Round(extrapolate(gridBounds.Min.X+1, gridBounds.Max.X, float64(startIndex), float64(endIndex), x)))]
  149. }
  150. }