158 lines
4.6 KiB
Go
158 lines
4.6 KiB
Go
package mujica
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// paint draws axes, the window's candles, layers and the crosshair at active.
|
|
func (m *mktCore) paint(p *ui.Painter, r ui.Rect, active int) {
|
|
g := m.geom(r)
|
|
k := m.k
|
|
size := FontSize(m.c, theme.CaptionSize)
|
|
label := func(v float64) string {
|
|
if m.base != 0 {
|
|
return internal.FormatTick(v, g.step) + "%"
|
|
}
|
|
return internal.FormatTick(v, g.step)
|
|
}
|
|
internal.AxisY(p, g.plot, g.ys, g.ticks, label, size, k)
|
|
m.paintTimeAxis(p, g, size)
|
|
p.Clip(g.plot, 0, func() {
|
|
m.st.drawn = m.paintCandles(p, g)
|
|
for i, l := range m.overlays {
|
|
mktPaintLine(p, g, m, l.values, l.color, i)
|
|
}
|
|
m.paintAlerts(p, g, size)
|
|
if d := m.opts.Drawings; d != nil {
|
|
d.paint(p, g, m, size)
|
|
}
|
|
})
|
|
at := active
|
|
if at < 0 && m.opts.Sync != nil {
|
|
at = m.opts.Sync.cursorIndex(m.opts.SyncID, m.candles)
|
|
}
|
|
if at >= m.from && at < m.to {
|
|
x := internal.Snap(p, g.x(m, at))
|
|
y := internal.Snap(p, g.ys.Map(m.val(m.candles[at].Close)))
|
|
p.Line(x, g.plot.Y, x, g.plot.Y+g.plot.H, 1, k.TextMuted)
|
|
p.Line(g.plot.X, y, g.plot.X+g.plot.W, y, 1, k.TextMuted.Alpha(0.6))
|
|
mktTag(p, g.plot.X+g.plot.W, y, label(m.val(m.candles[at].Close)), size, k.Text, k.Surface)
|
|
}
|
|
}
|
|
|
|
// paintTimeAxis labels candles about 90 DIPs apart.
|
|
func (m *mktCore) paintTimeAxis(p *ui.Painter, g mktGeom, size float32) {
|
|
every := max(1, int(math.Ceil(float64(90/g.band.Step()))))
|
|
layout := "Jan 2"
|
|
if m.to-m.from > 1 && m.candles[m.from+1].Time.Sub(m.candles[m.from].Time) < 24*time.Hour {
|
|
layout = "15:04"
|
|
}
|
|
var xs []float32
|
|
var labels []string
|
|
for i := m.from; i < m.to; i += every {
|
|
xs = append(xs, g.x(m, i))
|
|
labels = append(labels, m.candles[i].Time.Format(layout))
|
|
}
|
|
internal.AxisX(p, g.plot, xs, labels, size, m.k)
|
|
}
|
|
|
|
// paintCandles draws the window as the chart's type and returns how many candles it drew.
|
|
func (m *mktCore) paintCandles(p *ui.Painter, g mktGeom) int {
|
|
k := m.k
|
|
w := max(1, g.band.Width())
|
|
if m.opts.Type == ChartLine || m.opts.Type == ChartArea {
|
|
closes := make([]float64, len(m.candles))
|
|
for i := m.from; i < m.to; i++ {
|
|
closes[i] = m.val(m.candles[i].Close)
|
|
}
|
|
var line, fill ui.Path
|
|
for i := m.from; i < m.to; i++ {
|
|
x, y := internal.Snap(p, g.x(m, i)), internal.Snap(p, g.ys.Map(closes[i]))
|
|
if i == m.from {
|
|
line.MoveTo(x, y)
|
|
fill.MoveTo(x, g.plot.Y+g.plot.H).LineTo(x, y)
|
|
} else {
|
|
line.LineTo(x, y)
|
|
fill.LineTo(x, y)
|
|
}
|
|
}
|
|
if m.opts.Type == ChartArea {
|
|
fill.LineTo(internal.Snap(p, g.x(m, m.to-1)), g.plot.Y+g.plot.H).Close()
|
|
p.FillPath(&fill, k.AccentText.Alpha(0.18))
|
|
}
|
|
p.StrokePath(&line, 2, k.AccentText)
|
|
return m.to - m.from
|
|
}
|
|
for i := m.from; i < m.to; i++ {
|
|
c := m.candles[i]
|
|
up := c.Close >= c.Open
|
|
col := trendColor(m.c, 1)
|
|
if !up {
|
|
col = trendColor(m.c, -1)
|
|
}
|
|
x := internal.Snap(p, g.x(m, i))
|
|
yo, yc := g.ys.Map(m.val(c.Open)), g.ys.Map(m.val(c.Close))
|
|
yh, yl := g.ys.Map(m.val(c.High)), g.ys.Map(m.val(c.Low))
|
|
if m.opts.Type == ChartOHLC {
|
|
p.Line(x, yh, x, yl, 1.5, col)
|
|
p.Line(x-w/2, yo, x, yo, 1.5, col)
|
|
p.Line(x, yc, x+w/2, yc, 1.5, col)
|
|
continue
|
|
}
|
|
top, bot := min(yo, yc), max(yo, yc)
|
|
body := ui.Rect{X: x - w/2, Y: top, W: w, H: max(1, bot-top)}
|
|
p.Line(x, yh, x, top, 1, col)
|
|
p.Line(x, bot, x, yl, 1, col)
|
|
// Rises are hollow and falls filled, so direction reads without color.
|
|
if up && w >= 3 {
|
|
p.Fill(body, k.Surface, 0)
|
|
p.Stroke(body, col, 0, 1)
|
|
} else {
|
|
p.Fill(body, col, 0)
|
|
}
|
|
}
|
|
return m.to - m.from
|
|
}
|
|
|
|
// mktPaintLine draws values over the window with sparse markers of shape.
|
|
func mktPaintLine(p *ui.Painter, g mktGeom, m *mktCore, values []float64, col ui.Color, shape int) {
|
|
var path ui.Path
|
|
open := false
|
|
for i := m.from; i < m.to; i++ {
|
|
v := values[i]
|
|
if math.IsNaN(v) {
|
|
open = false
|
|
continue
|
|
}
|
|
x, y := internal.Snap(p, g.x(m, i)), internal.Snap(p, g.ys.Map(v))
|
|
if open {
|
|
path.LineTo(x, y)
|
|
} else {
|
|
path.MoveTo(x, y)
|
|
open = true
|
|
}
|
|
}
|
|
p.StrokePath(&path, 1.5, col)
|
|
every := max(1, int(60/max(g.band.Step(), 0.1)))
|
|
for i := m.from + shape%every; i < m.to; i += every {
|
|
if v := values[i]; !math.IsNaN(v) {
|
|
internal.Marker(p, shape, g.x(m, i), g.ys.Map(v), 6, col)
|
|
}
|
|
}
|
|
}
|
|
|
|
// mktTag draws a price label at the plot's right edge, centered on y.
|
|
func mktTag(p *ui.Painter, right, y float32, s string, size float32, fg, bg ui.Color) {
|
|
w, h := p.MeasureText(0, ui.Span{Text: s, Size: size})
|
|
b := ui.Rect{X: right - w - 8, Y: y - h/2 - 2, W: w + 8, H: h + 4}
|
|
p.Fill(b, bg, theme.ControlRadius)
|
|
p.Stroke(b, fg, theme.ControlRadius, 1)
|
|
p.Text(b.X+4, b.Y+2, s, size, fg)
|
|
}
|