#include <u.h>#include <libc.h>#include <bio.h>#include <thread.h>#include <draw.h>#include <mouse.h>#include <keyboard.h>#include "a.h" enum{ BACK, TEXT, AXIS, DIVS, LINE, BULL, BEAR, NCOLS,}; enum{ Padding = 4,}; Chart chart;char *title = "^FCHI";Mousectl *mc;Keyboardctl *kc;Image *cols[NCOLS];Font *afont;Rectangle chartr;Rectangle arear;Point pmin;Point pmax;Point pt;Point pl;int offset;int maxprices; #define MIN(x,y) ((x) < (y) ? (x) : (y))#define MAX(x,y) ((x) > (y) ? (x) : (y)) doubleparsedouble(char *s){ char *e; double d; d = strtod(s, &e); if(e == nil || e == s) return -1; return d;} voidloadprices(Chart *chart, int fd){ Biobuf *bp; char *s, *f[5]; int n, l; Price *price; bp = Bfdopen(fd, OREAD); if(bp == nil) sysfatal("Bfdopen: %r"); chart->ymin = 1000000000; chart->ymax = 0.0; for(l = 0; ;l++){ s = Brdstr(bp, '\n', 1); if(s == nil) break; n = getfields(s, f, nelem(f), 0, ","); if(n != 5){ fprint(2, "invalid input at line %d\n", l+1); continue; } price = &chart->prices[chart->nprices]; if(tmparse(&price->date, "YYYY-MM-DD", f[0], nil, nil) == nil){ fprint(2, "invalid date '%s' at line %d\n", f[0], l+1); continue; } price->open = parsedouble(f[1]); price->high = parsedouble(f[2]); price->low = parsedouble(f[3]); price->close = parsedouble(f[4]); chart->ymin = MIN(chart->ymin, price->low); chart->ymax = MAX(chart->ymax, price->high); chart->nprices += 1; } if(chart->nprices == 0) sysfatal("could not parse input");} Pointstringdbl(Image *b, Point p, Image *c, Point sp, Font *f, double d){ char buf[32] = {0}; snprint(buf, sizeof buf, "%f", d); return string(b, p, c, sp, f, buf);} voiddrawlegend(Price *price){ char buf[255] = {0}; Point p; Image *c; p = Pt(chartr.max.x, chartr.min.y - 1); draw(screen, Rpt(pl, p), cols[BACK], nil, ZP); if(price == nil){ flushimage(display, 1); return; } c = price->open < price->close ? cols[BULL] : cols[BEAR]; p = pl; if(title != nil) p = string(screen, p, cols[TEXT], ZP, font, " - "); snprint(buf, sizeof buf, "%τ", tmfmt(&price->date, "DD/MM/YYYY")); p = string(screen, p, cols[TEXT], ZP, font, buf); p = string(screen, p, cols[TEXT], ZP, font, " O:"); p = stringdbl(screen, p, c, ZP, font, price->open); p = string(screen, p, cols[TEXT], ZP, font, " H:"); p = stringdbl(screen, p, c, ZP, font, price->high); p = string(screen, p, cols[TEXT], ZP, font, " L:"); p = stringdbl(screen, p, c, ZP, font, price->low); p = string(screen, p, cols[TEXT], ZP, font, " C:"); p = stringdbl(screen, p, c, ZP, font, price->close); flushimage(display, 1);} voiddrawyaxis(void){ char buf[32] = {0}; Point p, q; int i, n; double v, dv; border(screen, arear, 1, cols[AXIS], ZP); n = (Dy(arear)-4*Padding) / 10; dv = (chart.ymax - chart.ymin) / 10; p = addpt(arear.min, Pt(1, 2*Padding)); q = addpt(p, Pt(Dx(arear) - 2, 0)); pmax = q; for(i = 0; i <= 10; i++){ line(screen, p, q, 0, 0, 0, cols[DIVS], ZP); line(screen, addpt(q, Pt(1,0)), addpt(q, Pt(5,0)), 0, 0, 0, cols[AXIS], ZP); if(i == 10) v = chart.ymin; else v = chart.ymax - i*dv; snprint(buf, sizeof buf, "%f", v); string(screen, addpt(q, Pt(5+Padding, -font->height/3)), cols[AXIS], ZP, afont, buf); p.y += n; q.y += n; } pmin = subpt(q, Pt(0, n));} voiddrawxtick(Price *price, int x){ Point p, q; char buf[12] = {0}; int w; p = Pt(x, arear.min.y + 1); q = Pt(x, arear.max.y - 1); line(screen, p, q, 0, 0, 0, cols[DIVS], ZP); line(screen, addpt(q, Pt(0,1)), addpt(q, Pt(0,5)), 0, 0, 0, cols[AXIS], ZP); snprint(buf, sizeof buf, "%τ", tmfmt(&price->date, "DD/MM/YYYY")); w = stringwidth(afont, buf); string(screen, addpt(q, Pt(-w/2, 5+Padding)), cols[AXIS], ZP, afont, buf);} intpointy(double v){ int y; double Δv, Δp, Δy; Δv = v - chart.ymin; Δp = pmax.y - pmin.y; Δy = chart.ymax - chart.ymin; y = pmin.y + Δp * (Δv / Δy); return y;} voidredraw(void){ Point open, high, low, close; Image *c; Price *price; int i, x; draw(screen, screen->r, cols[BACK], nil, ZP); if(title != nil) pl = string(screen, pt, cols[TEXT], ZP, font, title); else pl = pt; drawyaxis(); for(i = offset; i-offset < maxprices-1 && i < chart.nprices; i++){ price = &chart.prices[i]; c = price->open < price->close ? cols[BULL] : cols[BEAR]; x = arear.min.x + 6*(i-offset+1) + 2; if(x+4 >= arear.max.x){ fprint(2, "should not happen\n"); break; } open = Pt(x, pointy(price->open)); high = Pt(x, pointy(price->high)); low = Pt(x, pointy(price->low)); close = Pt(x, pointy(price->close)); if(i > 0 && i%15 == 0) drawxtick(price, x); line(screen, high, low, 0, 0, 0, cols[LINE], ZP); line(screen, open, close, 0, 0, 2, c, ZP); } flushimage(display, 1);} voidresize(void){ Point p, q; int wmin, wmax, w; char buf[32] = {0}; snprint(buf, sizeof buf, "%f", chart.ymin); wmin = stringwidth(afont, buf); snprint(buf, sizeof buf, "%f", chart.ymax); wmax = stringwidth(afont, buf); w = MAX(wmin, wmax) + Padding + Padding; pt = addpt(screen->r.min, Pt(Padding, Padding)); p = addpt(pt, Pt(0, font->height + Padding)); q = subpt(screen->r.max, Pt(Padding, Padding + font->height + Padding)); chartr = Rpt(p, q); arear = chartr; arear.max.x -= w; maxprices = 1+(Dx(arear) - 6)/6; if(maxprices >= chart.nprices) offset = 0; redraw();} Image*initcol(ulong c){ Image *i; i = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, c); if(i == nil) sysfatal("allocimage: %r"); return i;} voidinitcols(void){ cols[BACK] = initcol(0x282828ff); cols[TEXT] = initcol(0xebdbb2ff); cols[AXIS] = initcol(0x858891ff); cols[DIVS] = initcol(0x333333ff); cols[LINE] = initcol(0xa89984ff); cols[BULL] = initcol(0x98971aff); cols[BEAR] = initcol(0xcc241dff);} voidthreadmain(int argc, char *argv[]){ Mouse m; Rune k; Alt a[] = { { nil, &m, CHANRCV }, { nil, nil, CHANRCV }, { nil, &k, CHANRCV }, { nil, nil, CHANEND }, }; int n, l; title = nil; ARGBEGIN{ case 't': title = ARGF(); break; }ARGEND; tmfmtinstall(); loadprices(&chart, 0); if(initdraw(nil, nil, argv0) < 0) sysfatal("initdraw: %r"); display->locking = 0; if((mc = initmouse(nil, screen)) == nil) sysfatal("initmouse: %r"); if((kc = initkeyboard(nil)) == nil) sysfatal("initkeyboard: %r"); a[0].c = mc->c; a[1].c = mc->resizec; a[2].c = kc->c; initcols(); afont = openfont(display, "/lib/font/bit/misc/unicode.6x13.font"); if(afont == nil) sysfatal("openfont: %r"); offset = 0; resize(); l = 0; for(;;){ switch(alt(a)){ case 0: if(ptinrect(m.xy, arear)){ n = offset + ((m.xy.x - arear.min.x) / 6) - 1; if(n > 0 && n < chart.nprices){ drawlegend(&chart.prices[n]); l = 1; }else if(l){ drawlegend(nil); l = 0; } }else if(l){ drawlegend(nil); l = 0; } break; case 1: if(getwindow(display, Refnone) < 0) sysfatal("getwindow: %r"); resize(); break; case 2: switch(k){ case Kleft: if(offset > 0 && maxprices < chart.nprices){ offset -= 10; redraw(); } break; case Kright: if(offset+maxprices < chart.nprices){ offset += 10; redraw(); } break; case 'q': case Kdel: threadexitsall(nil); } break; } }}
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <thread.h>
#include <draw.h>
#include <mouse.h>
#include <keyboard.h>
#include "a.h"
enum
{
BACK,
TEXT,
AXIS,
DIVS,
LINE,
BULL,
BEAR,
NCOLS,
};
enum
{
Padding = 4,
};
Chart chart;
char *title = "^FCHI";
Mousectl *mc;
Keyboardctl *kc;
Image *cols[NCOLS];
Font *afont;
Rectangle chartr;
Rectangle arear;
Point pmin;
Point pmax;
Point pt;
Point pl;
int offset;
int maxprices;
#define MIN(x,y) ((x) < (y) ? (x) : (y))
#define MAX(x,y) ((x) > (y) ? (x) : (y))
double
parsedouble(char *s)
{
char *e;
double d;
d = strtod(s, &e);
if(e == nil || e == s)
return -1;
return d;
}
void
loadprices(Chart *chart, int fd)
{
Biobuf *bp;
char *s, *f[5];
int n, l;
Price *price;
bp = Bfdopen(fd, OREAD);
if(bp == nil)
sysfatal("Bfdopen: %r");
chart->ymin = 1000000000;
chart->ymax = 0.0;
for(l = 0; ;l++){
s = Brdstr(bp, '\n', 1);
if(s == nil)
break;
n = getfields(s, f, nelem(f), 0, ",");
if(n != 5){
fprint(2, "invalid input at line %d\n", l+1);
continue;
}
price = &chart->prices[chart->nprices];
if(tmparse(&price->date, "YYYY-MM-DD", f[0], nil, nil) == nil){
fprint(2, "invalid date '%s' at line %d\n", f[0], l+1);
continue;
}
price->open = parsedouble(f[1]);
price->high = parsedouble(f[2]);
price->low = parsedouble(f[3]);
price->close = parsedouble(f[4]);
chart->ymin = MIN(chart->ymin, price->low);
chart->ymax = MAX(chart->ymax, price->high);
chart->nprices += 1;
}
if(chart->nprices == 0)
sysfatal("could not parse input");
}
Point
stringdbl(Image *b, Point p, Image *c, Point sp, Font *f, double d)
{
char buf[32] = {0};
snprint(buf, sizeof buf, "%f", d);
return string(b, p, c, sp, f, buf);
}
void
drawlegend(Price *price)
{
char buf[255] = {0};
Point p;
Image *c;
p = Pt(chartr.max.x, chartr.min.y - 1);
draw(screen, Rpt(pl, p), cols[BACK], nil, ZP);
if(price == nil){
flushimage(display, 1);
return;
}
c = price->open < price->close ? cols[BULL] : cols[BEAR];
p = pl;
if(title != nil)
p = string(screen, p, cols[TEXT], ZP, font, " - ");
snprint(buf, sizeof buf, "%τ", tmfmt(&price->date, "DD/MM/YYYY"));
p = string(screen, p, cols[TEXT], ZP, font, buf);
p = string(screen, p, cols[TEXT], ZP, font, " O:");
p = stringdbl(screen, p, c, ZP, font, price->open);
p = string(screen, p, cols[TEXT], ZP, font, " H:");
p = stringdbl(screen, p, c, ZP, font, price->high);
p = string(screen, p, cols[TEXT], ZP, font, " L:");
p = stringdbl(screen, p, c, ZP, font, price->low);
p = string(screen, p, cols[TEXT], ZP, font, " C:");
p = stringdbl(screen, p, c, ZP, font, price->close);
flushimage(display, 1);
}
void
drawyaxis(void)
{
char buf[32] = {0};
Point p, q;
int i, n;
double v, dv;
border(screen, arear, 1, cols[AXIS], ZP);
n = (Dy(arear)-4*Padding) / 10;
dv = (chart.ymax - chart.ymin) / 10;
p = addpt(arear.min, Pt(1, 2*Padding));
q = addpt(p, Pt(Dx(arear) - 2, 0));
pmax = q;
for(i = 0; i <= 10; i++){
line(screen, p, q, 0, 0, 0, cols[DIVS], ZP);
line(screen, addpt(q, Pt(1,0)), addpt(q, Pt(5,0)), 0, 0, 0, cols[AXIS], ZP);
if(i == 10) v = chart.ymin;
else v = chart.ymax - i*dv;
snprint(buf, sizeof buf, "%f", v);
string(screen, addpt(q, Pt(5+Padding, -font->height/3)), cols[AXIS], ZP, afont, buf);
p.y += n;
q.y += n;
}
pmin = subpt(q, Pt(0, n));
}
void
drawxtick(Price *price, int x)
{
Point p, q;
char buf[12] = {0};
int w;
p = Pt(x, arear.min.y + 1);
q = Pt(x, arear.max.y - 1);
line(screen, p, q, 0, 0, 0, cols[DIVS], ZP);
line(screen, addpt(q, Pt(0,1)), addpt(q, Pt(0,5)), 0, 0, 0, cols[AXIS], ZP);
snprint(buf, sizeof buf, "%τ", tmfmt(&price->date, "DD/MM/YYYY"));
w = stringwidth(afont, buf);
string(screen, addpt(q, Pt(-w/2, 5+Padding)), cols[AXIS], ZP, afont, buf);
}
int
pointy(double v)
{
int y;
double Δv, Δp, Δy;
Δv = v - chart.ymin;
Δp = pmax.y - pmin.y;
Δy = chart.ymax - chart.ymin;
y = pmin.y + Δp * (Δv / Δy);
return y;
}
void
redraw(void)
{
Point open, high, low, close;
Image *c;
Price *price;
int i, x;
draw(screen, screen->r, cols[BACK], nil, ZP);
if(title != nil)
pl = string(screen, pt, cols[TEXT], ZP, font, title);
else
pl = pt;
drawyaxis();
for(i = offset; i-offset < maxprices-1 && i < chart.nprices; i++){
price = &chart.prices[i];
c = price->open < price->close ? cols[BULL] : cols[BEAR];
x = arear.min.x + 6*(i-offset+1) + 2;
if(x+4 >= arear.max.x){
fprint(2, "should not happen\n");
break;
}
open = Pt(x, pointy(price->open));
high = Pt(x, pointy(price->high));
low = Pt(x, pointy(price->low));
close = Pt(x, pointy(price->close));
if(i > 0 && i%15 == 0)
drawxtick(price, x);
line(screen, high, low, 0, 0, 0, cols[LINE], ZP);
line(screen, open, close, 0, 0, 2, c, ZP);
}
flushimage(display, 1);
}
void
resize(void)
{
Point p, q;
int wmin, wmax, w;
char buf[32] = {0};
snprint(buf, sizeof buf, "%f", chart.ymin);
wmin = stringwidth(afont, buf);
snprint(buf, sizeof buf, "%f", chart.ymax);
wmax = stringwidth(afont, buf);
w = MAX(wmin, wmax) + Padding + Padding;
pt = addpt(screen->r.min, Pt(Padding, Padding));
p = addpt(pt, Pt(0, font->height + Padding));
q = subpt(screen->r.max, Pt(Padding, Padding + font->height + Padding));
chartr = Rpt(p, q);
arear = chartr;
arear.max.x -= w;
maxprices = 1+(Dx(arear) - 6)/6;
if(maxprices >= chart.nprices)
offset = 0;
redraw();
}
Image*
initcol(ulong c)
{
Image *i;
i = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, c);
if(i == nil)
sysfatal("allocimage: %r");
return i;
}
void
initcols(void)
{
cols[BACK] = initcol(0x282828ff);
cols[TEXT] = initcol(0xebdbb2ff);
cols[AXIS] = initcol(0x858891ff);
cols[DIVS] = initcol(0x333333ff);
cols[LINE] = initcol(0xa89984ff);
cols[BULL] = initcol(0x98971aff);
cols[BEAR] = initcol(0xcc241dff);
}
void
threadmain(int argc, char *argv[])
{
Mouse m;
Rune k;
Alt a[] = {
{ nil, &m, CHANRCV },
{ nil, nil, CHANRCV },
{ nil, &k, CHANRCV },
{ nil, nil, CHANEND },
};
int n, l;
title = nil;
ARGBEGIN{
case 't':
title = ARGF();
break;
}ARGEND;
tmfmtinstall();
loadprices(&chart, 0);
if(initdraw(nil, nil, argv0) < 0)
sysfatal("initdraw: %r");
display->locking = 0;
if((mc = initmouse(nil, screen)) == nil)
sysfatal("initmouse: %r");
if((kc = initkeyboard(nil)) == nil)
sysfatal("initkeyboard: %r");
a[0].c = mc->c;
a[1].c = mc->resizec;
a[2].c = kc->c;
initcols();
afont = openfont(display, "/lib/font/bit/misc/unicode.6x13.font");
if(afont == nil)
sysfatal("openfont: %r");
offset = 0;
resize();
l = 0;
for(;;){
switch(alt(a)){
case 0:
if(ptinrect(m.xy, arear)){
n = offset + ((m.xy.x - arear.min.x) / 6) - 1;
if(n > 0 && n < chart.nprices){
drawlegend(&chart.prices[n]);
l = 1;
}else if(l){
drawlegend(nil);
l = 0;
}
}else if(l){
drawlegend(nil);
l = 0;
}
break;
case 1:
if(getwindow(display, Refnone) < 0)
sysfatal("getwindow: %r");
resize();
break;
case 2:
switch(k){
case Kleft:
if(offset > 0 && maxprices < chart.nprices){
offset -= 10;
redraw();
}
break;
case Kright:
if(offset+maxprices < chart.nprices){
offset += 10;
redraw();
}
break;
case 'q':
case Kdel:
threadexitsall(nil);
}
break;
}
}
}