From c34e5e1a7d5eb897c10a0d62616e188ee2b005ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B2=B3?= Date: Fri, 25 Jan 2019 12:10:12 +0800 Subject: [PATCH] =?UTF-8?q?dashboard=20=E5=A4=87=E6=B3=A8=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E7=AE=A1=E7=90=86=E4=BC=98=E5=8C=96=20?= =?UTF-8?q?=E5=A4=9A=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=94=AF=E6=8C=81=20?= =?UTF-8?q?=E6=B5=81=E9=87=8F=E6=98=BE=E7=A4=BA=E6=94=AF=E6=8C=81=20?= =?UTF-8?q?=E7=83=AD=E6=9B=B4=E6=96=B0=E6=94=AF=E6=8C=81=20404=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E9=A1=B5=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bridge/bridge.go | 87 +- client/client.go | 43 +- cmd/proxy_server/proxy_server.go | 4 +- conf/clients.csv | 2 + conf/hosts.csv | 4 +- conf/tasks.csv | 9 +- server/base.go | 76 + server/file.go | 271 --- server/process.go | 34 +- server/server.go | 187 +- server/socks5.go | 13 +- server/tcp.go | 46 +- server/udp.go | 21 +- utils/conn.go | 8 +- utils/file.go | 487 +++++ utils/pool.go | 13 +- utils/util.go | 60 +- web/controllers/base.go | 8 +- web/controllers/client.go | 100 + web/controllers/index.go | 100 +- web/routers/router.go | 1 + web/static/js/chart.js | 3477 ++++++++++++++++++++++++++++++ web/static/js/datatables.min.js | 46 +- web/static/js/main.js | 59 +- web/static/page/error.html | 10 + web/views/client/add.html | 67 + web/views/client/edit.html | 71 + web/views/client/list.html | 234 ++ web/views/index/add.html | 43 +- web/views/index/edit.html | 46 +- web/views/index/hadd.html | 12 +- web/views/index/hedit.html | 10 +- web/views/index/help.html | 146 ++ web/views/index/hlist.html | 33 +- web/views/index/index.html | 219 +- web/views/index/list.html | 90 +- web/views/public/layout.html | 10 +- 37 files changed, 5415 insertions(+), 732 deletions(-) create mode 100644 conf/clients.csv create mode 100644 server/base.go delete mode 100644 server/file.go create mode 100644 utils/file.go create mode 100644 web/controllers/client.go create mode 100755 web/static/js/chart.js create mode 100644 web/static/page/error.html create mode 100755 web/views/client/add.html create mode 100755 web/views/client/edit.html create mode 100755 web/views/client/list.html create mode 100644 web/views/index/help.html diff --git a/bridge/bridge.go b/bridge/bridge.go index 1dbf915..06a9b6c 100755 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -5,6 +5,7 @@ import ( "github.com/cnlh/easyProxy/utils" "log" "net" + "strconv" "sync" "time" ) @@ -31,20 +32,20 @@ func newList() *list { } type Tunnel struct { - TunnelPort int //通信隧道端口 - listener *net.TCPListener //server端监听 - SignalList map[string]*list //通信 - TunnelList map[string]*list //隧道 - RunList map[string]interface{} //运行中的任务 + TunnelPort int //通信隧道端口 + listener *net.TCPListener //server端监听 + SignalList map[int]*list //通信 + TunnelList map[int]*list //隧道 + RunList map[int]interface{} //运行中的任务 lock sync.Mutex tunnelLock sync.Mutex } -func NewTunnel(tunnelPort int, runList map[string]interface{}) *Tunnel { +func NewTunnel(tunnelPort int, runList map[int]interface{}) *Tunnel { t := new(Tunnel) t.TunnelPort = tunnelPort - t.SignalList = make(map[string]*list) - t.TunnelList = make(map[string]*list) + t.SignalList = make(map[int]*list) + t.TunnelList = make(map[int]*list) t.RunList = runList return t } @@ -87,7 +88,8 @@ func (s *Tunnel) cliProcess(c *utils.Conn) error { c.Conn.Close() return err } - if !s.verify(string(vval)) { + id, err := utils.GetCsvDb().GetIdByVerifyKey(string(vval),c.Conn.RemoteAddr().String()) + if err != nil { log.Println("当前客户端连接校验错误,关闭此客户端:", c.Conn.RemoteAddr()) s.verifyError(c) return errors.New("验证错误") @@ -97,18 +99,18 @@ func (s *Tunnel) cliProcess(c *utils.Conn) error { if flag, err := c.ReadFlag(); err != nil { return err } else { - return s.typeDeal(flag, c, string(vval)) + return s.typeDeal(flag, c, id) } } //tcp连接类型区分 -func (s *Tunnel) typeDeal(typeVal string, c *utils.Conn, cFlag string) error { +func (s *Tunnel) typeDeal(typeVal string, c *utils.Conn, id int) error { switch typeVal { case utils.WORK_MAIN: log.Println("客户端连接成功", c.Conn.RemoteAddr()) - s.addList(s.SignalList, c, cFlag) + s.addList(s.SignalList, c, id) case utils.WORK_CHAN: - s.addList(s.TunnelList, c, cFlag) + s.addList(s.TunnelList, c, id) default: return errors.New("无法识别") } @@ -117,41 +119,38 @@ func (s *Tunnel) typeDeal(typeVal string, c *utils.Conn, cFlag string) error { } //加到对应的list中 -func (s *Tunnel) addList(m map[string]*list, c *utils.Conn, cFlag string) { +func (s *Tunnel) addList(m map[int]*list, c *utils.Conn, id int) { s.lock.Lock() - if v, ok := m[cFlag]; ok { + if v, ok := m[id]; ok { v.Add(c) } else { l := newList() l.Add(c) - m[cFlag] = l + m[id] = l } s.lock.Unlock() } //新建隧道 -func (s *Tunnel) newChan(cFlag string) error { +func (s *Tunnel) newChan(id int) error { var connPass *utils.Conn var err error retry: - if connPass, err = s.waitAndPop(s.SignalList, cFlag); err != nil { + if connPass, err = s.waitAndPop(s.SignalList, id); err != nil { return err } if _, err = connPass.Conn.Write([]byte("chan")); err != nil { goto retry } - s.SignalList[cFlag].Add(connPass) + s.SignalList[id].Add(connPass) return nil } //得到一个tcp隧道 //TODO 超时问题 锁机制问题 对单个客户端加锁 -func (s *Tunnel) GetTunnel(cFlag string, en, de int, crypt, mux bool) (c *utils.Conn, err error) { - if v, ok := s.TunnelList[cFlag]; !ok || v.Len() < 3 { //新建通道 - go s.newChan(cFlag) - } +func (s *Tunnel) GetTunnel(id int, en, de int, crypt, mux bool) (c *utils.Conn, err error) { retry: - if c, err = s.waitAndPop(s.TunnelList, cFlag); err != nil { + if c, err = s.waitAndPop(s.TunnelList, id); err != nil { return } if _, err = c.WriteTest(); err != nil { @@ -163,61 +162,61 @@ retry: } //得到一个通信通道 -func (s *Tunnel) GetSignal(cFlag string) (err error, conn *utils.Conn) { - if v, ok := s.SignalList[cFlag]; !ok || v.Len() == 0 { +func (s *Tunnel) GetSignal(id int) (err error, conn *utils.Conn) { + if v, ok := s.SignalList[id]; !ok || v.Len() == 0 { err = errors.New("客户端未连接") return } - conn = s.SignalList[cFlag].Pop() + conn = s.SignalList[id].Pop() return } //重回slice 复用 -func (s *Tunnel) ReturnSignal(conn *utils.Conn, cFlag string) { - if v, ok := s.SignalList[cFlag]; ok { +func (s *Tunnel) ReturnSignal(conn *utils.Conn, id int) { + if v, ok := s.SignalList[id]; ok { v.Add(conn) } } //重回slice 复用 -func (s *Tunnel) ReturnTunnel(conn *utils.Conn, cFlag string) { - if v, ok := s.TunnelList[cFlag]; ok { +func (s *Tunnel) ReturnTunnel(conn *utils.Conn, id int) { + if v, ok := s.TunnelList[id]; ok { utils.FlushConn(conn.Conn) v.Add(conn) } } //删除通信通道 -func (s *Tunnel) DelClientSignal(cFlag string) { - s.delClient(cFlag, s.SignalList) +func (s *Tunnel) DelClientSignal(id int) { + s.delClient(id, s.SignalList) } //删除隧道 -func (s *Tunnel) DelClientTunnel(cFlag string) { - s.delClient(cFlag, s.TunnelList) +func (s *Tunnel) DelClientTunnel(id int) { + s.delClient(id, s.TunnelList) } -func (s *Tunnel) delClient(cFlag string, l map[string]*list) { - if t := l[utils.Getverifyval(cFlag)]; t != nil { +func (s *Tunnel) delClient(id int, l map[int]*list) { + if t := l[id]; t != nil { for { if t.Len() <= 0 { break } t.Pop().Close() } - delete(l, utils.Getverifyval(cFlag)) + delete(l, id) } } //等待 -func (s *Tunnel) waitAndPop(m map[string]*list, cFlag string) (c *utils.Conn, err error) { +func (s *Tunnel) waitAndPop(m map[int]*list, id int) (c *utils.Conn, err error) { ticker := time.NewTicker(time.Millisecond * 100) - stop := time.After(time.Second * 10) + stop := time.After(time.Second * 3) for { select { case <-ticker.C: s.lock.Lock() - if v, ok := m[cFlag]; ok && v.Len() > 0 { + if v, ok := m[id]; ok && v.Len() > 0 { c = v.Pop() ticker.Stop() s.lock.Unlock() @@ -225,16 +224,16 @@ func (s *Tunnel) waitAndPop(m map[string]*list, cFlag string) (c *utils.Conn, er } s.lock.Unlock() case <-stop: - err = errors.New("client key: " + cFlag + ",err: get client conn timeout") + err = errors.New("client id: " + strconv.Itoa(id) + ",err: get client conn timeout") return } } return } -func (s *Tunnel) verify(vKeyMd5 string) bool { +func (s *Tunnel) verify(id int) bool { for k := range s.RunList { - if utils.Getverifyval(k) == vKeyMd5 { + if k == id { return true } } diff --git a/client/client.go b/client/client.go index a57f0c7..7dde234 100755 --- a/client/client.go +++ b/client/client.go @@ -5,12 +5,16 @@ import ( "log" "net" "sync" + "sync/atomic" "time" ) type TRPClient struct { - svrAddr string - tcpNum int + svrAddr string + tcpNum int + tunnelNum int64 + tunnel chan bool + serverStatus bool sync.Mutex vKey string } @@ -21,6 +25,7 @@ func NewRPClient(svraddr string, tcpNum int, vKey string) *TRPClient { c.svrAddr = svraddr c.tcpNum = tcpNum c.vKey = vKey + c.tunnel = make(chan bool) return c } @@ -29,12 +34,17 @@ func (s *TRPClient) Start() error { for i := 0; i < s.tcpNum; i++ { go s.NewConn() } + for i := 0; i < 5; i++ { + go s.dealChan() + } + go s.session() return nil } //新建 func (s *TRPClient) NewConn() error { s.Lock() + s.serverStatus = false conn, err := net.Dial("tcp", s.svrAddr) if err != nil { log.Println("连接服务端失败,五秒后将重连") @@ -44,11 +54,12 @@ func (s *TRPClient) NewConn() error { return err } s.Unlock() - return s.process(utils.NewConn(conn)) + return s.processor(utils.NewConn(conn)) } //处理 -func (s *TRPClient) process(c *utils.Conn) error { +func (s *TRPClient) processor(c *utils.Conn) error { + s.serverStatus = true c.SetAlive() if _, err := c.Write([]byte(utils.Getverifyval(s.vKey))); err != nil { return err @@ -58,7 +69,6 @@ func (s *TRPClient) process(c *utils.Conn) error { flags, err := c.ReadFlag() if err != nil { log.Println("服务端断开,五秒后将重连", err) - time.Sleep(5 * time.Second) go s.NewConn() break } @@ -66,9 +76,6 @@ func (s *TRPClient) process(c *utils.Conn) error { case utils.VERIFY_EER: log.Fatalln("vkey:", s.vKey, "不正确,服务端拒绝连接,请检查") case utils.WORK_CHAN: //隧道模式,每次开启10个,加快连接速度 - for i := 0; i < 5; i++ { - go s.dealChan() - } case utils.RES_MSG: log.Println("服务端返回错误。") default: @@ -98,13 +105,16 @@ func (s *TRPClient) dealChan() { //写标志 c.WriteChan() re: + atomic.AddInt64(&s.tunnelNum, 1) //获取连接的host type(tcp or udp) typeStr, host, en, de, crypt, mux, err := c.GetHostFromConn() + s.tunnel <- true + atomic.AddInt64(&s.tunnelNum, -1) if err != nil { c.Close() return } - Process(c, typeStr, host, en, de, crypt, mux) + s.ConnectAndCopy(c, typeStr, host, en, de, crypt, mux) if mux { utils.FlushConn(conn) goto re @@ -113,7 +123,20 @@ re: } } -func Process(c *utils.Conn, typeStr, host string, en, de int, crypt, mux bool) { +func (s *TRPClient) session() { + t := time.NewTicker(time.Millisecond * 1000) + for { + select { + case <-s.tunnel: + case <-t.C: + } + if s.serverStatus && s.tunnelNum < 5 { + go s.dealChan() + } + } +} + +func (s *TRPClient) ConnectAndCopy(c *utils.Conn, typeStr, host string, en, de int, crypt, mux bool) { //与目标建立连接,超时时间为3 server, err := net.DialTimeout(typeStr, host, time.Second*3) if err != nil { diff --git a/cmd/proxy_server/proxy_server.go b/cmd/proxy_server/proxy_server.go index c250b3f..23bfb34 100644 --- a/cmd/proxy_server/proxy_server.go +++ b/cmd/proxy_server/proxy_server.go @@ -25,7 +25,7 @@ var ( func main() { flag.Parse() server.VerifyKey = *VerifyKey - cnf := server.ServerConfig{ + cnf := &utils.ServerConfig{ TcpPort: *httpPort, Mode: *rpMode, Target: *tunnelTarget, @@ -51,5 +51,5 @@ func main() { } log.Println("服务端启动,监听tcp服务端端口:", *TcpPort) cnf.CompressDecode, cnf.CompressEncode = utils.GetCompressType(cnf.Compress) - server.StartNewServer(*TcpPort, &cnf) + server.StartNewServer(*TcpPort, cnf) } diff --git a/conf/clients.csv b/conf/clients.csv new file mode 100644 index 0000000..206dcd6 --- /dev/null +++ b/conf/clients.csv @@ -0,0 +1,2 @@ +2,zl4p3da659qa9rh3,127.0.0.1:58000,测试2,true,,,0,0, +1,rfd0tl1anega0d0g,127.0.0.1:53603,测试,true,1,1,1,1,snappy diff --git a/conf/hosts.csv b/conf/hosts.csv index 8c1a9ff..b4ba649 100644 --- a/conf/hosts.csv +++ b/conf/hosts.csv @@ -1,2 +1,2 @@ -a.o.com,127.0.0.1:8082,7hiixust68kbz33a,,www.baidu.com -b.o.com,,7hiixust68kbz33a,,ab +a.o.com,127.0.0.1:8080,1,,,测试2 +b.o.com,127.0.0.1:8082,2,,,测试 diff --git a/conf/tasks.csv b/conf/tasks.csv index 71a3970..4043561 100644 --- a/conf/tasks.csv +++ b/conf/tasks.csv @@ -1,5 +1,4 @@ -8001,tunnelServer,127.0.0.1:88,jq5i7n0sjs1h0jje,aaa,bbb,,1,0,0,0,1 -0,hostServer,,7n7bxc2bm1fyjfab,ab,b,,1,1,1,0,1 -0,hostServer,,ts08z6vk5nc72fs8,aaa,bbb,snappy,1,0,1,2,3 -8002,tunnelServer,127.0.0.1:88,2nxo93wvotb9g75s,,,,1,0,0,0,1 -8025,socks5Server,,2p3qs71oym3zx52w,,,,1,0,0,0,1 +9001,tunnelServer,127.0.0.1:8080,,,,1,0,0,0,0,1,1,true,test +53,udpServer,114.114.114.114:53,,,,1,0,0,0,0,2,2,true,udp测试 +8024,socks5Server,,,,,1,0,0,0,0,3,2,true,socks5测试 +8025,httpProxyServer,,,,,1,0,0,0,0,4,2,true,http测试 diff --git a/server/base.go b/server/base.go new file mode 100644 index 0000000..74f1daf --- /dev/null +++ b/server/base.go @@ -0,0 +1,76 @@ +package server + +import ( + "github.com/cnlh/easyProxy/bridge" + "github.com/cnlh/easyProxy/utils" + "sync" +) + +//server base struct +type server struct { + bridge *bridge.Tunnel + config *utils.ServerConfig + sync.Mutex +} + +func (s *server) GetTunnelAndWriteHost(connType string, cnf *utils.ServerConfig, addr string) (*utils.Conn, error) { + var err error + link, err := s.bridge.GetTunnel(cnf.ClientId, cnf.CompressEncode, cnf.CompressDecode, cnf.Crypt, cnf.Mux) + if err != nil { + return nil, err + } + if _, err = link.WriteHost(connType, addr); err != nil { + link.Close() + return nil, err + } + return link, nil +} + +func (s *server) FlowAdd(in, out int64) { + s.Lock() + defer s.Unlock() + if s.config.Flow == nil { + s.config.Flow = new(utils.Flow) + } + s.config.Flow.ExportFlow += out + s.config.Flow.InletFlow += in +} + +func (s *server) FlowAddHost(host *utils.HostList, in, out int64) { + s.Lock() + defer s.Unlock() + if s.config.Flow == nil { + s.config.Flow = new(utils.Flow) + } + host.Flow.ExportFlow += out + host.Flow.InletFlow += in +} + +//热更新配置 +func (s *server) ResetConfig() { + task, err := CsvDb.GetTask(s.config.Id) + if err != nil { + return + } + s.config.UseClientCnf = task.UseClientCnf + if s.config.UseClientCnf { + client, err := CsvDb.GetClient(s.config.ClientId) + if err == nil { + s.config.U = client.Cnf.U + s.config.P = client.Cnf.P + s.config.Compress = client.Cnf.Compress + s.config.Mux = client.Cnf.Mux + s.config.Crypt = client.Cnf.Crypt + } + s.config.CompressDecode, s.config.CompressEncode = utils.GetCompressType(client.Cnf.Compress) + } else { + if err == nil { + s.config.U = task.U + s.config.P = task.P + s.config.Compress = task.Compress + s.config.Mux = task.Mux + s.config.Crypt = task.Crypt + } + s.config.CompressDecode, s.config.CompressEncode = utils.GetCompressType(task.Compress) + } +} diff --git a/server/file.go b/server/file.go deleted file mode 100644 index 408df1f..0000000 --- a/server/file.go +++ /dev/null @@ -1,271 +0,0 @@ -package server - -import ( - "encoding/csv" - "errors" - "github.com/astaxie/beego" - "github.com/cnlh/easyProxy/utils" - "log" - "os" - "strconv" -) - -type ServerConfig struct { - TcpPort int //服务端与客户端通信端口 - Mode string //启动方式 - Target string //目标 - VerifyKey string //flag - U string //socks5验证用户名 - P string //socks5验证密码 - Compress string //压缩方式 - Start int //是否开启 - IsRun int //是否在运行 - ClientStatus int //客s户端状态 - Crypt bool //是否加密 - Mux bool //是否加密 - CompressEncode int //加密方式 - CompressDecode int //解密方式 -} - -type HostList struct { - Vkey string //服务端与客户端通信端口 - Host string //启动方式 - Target string //目标 - HeaderChange string //host修改 - HostChange string //host修改 -} - -func NewCsv(runList map[string]interface{}) *Csv { - c := new(Csv) - c.RunList = runList - return c -} - -type Csv struct { - Tasks []*ServerConfig - Path string - RunList map[string]interface{} - Hosts []*HostList //域名列表 -} - -func (s *Csv) Init() { - s.LoadTaskFromCsv() - s.LoadHostFromCsv() -} - -func (s *Csv) StoreTasksToCsv() { - // 创建文件 - csvFile, err := os.Create(beego.AppPath + "/conf/tasks.csv") - if err != nil { - log.Fatalf(err.Error()) - } - defer csvFile.Close() - writer := csv.NewWriter(csvFile) - for _, task := range s.Tasks { - record := []string{ - strconv.Itoa(task.TcpPort), - task.Mode, - task.Target, - task.VerifyKey, - task.U, - task.P, - task.Compress, - strconv.Itoa(task.Start), - utils.GetStrByBool(task.Crypt), - utils.GetStrByBool(task.Mux), - strconv.Itoa(task.CompressEncode), - strconv.Itoa(task.CompressDecode), - } - err := writer.Write(record) - if err != nil { - log.Fatalf(err.Error()) - } - } - writer.Flush() -} - -func (s *Csv) openFile(path string) ([][]string, error) { - // 打开文件 - file, err := os.Open(path) - if err != nil { - panic(err) - } - defer file.Close() - - // 获取csv的reader - reader := csv.NewReader(file) - - // 设置FieldsPerRecord为-1 - reader.FieldsPerRecord = -1 - - // 读取文件中所有行保存到slice中 - return reader.ReadAll() -} - -func (s *Csv) LoadTaskFromCsv() { - path := beego.AppPath + "/conf/tasks.csv" - records, err := s.openFile(path) - if err != nil { - log.Fatal("配置文件打开错误:", path) - } - var tasks []*ServerConfig - // 将每一行数据保存到内存slice中 - for _, item := range records { - post := &ServerConfig{ - TcpPort: utils.GetIntNoErrByStr(item[0]), - Mode: item[1], - Target: item[2], - VerifyKey: item[3], - U: item[4], - P: item[5], - Compress: item[6], - Start: utils.GetIntNoErrByStr(item[7]), - Crypt: utils.GetBoolByStr(item[8]), - Mux: utils.GetBoolByStr(item[9]), - CompressEncode: utils.GetIntNoErrByStr(item[10]), - CompressDecode: utils.GetIntNoErrByStr(item[11]), - } - tasks = append(tasks, post) - } - s.Tasks = tasks -} - -func (s *Csv) NewTask(t *ServerConfig) { - s.Tasks = append(s.Tasks, t) - s.StoreTasksToCsv() -} - -func (s *Csv) UpdateTask(t *ServerConfig) error { - for k, v := range s.Tasks { - if v.VerifyKey == t.VerifyKey { - s.Tasks = append(s.Tasks[:k], s.Tasks[k+1:]...) - s.Tasks = append(s.Tasks, t) - s.StoreTasksToCsv() - return nil - } - } - return errors.New("不存在") -} - -func (s *Csv) UpdateHost(t *HostList) error { - for k, v := range s.Hosts { - if v.Host == t.Host { - s.Hosts = append(s.Hosts[:k], s.Hosts[k+1:]...) - s.Hosts = append(s.Hosts, t) - s.StoreHostToCsv() - return nil - } - } - return errors.New("不存在") -} - -func (s *Csv) AddRunList(vKey string, svr interface{}) { - s.RunList[vKey] = svr -} - -func (s *Csv) DelRunList(vKey string) { - delete(s.RunList, vKey) -} - -func (s *Csv) DelTask(vKey string) error { - for k, v := range s.Tasks { - if v.VerifyKey == vKey { - s.Tasks = append(s.Tasks[:k], s.Tasks[k+1:]...) - s.StoreTasksToCsv() - return nil - } - } - return errors.New("不存在") -} - -func (s *Csv) GetTask(vKey string) (v *ServerConfig, err error) { - for _, v = range s.Tasks { - if v.VerifyKey == vKey { - return - } - } - err = errors.New("未找到") - return -} - -func (s *Csv) StoreHostToCsv() { - // 创建文件 - csvFile, err := os.Create(beego.AppPath + "/conf/hosts.csv") - if err != nil { - panic(err) - } - defer csvFile.Close() - // 获取csv的Writer - writer := csv.NewWriter(csvFile) - // 将map中的Post转换成slice,因为csv的Write需要slice参数 - // 并写入csv文件 - for _, host := range s.Hosts { - record := []string{ - host.Host, - host.Target, - host.Vkey, - host.HeaderChange, - host.HostChange, - } - err1 := writer.Write(record) - if err1 != nil { - panic(err1) - } - } - // 确保所有内存数据刷到csv文件 - writer.Flush() -} - -func (s *Csv) LoadHostFromCsv() { - path := beego.AppPath + "/conf/hosts.csv" - records, err := s.openFile(path) - if err != nil { - log.Fatal("配置文件打开错误:", path) - } - var hosts []*HostList - // 将每一行数据保存到内存slice中 - for _, item := range records { - post := &HostList{ - Vkey: item[2], - Host: item[0], - Target: item[1], - HeaderChange: item[3], - HostChange: item[4], - } - hosts = append(hosts, post) - } - s.Hosts = hosts -} - -func (s *Csv) DelHost(host string) error { - for k, v := range s.Hosts { - if v.Host == host { - s.Hosts = append(s.Hosts[:k], s.Hosts[k+1:]...) - s.StoreHostToCsv() - return nil - } - } - return errors.New("不存在") -} - -func (s *Csv) NewHost(t *HostList) { - s.Hosts = append(s.Hosts, t) - s.StoreHostToCsv() - -} - -func (s *Csv) GetHostList(start, length int, vKey string) ([]*HostList, int) { - list := make([]*HostList, 0) - var cnt int - for _, v := range s.Hosts { - if v.Vkey == vKey { - cnt++ - if start--; start < 0 { - if length--; length > 0 { - list = append(list, v) - } - } - } - } - return list, cnt -} diff --git a/server/process.go b/server/process.go index 639dc55..f564bfd 100644 --- a/server/process.go +++ b/server/process.go @@ -41,8 +41,8 @@ func ProcessHost(c *utils.Conn, s *TunnelModeServer) error { var ( isConn = true link *utils.Conn - cnf *ServerConfig - host *HostList + client *utils.Client + host *utils.HostList wg sync.WaitGroup ) for { @@ -52,48 +52,56 @@ func ProcessHost(c *utils.Conn, s *TunnelModeServer) error { } //首次获取conn if isConn { - isConn = false - if host, cnf, err = GetKeyByHost(r.Host); err != nil { + if host, client, err = GetKeyByHost(r.Host); err != nil { log.Printf("the host %s is not found !", r.Host) break } - if err = s.auth(r, c, cnf.U, cnf.P); err != nil { + client.Cnf.ClientId = host.ClientId + client.Cnf.CompressDecode, client.Cnf.CompressEncode = utils.GetCompressType(client.Cnf.Compress) + if err = s.auth(r, c, client.Cnf.U, client.Cnf.P); err != nil { break } - - if link, err = s.GetTunnelAndWriteHost(utils.CONN_TCP, cnf, host.Target); err != nil { + if link, err = s.GetTunnelAndWriteHost(utils.CONN_TCP, client.Cnf, host.Target); err != nil { log.Println("get bridge tunnel error: ", err) break } - if flag, err := link.ReadFlag(); err != nil || flag == utils.CONN_ERROR { log.Printf("the host %s connection to %s error", r.Host, host.Target) break } else { wg.Add(1) go func() { - utils.Relay(c.Conn, link.Conn, cnf.CompressDecode, cnf.Crypt, cnf.Mux) + out, _ := utils.Relay(c.Conn, link.Conn, client.Cnf.CompressDecode, client.Cnf.Crypt, client.Cnf.Mux) wg.Done() + s.FlowAddHost(host, 0, out) }() } + isConn = false } utils.ChangeHostAndHeader(r, host.HostChange, host.HeaderChange, c.Conn.RemoteAddr().String()) b, err := httputil.DumpRequest(r, true) + s.FlowAddHost(host, int64(len(b)), 0) if err != nil { break } - if _, err := link.WriteTo(b, cnf.CompressEncode, cnf.Crypt); err != nil { + if _, err := link.WriteTo(b, client.Cnf.CompressEncode, client.Cnf.Crypt); err != nil { break } } wg.Wait() - if cnf != nil && cnf.Mux && link != nil { - link.WriteTo([]byte(utils.IO_EOF), cnf.CompressEncode, cnf.Crypt) - s.bridge.ReturnTunnel(link, getverifyval(cnf.VerifyKey)) + if client != nil && client.Cnf != nil && client.Cnf.Mux && link != nil { + link.WriteTo([]byte(utils.IO_EOF), client.Cnf.CompressEncode, client.Cnf.Crypt) + s.bridge.ReturnTunnel(link, client.Id) } else if link != nil { link.Close() } + + if isConn { + s.writeConnFail(c.Conn) + } c.Close() return nil } + + diff --git a/server/server.go b/server/server.go index 80928d0..06dfe51 100644 --- a/server/server.go +++ b/server/server.go @@ -11,35 +11,43 @@ import ( "sync" ) +type RunServer struct { + flag int //标志 + ExportFlow int64 //出口流量 + InletFlow int64 //入口流量 + service interface{} + sync.Mutex +} + var ( Bridge *bridge.Tunnel - RunList map[string]interface{} //运行中的任务 - CsvDb *Csv + RunList map[int]interface{} //运行中的任务 + CsvDb = utils.GetCsvDb() VerifyKey string ) func init() { - RunList = make(map[string]interface{}) + RunList = make(map[int]interface{}) } //从csv文件中恢复任务 func InitFromCsv() { for _, v := range CsvDb.Tasks { if v.Start == 1 { - log.Println("启动模式:", v.Mode, "监听端口:", v.TcpPort, "客户端令牌:", v.VerifyKey) + log.Println("启动模式:", v.Mode, "监听端口:", v.TcpPort) AddTask(v) } } } //start a new server -func StartNewServer(bridgePort int, cnf *ServerConfig) { +func StartNewServer(bridgePort int, cnf *utils.ServerConfig) { Bridge = bridge.NewTunnel(bridgePort, RunList) if err := Bridge.StartTunnel(); err != nil { log.Fatalln("服务端开启失败", err) } if svr := NewMode(Bridge, cnf); svr != nil { - RunList[cnf.VerifyKey] = svr + RunList[cnf.Id] = svr err := reflect.ValueOf(svr).MethodByName("Start").Call(nil)[0] if err.Interface() != nil { log.Println(err) @@ -50,7 +58,8 @@ func StartNewServer(bridgePort int, cnf *ServerConfig) { } //new a server by mode name -func NewMode(Bridge *bridge.Tunnel, config *ServerConfig) interface{} { +func NewMode(Bridge *bridge.Tunnel, c *utils.ServerConfig) interface{} { + config := utils.DeepCopyConfig(c) switch config.Mode { case "tunnelServer": return NewTunnelModeServer(ProcessTunnel, Bridge, config) @@ -61,14 +70,12 @@ func NewMode(Bridge *bridge.Tunnel, config *ServerConfig) interface{} { case "udpServer": return NewUdpModeServer(Bridge, config) case "webServer": - InitCsvDb() InitFromCsv() p, _ := beego.AppConfig.Int("hostPort") - t := &ServerConfig{ + t := &utils.ServerConfig{ TcpPort: p, Mode: "httpHostServer", Target: "", - VerifyKey: "", U: "", P: "", Compress: "", @@ -87,15 +94,10 @@ func NewMode(Bridge *bridge.Tunnel, config *ServerConfig) interface{} { } //stop server -func StopServer(cFlag string) error { - if v, ok := RunList[cFlag]; ok { +func StopServer(id int) error { + if v, ok := RunList[id]; ok { reflect.ValueOf(v).MethodByName("Close").Call(nil) - delete(RunList, cFlag) - if VerifyKey == "" { //多客户端模式关闭相关隧道 - Bridge.DelClientSignal(cFlag) - Bridge.DelClientTunnel(cFlag) - } - if t, err := CsvDb.GetTask(cFlag); err != nil { + if t, err := CsvDb.GetTask(id); err != nil { return err } else { t.Start = 0 @@ -107,15 +109,14 @@ func StopServer(cFlag string) error { } //add task -func AddTask(t *ServerConfig) error { - t.CompressDecode, t.CompressEncode = utils.GetCompressType(t.Compress) +func AddTask(t *utils.ServerConfig) error { if svr := NewMode(Bridge, t); svr != nil { - RunList[t.VerifyKey] = svr + RunList[t.Id] = svr go func() { err := reflect.ValueOf(svr).MethodByName("Start").Call(nil)[0] if err.Interface() != nil { - log.Println("客户端", t.VerifyKey, "启动失败,错误:", err) - delete(RunList, t.VerifyKey) + log.Println("客户端", t.Id, "启动失败,错误:", err) + delete(RunList, t.Id) } }() } else { @@ -125,8 +126,8 @@ func AddTask(t *ServerConfig) error { } //start task -func StartTask(vKey string) error { - if t, err := CsvDb.GetTask(vKey); err != nil { +func StartTask(id int) error { + if t, err := CsvDb.GetTask(id); err != nil { return err } else { AddTask(t) @@ -137,35 +138,20 @@ func StartTask(vKey string) error { } //delete task -func DelTask(vKey string) error { - if err := StopServer(vKey); err != nil { +func DelTask(id int) error { + if err := StopServer(id); err != nil { return err } - for _, v := range CsvDb.Hosts { - if v.Vkey == vKey { - CsvDb.DelHost(v.Host) - } - } - return CsvDb.DelTask(vKey) -} - -//init csv from file -func InitCsvDb() *Csv { - var once sync.Once - once.Do(func() { - CsvDb = NewCsv(RunList) - CsvDb.Init() - }) - return CsvDb + return CsvDb.DelTask(id) } //get key by host from x -func GetKeyByHost(host string) (h *HostList, t *ServerConfig, err error) { +func GetKeyByHost(host string) (h *utils.HostList, t *utils.Client, err error) { for _, v := range CsvDb.Hosts { s := strings.Split(host, ":") if s[0] == v.Host { h = v - t, err = CsvDb.GetTask(v.Vkey) + t, err = CsvDb.GetClient(v.ClientId) return } } @@ -174,22 +160,32 @@ func GetKeyByHost(host string) (h *HostList, t *ServerConfig, err error) { } //get task list by page num -func GetServerConfig(start, length int, typeVal string) ([]*ServerConfig, int) { - list := make([]*ServerConfig, 0) +func GetServerConfig(start, length int, typeVal string, clientId int) ([]*utils.ServerConfig, int) { + list := make([]*utils.ServerConfig, 0) var cnt int for _, v := range CsvDb.Tasks { - if v.Mode != typeVal { + if (typeVal != "" && v.Mode != typeVal) || (typeVal == "" && clientId != v.ClientId) { continue } + if v.UseClientCnf { + v = utils.DeepCopyConfig(v) + if c, err := CsvDb.GetClient(v.ClientId); err == nil { + v.Compress = c.Cnf.Compress + v.Mux = c.Cnf.Mux + v.Crypt = c.Cnf.Crypt + v.U = c.Cnf.U + v.P = c.Cnf.P + } + } cnt++ if start--; start < 0 { if length--; length > 0 { - if _, ok := RunList[v.VerifyKey]; ok { + if _, ok := RunList[v.Id]; ok { v.IsRun = 1 } else { v.IsRun = 0 } - if s, ok := Bridge.SignalList[getverifyval(v.VerifyKey)]; ok { + if s, ok := Bridge.SignalList[v.ClientId]; ok { if s.Len() > 0 { v.ClientStatus = 1 } else { @@ -201,16 +197,91 @@ func GetServerConfig(start, length int, typeVal string) ([]*ServerConfig, int) { list = append(list, v) } } - } return list, cnt } -//get verify value -//when mode is webServer and vKey is not none -func getverifyval(vkey string) string { - if VerifyKey != "" { - return utils.Md5(VerifyKey) - } - return utils.Md5(vkey) +//获取客户端列表 +func GetClientList(start, length int) (list []*utils.Client, cnt int) { + list, cnt = CsvDb.GetClientList(start, length) + dealClientData(list) + return +} + +func dealClientData(list []*utils.Client) { + for _, v := range list { + if _, ok := Bridge.SignalList[v.Id]; ok { + v.IsConnect = true + } else { + v.IsConnect = false + } + v.Flow.InletFlow = 0 + v.Flow.ExportFlow = 0 + for _, h := range CsvDb.Hosts { + if h.ClientId == v.Id { + v.Flow.InletFlow += h.Flow.InletFlow + v.Flow.ExportFlow += h.Flow.ExportFlow + } + } + for _, t := range CsvDb.Tasks { + if t.ClientId == v.Id { + v.Flow.InletFlow += t.Flow.InletFlow + v.Flow.ExportFlow += t.Flow.ExportFlow + } + } + } + return +} + +//根据客户端id删除其所属的所有隧道和域名 +func DelTunnelAndHostByClientId(clientId int) { + for _, v := range CsvDb.Tasks { + if v.ClientId == clientId { + DelTask(v.Id) + } + } + for _, v := range CsvDb.Hosts { + if v.ClientId == clientId { + CsvDb.DelHost(v.Host) + } + } +} + +//关闭客户端连接 +func DelClientConnect(clientId int) { + Bridge.DelClientTunnel(clientId) + Bridge.DelClientSignal(clientId) +} + +func GetDashboardData() map[string]int { + data := make(map[string]int) + data["hostCount"] = len(CsvDb.Hosts) + data["clientCount"] = len(CsvDb.Clients) + list := CsvDb.Clients + dealClientData(list) + c := 0 + var in, out int64 + for _, v := range list { + if v.IsConnect { + c += 1 + } + in += v.Flow.InletFlow + out += v.Flow.ExportFlow + } + data["clientOnlineCount"] = c + data["inletFlowCount"] = int(in) + data["exportFlowCount"] = int(out) + for _, v := range CsvDb.Tasks { + switch v.Mode { + case "tunnelServer": + data["tunnelServerCount"] += 1 + case "socks5Server": + data["socks5ServerCount"] += 1 + case "httpProxyServer": + data["httpProxyServerCount"] += 1 + case "udpServer": + data["udpServerCount"] += 1 + } + } + return data } diff --git a/server/socks5.go b/server/socks5.go index e0cb2df..f8f9211 100755 --- a/server/socks5.go +++ b/server/socks5.go @@ -160,13 +160,14 @@ func (s *Sock5ModeServer) handleConnect(c net.Conn) { proxyConn, err := s.doConnect(c, connectMethod) defer func() { if s.config.Mux && proxyConn != nil { - s.bridge.ReturnTunnel(proxyConn, getverifyval(s.config.VerifyKey)) + s.bridge.ReturnTunnel(proxyConn, s.config.ClientId) } }() if err != nil { c.Close() } else { - utils.ReplayWaitGroup(proxyConn.Conn, c, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux) + out, in := utils.ReplayWaitGroup(proxyConn.Conn, c, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux) + s.FlowAdd(in, out) } } @@ -197,13 +198,14 @@ func (s *Sock5ModeServer) handleUDP(c net.Conn) { proxyConn, err := s.doConnect(c, associateMethod) defer func() { if s.config.Mux && proxyConn != nil { - s.bridge.ReturnTunnel(proxyConn, getverifyval(s.config.VerifyKey)) + s.bridge.ReturnTunnel(proxyConn, s.config.ClientId) } }() if err != nil { c.Close() } else { - utils.ReplayWaitGroup(proxyConn.Conn, c, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux) + out, in := utils.ReplayWaitGroup(proxyConn.Conn, c, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux) + s.FlowAdd(in, out) } } @@ -295,6 +297,7 @@ func (s *Sock5ModeServer) Start() error { } log.Fatal("accept error: ", err) } + s.ResetConfig() go s.handleConn(conn) } return nil @@ -306,7 +309,7 @@ func (s *Sock5ModeServer) Close() error { } //new -func NewSock5ModeServer(bridge *bridge.Tunnel, cnf *ServerConfig) *Sock5ModeServer { +func NewSock5ModeServer(bridge *bridge.Tunnel, cnf *utils.ServerConfig) *Sock5ModeServer { s := new(Sock5ModeServer) s.bridge = bridge s.config = cnf diff --git a/server/tcp.go b/server/tcp.go index ff749ed..d47ae43 100755 --- a/server/tcp.go +++ b/server/tcp.go @@ -12,33 +12,15 @@ import ( "strings" ) -//server base struct -type server struct { - bridge *bridge.Tunnel - config *ServerConfig -} - -func (s *server) GetTunnelAndWriteHost(connType string, cnf *ServerConfig, addr string) (*utils.Conn, error) { - var err error - link, err := s.bridge.GetTunnel(getverifyval(cnf.VerifyKey), cnf.CompressEncode, cnf.CompressDecode, cnf.Crypt, cnf.Mux) - if err != nil { - return nil, err - } - if _, err = link.WriteHost(connType, addr); err != nil { - link.Close() - return nil, err - } - return link, nil -} - type TunnelModeServer struct { server - process process - listener *net.TCPListener + errorContent []byte + process process + listener *net.TCPListener } //tcp|http|host -func NewTunnelModeServer(process process, bridge *bridge.Tunnel, cnf *ServerConfig) *TunnelModeServer { +func NewTunnelModeServer(process process, bridge *bridge.Tunnel, cnf *utils.ServerConfig) *TunnelModeServer { s := new(TunnelModeServer) s.bridge = bridge s.process = process @@ -49,6 +31,9 @@ func NewTunnelModeServer(process process, bridge *bridge.Tunnel, cnf *ServerConf //开始 func (s *TunnelModeServer) Start() error { var err error + if s.errorContent, err = utils.ReadAllFromFile(beego.AppPath + "/web/static/page/error.html"); err != nil { + s.errorContent = []byte("easyProxy 404") + } s.listener, err = net.ListenTCP("tcp", &net.TCPAddr{net.ParseIP("0.0.0.0"), s.config.TcpPort, ""}) if err != nil { return err @@ -62,6 +47,7 @@ func (s *TunnelModeServer) Start() error { log.Println(err) continue } + s.ResetConfig() go s.process(utils.NewConn(conn), s) } return nil @@ -70,20 +56,25 @@ func (s *TunnelModeServer) Start() error { //权限认证 func (s *TunnelModeServer) auth(r *http.Request, c *utils.Conn, u, p string) error { if u != "" && p != "" && !utils.CheckAuth(r, u, p) { - c.Write([]byte(utils.Unauthorized_BYTES)) + c.Write([]byte(utils.UnauthorizedBytes)) c.Close() return errors.New("401 Unauthorized") } return nil } +func (s *TunnelModeServer) writeConnFail(c net.Conn) { + c.Write([]byte(utils.ConnectionFailBytes)) + c.Write(s.errorContent) +} + //与客户端建立通道 -func (s *TunnelModeServer) dealClient(c *utils.Conn, cnf *ServerConfig, addr string, method string, rb []byte) error { +func (s *TunnelModeServer) dealClient(c *utils.Conn, cnf *utils.ServerConfig, addr string, method string, rb []byte) error { var link *utils.Conn var err error defer func() { if cnf.Mux && link != nil { - s.bridge.ReturnTunnel(link, getverifyval(cnf.VerifyKey)) + s.bridge.ReturnTunnel(link, cnf.ClientId) } }() if link, err = s.GetTunnelAndWriteHost(utils.CONN_TCP, cnf, addr); err != nil { @@ -97,7 +88,8 @@ func (s *TunnelModeServer) dealClient(c *utils.Conn, cnf *ServerConfig, addr str } else if rb != nil { link.WriteTo(rb, cnf.CompressEncode, cnf.Crypt) } - utils.ReplayWaitGroup(link.Conn, c.Conn, cnf.CompressEncode, cnf.CompressDecode, cnf.Crypt, cnf.Mux) + out, in := utils.ReplayWaitGroup(link.Conn, c.Conn, cnf.CompressEncode, cnf.CompressDecode, cnf.Crypt, cnf.Mux) + s.FlowAdd(in, out) } } return nil @@ -139,7 +131,7 @@ func (s *HostServer) Start() error { return nil } -func NewHostServer(cnf *ServerConfig) *HostServer { +func NewHostServer(cnf *utils.ServerConfig) *HostServer { s := new(HostServer) s.config = cnf return s diff --git a/server/udp.go b/server/udp.go index 9cda953..7ede79e 100755 --- a/server/udp.go +++ b/server/udp.go @@ -10,13 +10,12 @@ import ( ) type UdpModeServer struct { - bridge *bridge.Tunnel + server listener *net.UDPConn udpMap map[string]*utils.Conn - config *ServerConfig } -func NewUdpModeServer(bridge *bridge.Tunnel, cnf *ServerConfig) *UdpModeServer { +func NewUdpModeServer(bridge *bridge.Tunnel, cnf *utils.ServerConfig) *UdpModeServer { s := new(UdpModeServer) s.bridge = bridge s.udpMap = make(map[string]*utils.Conn) @@ -40,6 +39,7 @@ func (s *UdpModeServer) Start() error { } continue } + s.ResetConfig() go s.process(addr, data[:n]) } return nil @@ -47,7 +47,7 @@ func (s *UdpModeServer) Start() error { //TODO:效率问题有待解决 func (s *UdpModeServer) process(addr *net.UDPAddr, data []byte) { - conn, err := s.bridge.GetTunnel(getverifyval(s.config.VerifyKey), s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux) + conn, err := s.bridge.GetTunnel(s.config.ClientId, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux) if err != nil { log.Println(err) return @@ -60,21 +60,22 @@ func (s *UdpModeServer) process(addr *net.UDPAddr, data []byte) { defer func() { if conn != nil && s.config.Mux { conn.WriteTo([]byte(utils.IO_EOF), s.config.CompressEncode, s.config.Crypt) - s.bridge.ReturnTunnel(conn, getverifyval(s.config.VerifyKey)) + s.bridge.ReturnTunnel(conn, s.config.ClientId) } else { conn.Close() } }() if flag == utils.CONN_SUCCESS { - conn.WriteTo(data, s.config.CompressEncode, s.config.Crypt) - buf := make([]byte, 1024) - //conn.conn.SetReadDeadline(time.Now().Add(time.Duration(time.Second * 3))) - n, err := conn.ReadFrom(buf, s.config.CompressDecode, s.config.Crypt) + in, _ := conn.WriteTo(data, s.config.CompressEncode, s.config.Crypt) + buf := utils.BufPoolUdp.Get().([]byte) + out, err := conn.ReadFrom(buf, s.config.CompressDecode, s.config.Crypt) if err != nil || err == io.EOF { log.Println("revieve error:", err) return } - s.listener.WriteToUDP(buf[:n], addr) + s.listener.WriteToUDP(buf[:out], addr) + s.FlowAdd(int64(in), int64(out)) + utils.BufPoolUdp.Put(buf) } } } diff --git a/utils/conn.go b/utils/conn.go index e881296..d661942 100755 --- a/utils/conn.go +++ b/utils/conn.go @@ -107,14 +107,14 @@ func (s *SnappyConn) Write(b []byte) (n int, err error) { //snappy压缩读 包含解密 func (s *SnappyConn) Read(b []byte) (n int, err error) { + buf := bufPool.Get().([]byte) defer func() { if err == nil && n == len(IO_EOF) && string(b[:n]) == IO_EOF { err = io.EOF n = 0 } + bufPool.Put(buf) }() - buf := bufPool.Get().([]byte) - defer bufPool.Put(buf) if n, err = s.r.Read(buf); err != nil { return } @@ -153,8 +153,8 @@ func (s *Conn) ReadLen(cLen int) ([]byte, error) { buf = bufPoolSmall.Get().([]byte)[:cLen] defer bufPoolSmall.Put(buf) } else { - buf = bufPool.Get().([]byte)[:cLen] - defer bufPool.Put(buf) + buf = bufPoolMax.Get().([]byte)[:cLen] + defer bufPoolMax.Put(buf) } if n, err := io.ReadFull(s, buf); err != nil || n != cLen { return buf, errors.New("读取指定长度错误" + err.Error()) diff --git a/utils/file.go b/utils/file.go new file mode 100644 index 0000000..f892fa6 --- /dev/null +++ b/utils/file.go @@ -0,0 +1,487 @@ +package utils + +import ( + "easyProxy/utils" + "encoding/csv" + "errors" + "github.com/astaxie/beego" + "log" + "os" + "strconv" + "sync" +) + +var ( + CsvDb *Csv + once sync.Once +) + +type Flow struct { + ExportFlow int64 //出口流量 + InletFlow int64 //入口流量 +} + +type Client struct { + Cnf *ServerConfig + Id int //id + VerifyKey string //验证密钥 + Addr string //客户端ip地址 + Remark string //备注 + Status bool //是否开启 + IsConnect bool //是否连接 + Flow *Flow +} + +type ServerConfig struct { + TcpPort int //服务端与客户端通信端口 + VerifyKey string + Mode string //启动方式 + Target string //目标 + U string //socks5验证用户名 + P string //socks5验证密码 + Compress string //压缩方式 + Start int //是否开启 + IsRun int //是否在运行 + ClientStatus int //客s户端状态 + Crypt bool //是否加密 + Mux bool //是否加密 + CompressEncode int //加密方式 + CompressDecode int //解密方式 + Id int //Id + ClientId int //所属客户端id + UseClientCnf bool //是否继承客户端配置 + Flow *Flow + Remark string //备注 +} + +type HostList struct { + ClientId int //服务端与客户端通信端口 + Host string //启动方式 + Target string //目标 + HeaderChange string //host修改 + HostChange string //host修改 + Flow *Flow + Remark string //备注 +} + +func NewCsv() *Csv { + c := new(Csv) + return c +} + +type Csv struct { + Tasks []*ServerConfig + Path string + Hosts []*HostList //域名列表 + Clients []*Client //客户端 + ClientIncreaseId int //客户端id + TaskIncreaseId int //任务自增ID + sync.Mutex +} + +func (s *Csv) Init() { + s.LoadTaskFromCsv() + s.LoadHostFromCsv() + s.LoadClientFromCsv() +} + +func (s *Csv) StoreTasksToCsv() { + // 创建文件 + csvFile, err := os.Create(beego.AppPath + "/conf/tasks.csv") + if err != nil { + log.Fatalf(err.Error()) + } + defer csvFile.Close() + writer := csv.NewWriter(csvFile) + for _, task := range s.Tasks { + record := []string{ + strconv.Itoa(task.TcpPort), + task.Mode, + task.Target, + task.U, + task.P, + task.Compress, + strconv.Itoa(task.Start), + GetStrByBool(task.Crypt), + GetStrByBool(task.Mux), + strconv.Itoa(task.CompressEncode), + strconv.Itoa(task.CompressDecode), + strconv.Itoa(task.Id), + strconv.Itoa(task.ClientId), + strconv.FormatBool(task.UseClientCnf), + task.Remark, + } + err := writer.Write(record) + if err != nil { + log.Fatalf(err.Error()) + } + } + writer.Flush() +} + +func (s *Csv) openFile(path string) ([][]string, error) { + // 打开文件 + file, err := os.Open(path) + if err != nil { + panic(err) + } + defer file.Close() + + // 获取csv的reader + reader := csv.NewReader(file) + + // 设置FieldsPerRecord为-1 + reader.FieldsPerRecord = -1 + + // 读取文件中所有行保存到slice中 + return reader.ReadAll() +} + +func (s *Csv) LoadTaskFromCsv() { + path := beego.AppPath + "/conf/tasks.csv" + records, err := s.openFile(path) + if err != nil { + log.Fatal("配置文件打开错误:", path) + } + var tasks []*ServerConfig + // 将每一行数据保存到内存slice中 + for _, item := range records { + post := &ServerConfig{ + TcpPort: GetIntNoErrByStr(item[0]), + Mode: item[1], + Target: item[2], + U: item[3], + P: item[4], + Compress: item[5], + Start: GetIntNoErrByStr(item[6]), + Crypt: GetBoolByStr(item[7]), + Mux: GetBoolByStr(item[8]), + CompressEncode: GetIntNoErrByStr(item[9]), + CompressDecode: GetIntNoErrByStr(item[10]), + Id: GetIntNoErrByStr(item[11]), + ClientId: GetIntNoErrByStr(item[12]), + UseClientCnf: GetBoolByStr(item[13]), + Remark: item[14], + } + post.Flow = new(Flow) + tasks = append(tasks, post) + if post.Id > s.TaskIncreaseId { + s.TaskIncreaseId = post.Id + } + } + s.Tasks = tasks +} + +func (s *Csv) GetTaskId() int { + s.Lock() + defer s.Unlock() + s.TaskIncreaseId++ + return s.TaskIncreaseId +} + +func (s *Csv) GetIdByVerifyKey(vKey string, addr string) (int, error) { + s.Lock() + defer s.Unlock() + for _, v := range s.Clients { + if utils.Getverifyval(v.VerifyKey) == vKey && v.Status { + v.Addr = addr + return v.Id, nil + } + } + return 0, errors.New("not found") +} + +func (s *Csv) NewTask(t *ServerConfig) { + t.Flow = new(Flow) + s.Tasks = append(s.Tasks, t) + s.StoreTasksToCsv() +} + +func (s *Csv) UpdateTask(t *ServerConfig) error { + for k, v := range s.Tasks { + if v.Id == t.Id { + s.Tasks = append(s.Tasks[:k], s.Tasks[k+1:]...) + s.Tasks = append(s.Tasks, t) + s.StoreTasksToCsv() + return nil + } + } + return errors.New("不存在") +} + +func (s *Csv) DelTask(id int) error { + for k, v := range s.Tasks { + if v.Id == id { + s.Tasks = append(s.Tasks[:k], s.Tasks[k+1:]...) + s.StoreTasksToCsv() + return nil + } + } + return errors.New("不存在") +} + +func (s *Csv) GetTask(id int) (v *ServerConfig, err error) { + for _, v = range s.Tasks { + if v.Id == id { + return + } + } + err = errors.New("未找到") + return +} + +func (s *Csv) StoreHostToCsv() { + // 创建文件 + csvFile, err := os.Create(beego.AppPath + "/conf/hosts.csv") + if err != nil { + panic(err) + } + defer csvFile.Close() + // 获取csv的Writer + writer := csv.NewWriter(csvFile) + // 将map中的Post转换成slice,因为csv的Write需要slice参数 + // 并写入csv文件 + for _, host := range s.Hosts { + record := []string{ + host.Host, + host.Target, + strconv.Itoa(host.ClientId), + host.HeaderChange, + host.HostChange, + host.Remark, + } + err1 := writer.Write(record) + if err1 != nil { + panic(err1) + } + } + // 确保所有内存数据刷到csv文件 + writer.Flush() +} + +func (s *Csv) LoadClientFromCsv() { + path := beego.AppPath + "/conf/clients.csv" + records, err := s.openFile(path) + if err != nil { + log.Fatal("配置文件打开错误:", path) + } + var clients []*Client + // 将每一行数据保存到内存slice中 + for _, item := range records { + post := &Client{ + Id: GetIntNoErrByStr(item[0]), + VerifyKey: item[1], + Addr: item[2], + Remark: item[3], + Status: GetBoolByStr(item[4]), + Cnf: &ServerConfig{ + U: item[5], + P: item[6], + Crypt: GetBoolByStr(item[7]), + Mux: GetBoolByStr(item[8]), + Compress: item[9], + }, + } + if post.Id > s.ClientIncreaseId { + s.ClientIncreaseId = post.Id + } + post.Flow = new(Flow) + clients = append(clients, post) + } + s.Clients = clients +} + +func (s *Csv) LoadHostFromCsv() { + path := beego.AppPath + "/conf/hosts.csv" + records, err := s.openFile(path) + if err != nil { + log.Fatal("配置文件打开错误:", path) + } + var hosts []*HostList + // 将每一行数据保存到内存slice中 + for _, item := range records { + post := &HostList{ + ClientId: GetIntNoErrByStr(item[2]), + Host: item[0], + Target: item[1], + HeaderChange: item[3], + HostChange: item[4], + Remark: item[5], + } + post.Flow = new(Flow) + hosts = append(hosts, post) + } + s.Hosts = hosts +} + +func (s *Csv) DelHost(host string) error { + for k, v := range s.Hosts { + if v.Host == host { + s.Hosts = append(s.Hosts[:k], s.Hosts[k+1:]...) + s.StoreHostToCsv() + return nil + } + } + return errors.New("不存在") +} + +func (s *Csv) NewHost(t *HostList) { + t.Flow = new(Flow) + s.Hosts = append(s.Hosts, t) + s.StoreHostToCsv() + +} + +func (s *Csv) UpdateHost(t *HostList) error { + for k, v := range s.Hosts { + if v.Host == t.Host { + s.Hosts = append(s.Hosts[:k], s.Hosts[k+1:]...) + s.Hosts = append(s.Hosts, t) + s.StoreHostToCsv() + return nil + } + } + return errors.New("不存在") +} + +func (s *Csv) GetHostList(start, length int, id int) ([]*HostList, int) { + list := make([]*HostList, 0) + var cnt int + for _, v := range s.Hosts { + if id == 0 || v.ClientId == id { + cnt++ + if start--; start < 0 { + if length--; length > 0 { + list = append(list, v) + } + } + } + } + return list, cnt +} + +func (s *Csv) DelClient(id int) error { + for k, v := range s.Clients { + if v.Id == id { + s.Clients = append(s.Clients[:k], s.Clients[k+1:]...) + s.StoreClientsToCsv() + return nil + } + } + return errors.New("不存在") +} + +func (s *Csv) NewClient(c *Client) { + s.Lock() + defer s.Unlock() + c.Flow = new(Flow) + s.Clients = append(s.Clients, c) + s.StoreClientsToCsv() +} + +func (s *Csv) GetClientId() int { + s.Lock() + defer s.Unlock() + s.ClientIncreaseId++ + return s.ClientIncreaseId +} + +func (s *Csv) UpdateClient(t *Client) error { + s.Lock() + defer s.Unlock() + for k, v := range s.Clients { + if v.Id == t.Id { + s.Clients = append(s.Clients[:k], s.Clients[k+1:]...) + s.Clients = append(s.Clients, t) + s.StoreClientsToCsv() + return nil + } + } + return errors.New("不存在") +} + +func (s *Csv) GetClientList(start, length int) ([]*Client, int) { + list := make([]*Client, 0) + var cnt int + for _, v := range s.Clients { + cnt++ + if start--; start < 0 { + if length--; length > 0 { + list = append(list, v) + } + } + } + return list, cnt +} + +func (s *Csv) GetClient(id int) (v *Client, err error) { + for _, v = range s.Clients { + if v.Id == id { + return + } + } + err = errors.New("未找到") + return +} +func (s *Csv) StoreClientsToCsv() { + // 创建文件 + csvFile, err := os.Create(beego.AppPath + "/conf/clients.csv") + if err != nil { + log.Fatalf(err.Error()) + } + defer csvFile.Close() + writer := csv.NewWriter(csvFile) + for _, client := range s.Clients { + record := []string{ + strconv.Itoa(client.Id), + client.VerifyKey, + client.Addr, + client.Remark, + strconv.FormatBool(client.Status), + client.Cnf.U, + client.Cnf.P, + utils.GetStrByBool(client.Cnf.Crypt), + utils.GetStrByBool(client.Cnf.Mux), + client.Cnf.Compress, + } + err := writer.Write(record) + if err != nil { + log.Fatalf(err.Error()) + } + } + writer.Flush() +} + +//init csv from file +func GetCsvDb() *Csv { + once.Do(func() { + CsvDb = NewCsv() + CsvDb.Init() + }) + return CsvDb +} + +//深拷贝serverConfig +func DeepCopyConfig(c *ServerConfig) *ServerConfig { + return &ServerConfig{ + TcpPort: c.TcpPort, + VerifyKey: c.VerifyKey, + Mode: c.Mode, + Target: c.Target, + U: c.U, + P: c.P, + Compress: c.Compress, + Start: c.Start, + IsRun: c.IsRun, + ClientStatus: c.ClientStatus, + Crypt: c.Crypt, + Mux: c.Mux, + CompressEncode: c.CompressEncode, + CompressDecode: c.CompressDecode, + Id: c.Id, + ClientId: c.ClientId, + UseClientCnf: c.UseClientCnf, + Flow: c.Flow, + Remark: c.Remark, + } +} diff --git a/utils/pool.go b/utils/pool.go index 2dcb7fc..9fab926 100644 --- a/utils/pool.go +++ b/utils/pool.go @@ -3,7 +3,8 @@ package utils import "sync" const poolSize = 64 * 1024 -const poolSizeSmall = 10 +const poolSizeSmall = 100 +const poolSizeUdp = 1472 const poolSizeCopy = 32 * 1024 var bufPool = sync.Pool{ @@ -11,6 +12,16 @@ var bufPool = sync.Pool{ return make([]byte, poolSize) }, } +var BufPoolUdp = sync.Pool{ + New: func() interface{} { + return make([]byte, poolSizeUdp) + }, +} +var bufPoolMax = sync.Pool{ + New: func() interface{} { + return make([]byte, poolSize) + }, +} var bufPoolSmall = sync.Pool{ New: func() interface{} { return make([]byte, poolSizeSmall) diff --git a/utils/util.go b/utils/util.go index 3e27dd2..89b9b11 100755 --- a/utils/util.go +++ b/utils/util.go @@ -3,9 +3,11 @@ package utils import ( "encoding/base64" "io" + "io/ioutil" "log" "net" "net/http" + "os" "regexp" "strconv" "strings" @@ -18,48 +20,52 @@ const ( COMPRESS_NONE_DECODE COMPRESS_SNAPY_ENCODE COMPRESS_SNAPY_DECODE - VERIFY_EER = "vkey" - WORK_MAIN = "main" - WORK_CHAN = "chan" - RES_SIGN = "sign" - RES_MSG = "msg0" - CONN_SUCCESS = "sucs" - CONN_ERROR = "fail" - TEST_FLAG = "tst" - CONN_TCP = "tcp" - CONN_UDP = "udp" - Unauthorized_BYTES = `HTTP/1.1 401 Unauthorized + VERIFY_EER = "vkey" + WORK_MAIN = "main" + WORK_CHAN = "chan" + RES_SIGN = "sign" + RES_MSG = "msg0" + CONN_SUCCESS = "sucs" + CONN_ERROR = "fail" + TEST_FLAG = "tst" + CONN_TCP = "tcp" + CONN_UDP = "udp" + UnauthorizedBytes = `HTTP/1.1 401 Unauthorized Content-Type: text/plain; charset=utf-8 WWW-Authenticate: Basic realm="easyProxy" 401 Unauthorized` - IO_EOF = "PROXYEOF" + IO_EOF = "PROXYEOF" + ConnectionFailBytes = `HTTP/1.1 404 Not Found + +` ) //copy -func Relay(in, out net.Conn, compressType int, crypt, mux bool) { +func Relay(in, out net.Conn, compressType int, crypt, mux bool) (n int64, err error) { switch compressType { case COMPRESS_SNAPY_ENCODE: - copyBuffer(NewSnappyConn(in, crypt), out) + n, err = copyBuffer(NewSnappyConn(in, crypt), out) out.Close() NewSnappyConn(in, crypt).Write([]byte(IO_EOF)) case COMPRESS_SNAPY_DECODE: - copyBuffer(in, NewSnappyConn(out, crypt)) + n, err = copyBuffer(in, NewSnappyConn(out, crypt)) in.Close() if !mux { out.Close() } case COMPRESS_NONE_ENCODE: - copyBuffer(NewCryptConn(in, crypt), out) + n, err = copyBuffer(NewCryptConn(in, crypt), out) out.Close() NewCryptConn(in, crypt).Write([]byte(IO_EOF)) case COMPRESS_NONE_DECODE: - copyBuffer(in, NewCryptConn(out, crypt)) + n, err = copyBuffer(in, NewCryptConn(out, crypt)) in.Close() if !mux { out.Close() } } + return } //判断压缩方式 @@ -145,7 +151,6 @@ func GetIntNoErrByStr(str string) int { return i } - // io.copy的优化版,读取buffer长度原为32*1024,与snappy不同,导致读取出的内容存在差异,不利于解密 //内存优化 用到pool,快速回收 func copyBuffer(dst io.Writer, src io.Reader) (written int64, err error) { @@ -167,7 +172,7 @@ func copyBuffer(dst io.Writer, src io.Reader) (written int64, err error) { err = io.ErrShortWrite break } - }else { + } else { bufPoolCopy.Put(buf) } if er != nil { @@ -199,15 +204,17 @@ func Getverifyval(vkey string) string { } //wait replay group -func ReplayWaitGroup(conn1 net.Conn, conn2 net.Conn, compressEncode, compressDecode int, crypt, mux bool) { +//conn1 网桥 conn2 +func ReplayWaitGroup(conn1 net.Conn, conn2 net.Conn, compressEncode, compressDecode int, crypt, mux bool) (out int64, in int64) { var wg sync.WaitGroup wg.Add(1) go func() { - Relay(conn1, conn2, compressEncode, crypt, mux) + in, _ = Relay(conn1, conn2, compressEncode, crypt, mux) wg.Done() }() - Relay(conn2, conn1, compressDecode, crypt, mux) + out, _ = Relay(conn2, conn1, compressDecode, crypt, mux) wg.Wait() + return } func ChangeHostAndHeader(r *http.Request, host string, header string, addr string) { @@ -227,3 +234,12 @@ func ChangeHostAndHeader(r *http.Request, host string, header string, addr strin r.Header.Set("X-Forwarded-For", addr) r.Header.Set("X-Real-IP", addr) } + +func ReadAllFromFile(filePth string) ([]byte, error) { + f, err := os.Open(filePth) + if err != nil { + return nil, err + } + + return ioutil.ReadAll(f) +} diff --git a/web/controllers/base.go b/web/controllers/base.go index 38342e9..7140f20 100755 --- a/web/controllers/base.go +++ b/web/controllers/base.go @@ -27,14 +27,18 @@ func (s *BaseController) Prepare() { //加载模板 func (s *BaseController) display(tpl ...string) { var tplname string + if s.Data["menu"] == nil { + s.Data["menu"] = s.actionName + } if len(tpl) > 0 { tplname = strings.Join([]string{tpl[0], "html"}, ".") } else { tplname = s.controllerName + "/" + s.actionName + ".html" } - s.Data["menu"] = s.actionName ip := s.Ctx.Request.Host - s.Data["ip"] = utils.GetHostByName(ip[0:strings.LastIndex(ip, ":")]) + if strings.LastIndex(ip, ":") > 0 { + s.Data["ip"] = utils.GetHostByName(ip[0:]) + } s.Data["p"] = server.Bridge.TunnelPort s.Data["proxyPort"] = beego.AppConfig.String("hostPort") s.Layout = "public/layout.html" diff --git a/web/controllers/client.go b/web/controllers/client.go new file mode 100644 index 0000000..c7d943d --- /dev/null +++ b/web/controllers/client.go @@ -0,0 +1,100 @@ +package controllers + +import ( + "github.com/cnlh/easyProxy/server" + "github.com/cnlh/easyProxy/utils" +) + +type ClientController struct { + BaseController +} + +func (s *ClientController) Client() { + if s.Ctx.Request.Method == "GET" { + s.Data["menu"] = "client" + s.SetInfo("客户端管理") + s.display("client/list") + return + } + start, length := s.GetAjaxParams() + list, cnt := server.GetClientList(start, length) + s.AjaxTable(list, cnt, cnt) +} + +//添加客户端 +func (s *ClientController) Add() { + if s.Ctx.Request.Method == "GET" { + s.Data["menu"] = "client" + s.SetInfo("新增") + s.display() + } else { + t := &utils.Client{ + VerifyKey: utils.GetRandomString(16), + Id: server.CsvDb.GetClientId(), + Status: true, + Remark: s.GetString("Remark"), + Cnf: &utils.ServerConfig{ + U: s.GetString("u"), + P: s.GetString("p"), + Compress: s.GetString("compress"), + Crypt: s.GetBoolNoErr("crypt"), + Mux: s.GetBoolNoErr("mux"), + }, + } + server.CsvDb.NewClient(t) + s.AjaxOk("添加成功") + } +} + +//修改客户端 +func (s *ClientController) Edit() { + if s.Ctx.Request.Method == "GET" { + s.Data["menu"] = "client" + id := s.GetIntNoErr("id") + if c, err := server.CsvDb.GetClient(id); err != nil { + s.error() + } else { + s.Data["c"] = c + } + s.SetInfo("修改") + s.display() + } else { + id := s.GetIntNoErr("Id") + if c, err := server.CsvDb.GetClient(id); err != nil { + s.error() + } else { + c.Remark = s.GetString("Remark") + c.Cnf.U = s.GetString("u") + c.Cnf.P = s.GetString("p") + c.Cnf.Compress = s.GetString("compress") + c.Cnf.Crypt = s.GetBoolNoErr("crypt") + c.Cnf.Mux = s.GetBoolNoErr("mux") + server.CsvDb.UpdateClient(c) + } + s.AjaxOk("修改成功") + } +} + +//更改状态 +func (s *ClientController) ChangeStatus() { + id := s.GetIntNoErr("id") + if client, err := server.CsvDb.GetClient(id); err == nil { + client.Status = s.GetBoolNoErr("status") + if client.Status == false { + server.DelClientConnect(client.Id) + } + s.AjaxOk("修改成功") + } + s.AjaxErr("修改失败") +} + +//删除客户端 +func (s *ClientController) Del() { + id := s.GetIntNoErr("id") + if err := server.CsvDb.DelClient(id); err != nil { + s.AjaxErr("删除失败") + } + server.DelTunnelAndHostByClientId(id) + server.DelClientConnect(id) + s.AjaxOk("删除成功") +} diff --git a/web/controllers/index.go b/web/controllers/index.go index cb7896e..d827542 100755 --- a/web/controllers/index.go +++ b/web/controllers/index.go @@ -10,9 +10,14 @@ type IndexController struct { } func (s *IndexController) Index() { - s.SetInfo("使用说明") + s.Data["data"] = server.GetDashboardData() + s.SetInfo("dashboard") s.display("index/index") } +func (s *IndexController) Help() { + s.SetInfo("使用说明") + s.display("index/help") +} func (s *IndexController) Tcp() { s.SetInfo("tcp隧道管理") @@ -44,30 +49,44 @@ func (s *IndexController) Host() { s.display("index/list") } +func (s *IndexController) All() { + s.Data["menu"] = "client" + clientId := s.GetString("client_id") + s.Data["client_id"] = clientId + s.SetInfo("客户端" + clientId + "的所有隧道") + s.display("index/list") +} + func (s *IndexController) GetServerConfig() { start, length := s.GetAjaxParams() taskType := s.GetString("type") - list, cnt := server.GetServerConfig(start, length, taskType) + clientId := s.GetIntNoErr("client_id") + list, cnt := server.GetServerConfig(start, length, taskType, clientId) s.AjaxTable(list, cnt, cnt) } func (s *IndexController) Add() { if s.Ctx.Request.Method == "GET" { s.Data["type"] = s.GetString("type") + s.Data["client_id"] = s.GetString("client_id") s.SetInfo("新增") s.display() } else { - t := &server.ServerConfig{ - TcpPort: s.GetIntNoErr("port"), - Mode: s.GetString("type"), - Target: s.GetString("target"), - VerifyKey: utils.GetRandomString(16), - U: s.GetString("u"), - P: s.GetString("p"), - Compress: s.GetString("compress"), - Crypt: utils.GetBoolByStr(s.GetString("crypt")), - Mux: utils.GetBoolByStr(s.GetString("mux")), - IsRun: 0, + t := &utils.ServerConfig{ + TcpPort: s.GetIntNoErr("port"), + Mode: s.GetString("type"), + Target: s.GetString("target"), + U: s.GetString("u"), + P: s.GetString("p"), + Compress: s.GetString("compress"), + Crypt: s.GetBoolNoErr("crypt"), + Mux: s.GetBoolNoErr("mux"), + IsRun: 0, + Id: server.CsvDb.GetTaskId(), + ClientId: s.GetIntNoErr("client_id"), + UseClientCnf: s.GetBoolNoErr("use_client"), + Start: 1, + Remark: s.GetString("remark"), } server.CsvDb.NewTask(t) if err := server.AddTask(t); err != nil { @@ -79,9 +98,9 @@ func (s *IndexController) Add() { } func (s *IndexController) Edit() { + id := s.GetIntNoErr("id") if s.Ctx.Request.Method == "GET" { - vKey := s.GetString("vKey") - if t, err := server.CsvDb.GetTask(vKey); err != nil { + if t, err := server.CsvDb.GetTask(id); err != nil { s.error() } else { s.Data["t"] = t @@ -89,44 +108,46 @@ func (s *IndexController) Edit() { s.SetInfo("修改") s.display() } else { - vKey := s.GetString("vKey") - if t, err := server.CsvDb.GetTask(vKey); err != nil { + if t, err := server.CsvDb.GetTask(id); err != nil { s.error() } else { t.TcpPort = s.GetIntNoErr("port") t.Mode = s.GetString("type") t.Target = s.GetString("target") + t.Id = id + t.ClientId = s.GetIntNoErr("client_id") t.U = s.GetString("u") t.P = s.GetString("p") t.Compress = s.GetString("compress") - t.Crypt = utils.GetBoolByStr(s.GetString("crypt")) - t.Mux = utils.GetBoolByStr(s.GetString("mux")) + t.Crypt = s.GetBoolNoErr("crypt") + t.Mux = s.GetBoolNoErr("mux") + t.UseClientCnf = s.GetBoolNoErr("use_client") + t.Remark = s.GetString("remark") server.CsvDb.UpdateTask(t) - server.StopServer(t.VerifyKey) - server.StartTask(t.VerifyKey) } s.AjaxOk("修改成功") } } func (s *IndexController) Stop() { - vKey := s.GetString("vKey") - if err := server.StopServer(vKey); err != nil { + id := s.GetIntNoErr("id") + if err := server.StopServer(id); err != nil { s.AjaxErr("停止失败") } s.AjaxOk("停止成功") } + func (s *IndexController) Del() { - vKey := s.GetString("vKey") - if err := server.DelTask(vKey); err != nil { + id := s.GetIntNoErr("id") + if err := server.DelTask(id); err != nil { s.AjaxErr("删除失败") } s.AjaxOk("删除成功") } func (s *IndexController) Start() { - vKey := s.GetString("vKey") - if err := server.StartTask(vKey); err != nil { + id := s.GetIntNoErr("id") + if err := server.StartTask(id); err != nil { s.AjaxErr("开启失败") } s.AjaxOk("开启成功") @@ -134,13 +155,14 @@ func (s *IndexController) Start() { func (s *IndexController) HostList() { if s.Ctx.Request.Method == "GET" { - s.Data["vkey"] = s.GetString("vkey") + s.Data["client_id"] = s.GetString("client_id") + s.Data["menu"] = "host" s.SetInfo("域名列表") s.display("index/hlist") } else { start, length := s.GetAjaxParams() - vkey := s.GetString("vkey") - list, cnt := server.CsvDb.GetHostList(start, length, vkey) + clientId := s.GetIntNoErr("client_id") + list, cnt := server.CsvDb.GetHostList(start, length, clientId) s.AjaxTable(list, cnt, cnt) } } @@ -155,16 +177,18 @@ func (s *IndexController) DelHost() { func (s *IndexController) AddHost() { if s.Ctx.Request.Method == "GET" { - s.Data["vkey"] = s.GetString("vkey") + s.Data["client_id"] = s.GetString("client_id") + s.Data["menu"] = "host" s.SetInfo("新增") s.display("index/hadd") } else { - h := &server.HostList{ - Vkey: s.GetString("vkey"), + h := &utils.HostList{ + ClientId: s.GetIntNoErr("client_id"), Host: s.GetString("host"), Target: s.GetString("target"), HeaderChange: s.GetString("header"), HostChange: s.GetString("hostchange"), + Remark: s.GetString("remark"), } server.CsvDb.NewHost(h) s.AjaxOk("添加成功") @@ -172,26 +196,26 @@ func (s *IndexController) AddHost() { } func (s *IndexController) EditHost() { + host := s.GetString("host") if s.Ctx.Request.Method == "GET" { - host := s.GetString("host") - if h, t, err := server.GetKeyByHost(host); err != nil { + s.Data["menu"] = "host" + if h, _, err := server.GetKeyByHost(host); err != nil { s.error() } else { - s.Data["t"] = t s.Data["h"] = h } s.SetInfo("修改") s.display("index/hedit") } else { - host := s.GetString("host") if h, _, err := server.GetKeyByHost(host); err != nil { s.error() } else { - h.Vkey = s.GetString("vkey") + h.ClientId = s.GetIntNoErr("client_id") h.Host = s.GetString("nhost") h.Target = s.GetString("target") h.HeaderChange = s.GetString("header") h.HostChange = s.GetString("hostchange") + h.Remark = s.GetString("remark") server.CsvDb.UpdateHost(h) } s.AjaxOk("修改成功") diff --git a/web/routers/router.go b/web/routers/router.go index c411dbb..e16d8bd 100755 --- a/web/routers/router.go +++ b/web/routers/router.go @@ -9,4 +9,5 @@ func init() { beego.Router("/", &controllers.IndexController{}, "*:Index") beego.AutoRouter(&controllers.IndexController{}) beego.AutoRouter(&controllers.LoginController{}) + beego.AutoRouter(&controllers.ClientController{}) } diff --git a/web/static/js/chart.js b/web/static/js/chart.js new file mode 100755 index 0000000..b0a6b9c --- /dev/null +++ b/web/static/js/chart.js @@ -0,0 +1,3477 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: 1.0.1 + * + * Copyright 2015 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ + + +(function(){ + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; + + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context){ + var chart = this; + this.canvas = context.canvas; + + this.ctx = context; + + //Variables global to the chart + var computeDimension = function(element,dimension) + { + if (element['offset'+dimension]) + { + return element['offset'+dimension]; + } + else + { + return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); + } + } + + var width = this.width = computeDimension(context.canvas,'Width'); + var height = this.height = computeDimension(context.canvas,'Height'); + + // Firefox requires this to work correctly + context.canvas.width = width; + context.canvas.height = height; + + var width = this.width = context.canvas.width; + var height = this.height = context.canvas.height; + this.aspectRatio = this.width / this.height; + //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. + helpers.retinaScale(this); + + return this; + }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "inherit", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: true, + + // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container + maintainAspectRatio: true, + + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, + + // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function + customTooltips: false, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "inherit", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "inherit", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} + + } + }; + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //Global Chart helpers object for utility methods and classes + var helpers = Chart.helpers = {}; + + //-- Basic js utility methods + var each = helpers.each = function(loopable,callback,self){ + var additionalArgs = Array.prototype.slice.call(arguments, 3); + // Check to see if null or undefined firstly. + if (loopable){ + if (loopable.length === +loopable.length){ + var i; + for (i=0; i= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)){ + return currentItem; + } + } + }, + inherits = helpers.inherits = function(extensions){ + //Basic javascript inheritance based on the model created in Backbone.js + var parent = this; + var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; + + var Surrogate = function(){ this.constructor = ChartElement;}; + Surrogate.prototype = parent.prototype; + ChartElement.prototype = new Surrogate(); + + ChartElement.extend = inherits; + + if (extensions) extend(ChartElement.prototype, extensions); + + ChartElement.__super__ = parent.prototype; + + return ChartElement; + }, + noop = helpers.noop = function(){}, + uid = helpers.uid = (function(){ + var id=0; + return function(){ + return "chart-" + id++; + }; + })(), + warn = helpers.warn = function(str){ + //Method for warning of errors + if (window.console && typeof window.console.warn == "function") console.warn(str); + }, + amd = helpers.amd = (typeof define == 'function' && define.amd), + //-- Math methods + isNumber = helpers.isNumber = function(n){ + return !isNaN(parseFloat(n)) && isFinite(n); + }, + max = helpers.max = function(array){ + return Math.max.apply( Math, array ); + }, + min = helpers.min = function(array){ + return Math.min.apply( Math, array ); + }, + cap = helpers.cap = function(valueToCap,maxValue,minValue){ + if(isNumber(maxValue)) { + if( valueToCap > maxValue ) { + return maxValue; + } + } + else if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; + } + } + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num){ + if (num%1!==0 && isNumber(num)){ + return num.toString().split(".")[1].length; + } + else { + return 0; + } + }, + toRadians = helpers.radians = function(degrees){ + return degrees * (Math.PI/180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ + angle += Math.PI*2; + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth){ + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), + d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), + fa=t*d01/(d01+d12),// scaling factor for triangle Ta + fb=t*d12/(d01+d12); + return { + inner : { + x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) + }, + outer : { + x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) + } + }; + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ + + //Set a minimum step of two - a point at the top of the graph, and a point at the base + var minSteps = 2, + maxSteps = Math.floor(drawingSize/(textSize * 1.5)), + skipFitting = (minSteps >= maxSteps); + + var maxValue = max(valuesArray), + minValue = min(valuesArray); + + // We need some degree of seperation here to calculate the scales if all the values are the same + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue){ + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero){ + minValue -= 0.5; + } + else{ + // Make up a whole number above the values + maxValue += 0.5; + } + } + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if(numberOfSteps > maxSteps){ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0){ + skipFitting = true; + } + } + //We can fit in double the amount of scale points on the scale + else{ + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0){ + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if(stepValue/2 % 1 === 0){ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + //If it would make it a float break out of the loop + else{ + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else{ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + + } + } + + if (skipFitting){ + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; + } + + return { + steps : numberOfSteps, + stepValue : stepValue, + min : graphMin, + max : graphMin + (numberOfSteps * stepValue) + }; + + }, + /* jshint ignore:start */ + // Blows up jshint errors based on the new Function constructor + //Templating methods + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + template = helpers.template = function(templateString, valuesObject){ + + // If templateString is function rather than string-template - call the function for valuesObject + + if(templateString instanceof Function){ + return templateString(valuesObject); + } + + var cache = {}; + function tmpl(str, data){ + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + } + return tmpl(templateString,valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ + var labelsArray = new Array(numberOfSteps); + if (labelTemplateString){ + each(labelsArray,function(val,index){ + labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function (t) { + return t; + }, + easeInQuad: function (t) { + return t * t; + }, + easeOutQuad: function (t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function (t) { + return t * t * t; + }, + easeOutCubic: function (t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function (t) { + return t * t * t * t; + }, + easeOutQuart: function (t) { + return -1 * ((t = t / 1 - 1) * t * t * t - 1); + }, + easeInOutQuart: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function (t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function (t) { + return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); + }, + easeInOutQuint: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function (t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function (t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function (t) { + return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); + }, + easeInOutExpo: function (t) { + if (t === 0) return 0; + if (t === 1) return 1; + if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (t) { + if (t >= 1) return t; + return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); + }, + easeOutCirc: function (t) { + return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); + }, + easeInOutCirc: function (t) { + if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + easeInElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + }, + easeOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; + }, + easeInOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1 / 2) == 2) return 1; + if (!p) p = 1 * (0.3 * 1.5); + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); + return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: function (t) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function (t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); + } + }, + easeInOutBounce: function (t) { + if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function(){ + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function(){ + currentStep++; + var stepDecimal = currentStep/totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); + onProgress.call(chartInstance,easeDecimal,stepDecimal); + if (currentStep < totalSteps){ + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else{ + onComplete.apply(chartInstance); + } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt){ + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches){ + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } + else{ + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x : mouseX, + y : mouseY + }; + + }, + addEvent = helpers.addEvent = function(node,eventType,method){ + if (node.addEventListener){ + node.addEventListener(eventType,method); + } else if (node.attachEvent){ + node.attachEvent("on"+eventType, method); + } else { + node["on"+eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler){ + if (node.removeEventListener){ + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent){ + node.detachEvent("on"+eventType,handler); + } else{ + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents,function(eventName){ + chartInstance.events[eventName] = function(){ + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler,eventName){ + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumWidth = helpers.getMaximumWidth = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientWidth; + }, + getMaximumHeight = helpers.getMaximumHeight = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientHeight; + }, + getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support + retinaScale = helpers.retinaScale = function(chart){ + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + }, + //-- Canvas methods + clear = helpers.clear = function(chart){ + chart.ctx.clearRect(0,0,chart.width,chart.height); + }, + fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ + return fontStyle + " " + pixelSize+"px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ + ctx.font = font; + var longest = 0; + each(arrayOfStrings,function(string){ + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }; + + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data,options,chart){ + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive){ + this.resize(); + } + this.initialize.call(this,data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype,{ + initialize : function(){return this;}, + clear : function(){ + clear(this.chart); + return this; + }, + stop : function(){ + // Stops any current animation loop occuring + cancelAnimFrame(this.animationFrame); + return this; + }, + resize : function(callback){ + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumWidth(this.chart.canvas), + newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function"){ + callback.apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this; + }, + reflow : noop, + render : function(reflow){ + if (reflow){ + this.reflow(); + } + if (this.options.animation && !reflow){ + helpers.animationLoop( + this.draw, + this.options.animationSteps, + this.options.animationEasing, + this.options.onAnimationProgress, + this.options.onAnimationComplete, + this + ); + } + else{ + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + generateLegend : function(){ + return template(this.options.legendTemplate,this); + }, + destroy : function(){ + this.clear(); + unbindEvents(this, this.events); + var canvas = this.chart.canvas; + + // Reset canvas height/width attributes starts a fresh with the canvas context + canvas.width = this.chart.width; + canvas.height = this.chart.height; + + // < IE9 doesn't support removeProperty + if (canvas.style.removeProperty) { + canvas.style.removeProperty('width'); + canvas.style.removeProperty('height'); + } else { + canvas.style.removeAttribute('width'); + canvas.style.removeAttribute('height'); + } + + delete Chart.instances[this.id]; + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; + } + + each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; + } + else{ + this.activeElements = ChartElements; + } + this.draw(); + if(this.options.customTooltips){ + this.options.customTooltips(false); + } + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ + Elements.push(dataCollection[dataIndex]); + } + }); + + helpers.each(Elements, function(element) { + xPositions.push(element.x); + yPositions.push(element.y); + + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx, + custom: this.options.customTooltips + }).draw(); + + } else { + each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: template(this.options.tooltipTemplate, Element), + chart: this.chart, + custom: this.options.customTooltips + }).draw(); + }, this); + } + } + return this; + }, + toBase64Image : function(){ + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions){ + + var parent = this; + + var ChartType = function(){ + return parent.apply(this,arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name){ + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data,options){ + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data,config,this); + }; + } else{ + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration){ + extend(this,configuration); + this.initialize.apply(this,arguments); + this.save(); + }; + extend(Chart.Element.prototype,{ + initialize : function(){}, + restore : function(props){ + if (!props){ + extend(this,this._saved); + } else { + each(props,function(key){ + this[key] = this._saved[key]; + },this); + } + return this; + }, + save : function(){ + this._saved = clone(this); + delete this._saved._saved; + return this; + }, + update : function(newProps){ + each(newProps,function(value,key){ + this._saved[key] = this[key]; + this[key] = value; + },this); + return this; + }, + transition : function(props,ease){ + each(props,function(value,key){ + this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; + },this); + return this; + }, + tooltipPosition : function(){ + return { + x : this.x, + y : this.y + }; + }, + hasValue: function(){ + return isNumber(this.value); + } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + display: true, + inRange: function(chartX,chartY){ + var hitDetectionRange = this.hitDetectionRadius + this.radius; + return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); + }, + draw : function(){ + if (this.display){ + var ctx = this.ctx; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.closePath(); + + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.stroke(); + } + + + //Quick debug for bezier curve splining + //Highlights control points and the line between them. + //Handy for dev - stripped in the min version. + + // ctx.save(); + // ctx.fillStyle = "black"; + // ctx.strokeStyle = "black" + // ctx.beginPath(); + // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.beginPath(); + // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); + // ctx.lineTo(this.x, this.y); + // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); + // ctx.stroke(); + + // ctx.restore(); + + + + } + }); + + Chart.Arc = Chart.Element.extend({ + inRange : function(chartX,chartY){ + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition : function(){ + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x : this.x + (Math.cos(centreAngle) * rangeFromCentre), + y : this.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw : function(animationPercent){ + + var easingDecimal = animationPercent || 1; + + var ctx = this.ctx; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showStroke){ + ctx.stroke(); + } + } + }); + + Chart.Rectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfWidth = this.width/2, + leftX = this.x - halfWidth, + rightX = this.x + halfWidth, + top = this.base - (this.base - this.y), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, this.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, this.base); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + height : function(){ + return this.base - this.y; + }, + inRange : function(chartX,chartY){ + return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); + } + }); + + Chart.Tooltip = Chart.Element.extend({ + draw : function(){ + + var ctx = this.chart.ctx; + + ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.xAlign = "center"; + this.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = this.caretPadding = 2; + + var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, + tooltipRectHeight = this.fontSize + 2*this.yPadding, + tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; + + if (this.x + tooltipWidth/2 >this.chart.width){ + this.xAlign = "left"; + } else if (this.x - tooltipWidth/2 < 0){ + this.xAlign = "right"; + } + + if (this.y - tooltipHeight < 0){ + this.yAlign = "below"; + } + + + var tooltipX = this.x - tooltipWidth/2, + tooltipY = this.y - tooltipHeight; + + ctx.fillStyle = this.fillColor; + + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + switch(this.yAlign) + { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(this.x,this.y - caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = this.y + caretPadding + this.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(this.x, this.y + caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch(this.xAlign) + { + case "left": + tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); + break; + case "right": + tooltipX = this.x - (this.cornerRadius + this.caretHeight); + break; + } + + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = this.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); + } + } + }); + + Chart.MultiTooltip = Chart.Element.extend({ + initialize : function(){ + this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); + + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; + + this.ctx.font = this.titleFont; + + var titleWidth = this.ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth,titleWidth]); + + this.width = longestTextWidth + (this.xPadding*2); + + + var halfHeight = this.height/2; + + //Check to ensure the height will fit on the canvas + if (this.y - halfHeight < 0 ){ + this.y = halfHeight; + } else if (this.y + halfHeight > this.chart.height){ + this.y = this.chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this.chart.width/2){ + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + + + }, + getLineHeight : function(index){ + var baseLineHeight = this.y - (this.height/2) + this.yPadding, + afterTitleIndex = index-1; + + //If the index is zero, we're getting the title + if (index === 0){ + return baseLineHeight + this.titleFontSize/2; + } else{ + return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + } + + }, + draw : function(){ + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); + var ctx = this.ctx; + ctx.fillStyle = this.fillColor; + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.titleTextColor; + ctx.font = this.titleFont; + + ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); + + ctx.font = this.font; + helpers.each(this.labels,function(label,index){ + ctx.fillStyle = this.textColor; + ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = this.legendColorBackground; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + + },this); + } + } + }); + + Chart.Scale = Chart.Element.extend({ + initialize : function(){ + this.fit(); + }, + buildYLabels : function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; + }, + addXLabel : function(label){ + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel : function(){ + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function(){ + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* + * This sets what is returned from calculateScaleRange as static properties of this class: + * + this.steps; + this.stepValue; + this.min; + this.max; + * + */ + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while((cachedHeight > this.endPoint - this.startPoint)){ + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth){ + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation : function(){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth/2 + 3; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + + this.xLabelRotation = 0; + if (this.display){ + var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize/2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0){ + this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; + } + } + else{ + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function(){ + return this.startPoint - this.endPoint; + }, + calculateY : function(value){ + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + update : function(newProps){ + helpers.extend(this, newProps); + this.fit(); + }, + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter), + drawHorizontalLine = this.showHorizontalLines; + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + + // This is X axis, so draw it + if (index === 0 && !drawHorizontalLine){ + drawHorizontalLine = true; + } + + if (drawHorizontalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + if(drawHorizontalLine){ + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + } + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + each(this.xLabels,function(label,index){ + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0), + drawVerticalLine = this.showVerticalLines; + + // This is Y axis, so draw it + if (index === 0 && !drawVerticalLine){ + drawVerticalLine = true; + } + + if (drawVerticalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + if (drawVerticalLine){ + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + } + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function(){ + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + }, + calculateCenterOffset: function(value){ + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update : function(){ + if (!this.lineArc){ + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + } + this.buildYLabels(); + }, + buildYLabels: function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + getCircumference : function(){ + return ((Math.PI*2) / this.valuesCount); + }, + setScaleSize: function(){ + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + for (i=0;i furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } + else if (i < this.valuesCount/2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } + else if (i > this.valuesCount/2){ + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement){ + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight)/2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height/2); + }, + + getIndexAngle : function(index){ + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI/2); + }, + getPointPosition : function(index, distanceFromCenter){ + var thisAngle = this.getIndexAngle(index); + return { + x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function(){ + if (this.display){ + var ctx = this.ctx; + each(this.yLabels, function(label, index){ + // Don't draw a centre value + if (index > 0){ + var yCenterOffset = index * (this.drawingArea/this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0){ + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if(this.lineArc){ + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); + ctx.closePath(); + ctx.stroke(); + } else{ + ctx.beginPath(); + for (var i=0;i= 0; i--) { + if (this.angleLineWidth > 0){ + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length/2, + quarterLabelsCount = halfLabelsCount/2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0){ + ctx.textAlign = 'center'; + } else if(i === halfLabelsCount){ + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount){ + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter){ + ctx.textBaseline = 'middle'; + } else if (upperHalf){ + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + // Attach global event to resize each chart instance when the browser resizes + helpers.addEvent(window, "resize", (function(){ + // Basic debounce of resize function so it doesn't hurt performance when resizing browser. + var timeout; + return function(){ + clearTimeout(timeout); + timeout = setTimeout(function(){ + each(Chart.instances,function(instance){ + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive){ + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function(){ + return Chart; + }); + } else if (typeof module === 'object' && module.exports) { + module.exports = Chart; + } + + root.Chart = Chart; + + Chart.noConflict = function(){ + root.Chart = previous; + return Chart; + }; + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + var defaultConfig = { + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing : 1, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Bar", + defaults : defaultConfig, + initialize: function(data){ + + //Expose options as a scope variable here so we can access it in the ScaleClass + var options = this.options; + + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines : true, + calculateBarX : function(datasetCount, datasetIndex, barIndex){ + //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.calculateX(barIndex) - (xWidth/2), + barWidth = this.calculateBarWidth(datasetCount); + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; + }, + calculateBaseWidth : function(){ + return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); + }, + calculateBarWidth : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); + + return (baseWidth / datasetCount); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; + + this.eachBars(function(bar){ + bar.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activeBars, function(activeBar){ + activeBar.fillColor = activeBar.highlightFill; + activeBar.strokeColor = activeBar.highlightStroke; + }); + this.showTooltip(activeBars); + }); + } + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.Rectangle.extend({ + strokeWidth : this.options.barStrokeWidth, + showStroke : this.options.barShowStroke, + ctx : this.chart.ctx + }); + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset,datasetIndex){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + bars : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); + },this); + + },this); + + this.buildScale(data.labels); + + this.BarClass.prototype.base = this.scale.endPoint; + + this.eachBars(function(bar, index, datasetIndex){ + helpers.extend(bar, { + width : this.scale.calculateBarWidth(this.datasets.length), + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y: this.scale.endPoint + }); + bar.save(); + }, this); + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + + this.eachBars(function(bar){ + bar.save(); + }); + this.render(); + }, + eachBars : function(callback){ + helpers.each(this.datasets,function(dataset, datasetIndex){ + helpers.each(dataset.bars, callback, this, datasetIndex); + },this); + }, + getBarsAtEvent : function(e){ + var barsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset){ + barsArray.push(dataset.bars[barIndex]); + }, + barIndex; + + for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { + for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { + if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ + helpers.each(this.datasets, datasetIterator); + return barsArray; + } + } + } + + return barsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachBars(function(bar){ + values.push(bar.value); + }); + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : value, + label : label, + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.bars.shift(); + },this); + this.update(); + }, + reflow : function(){ + helpers.extend(this.BarClass.prototype,{ + y: this.scale.endPoint, + base : this.scale.endPoint + }); + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + helpers.each(this.datasets,function(dataset,datasetIndex){ + helpers.each(dataset.bars,function(bar,index){ + if (bar.hasValue()){ + bar.base = this.scale.endPoint; + //Transition then draw + bar.transition({ + x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y : this.scale.calculateY(bar.value), + width : this.scale.calculateBarWidth(this.datasets.length) + }, easingDecimal).draw(); + } + },this); + + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout : 50, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + + //Declare segments as a static property to prevent inheriting across the Chart type prototype + this.segments = []; + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + + this.SegmentArc = Chart.Arc.extend({ + ctx : this.chart.ctx, + x : this.chart.width/2, + y : this.chart.height/2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + this.calculateTotal(data); + + helpers.each(data,function(datapoint, index){ + this.addData(datapoint, index, true); + },this); + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 1.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + calculateCircumference : function(value){ + return (Math.PI*2)*(Math.abs(value) / this.total); + }, + calculateTotal : function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += Math.abs(segment.value); + },this); + }, + update : function(){ + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor']); + }); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw : function(easeDecimal){ + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments,function(segment,index){ + segment.transition({ + circumference : this.calculateCircumference(segment.value), + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + },animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length-1){ + this.segments[index+1].startAngle = segment.endAngle; + } + },this); + + } + }); + + Chart.types.Doughnut.extend({ + name : "Pie", + defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - Whether the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Line", + defaults : defaultConfig, + initialize: function(data){ + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx, + inRange : function(mouseX){ + return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePoints, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + this.showTooltip(activePoints); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + this.buildScale(data.labels); + + + this.eachPoints(function(point, index){ + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint + }); + point.save(); + }, this); + + },this); + + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + this.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + getPointsAtEvent : function(e){ + var pointsArray = [], + eventPosition = helpers.getRelativePosition(e); + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,function(point){ + if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); + }); + },this); + return pointsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachPoints(function(point){ + values.push(point.value); + }); + + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange : function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + + this.scale = new Chart.Scale(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: this.scale.calculateX(this.scale.valuesCount+1), + y: this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.update(); + }, + reflow : function(){ + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + // Some helper methods for getting the next/prev points + var hasValue = function(item){ + return item.value !== null; + }, + nextPoint = function(point, collection, index){ + return helpers.findNextWhere(collection, hasValue, index) || point; + }, + previousPoint = function(point, collection, index){ + return helpers.findPreviousWhere(collection, hasValue, index) || point; + }; + + this.scale.draw(easingDecimal); + + + helpers.each(this.datasets,function(dataset){ + var pointsWithValues = helpers.where(dataset.points, hasValue); + + //Transition each point first so that the line and point drawing isn't out of sync + //We can use this extra loop to calculate the control points of this dataset also in this loop + + helpers.each(dataset.points, function(point, index){ + if (point.hasValue()){ + point.transition({ + y : this.scale.calculateY(point.value), + x : this.scale.calculateX(index) + }, easingDecimal); + } + },this); + + + // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point + // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed + if (this.options.bezierCurve){ + helpers.each(pointsWithValues, function(point, index){ + var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; + point.controlPoints = helpers.splineCurve( + previousPoint(point, pointsWithValues, index), + point, + nextPoint(point, pointsWithValues, index), + tension + ); + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (point.controlPoints.outer.y > this.scale.endPoint){ + point.controlPoints.outer.y = this.scale.endPoint; + } + else if (point.controlPoints.outer.y < this.scale.startPoint){ + point.controlPoints.outer.y = this.scale.startPoint; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (point.controlPoints.inner.y > this.scale.endPoint){ + point.controlPoints.inner.y = this.scale.endPoint; + } + else if (point.controlPoints.inner.y < this.scale.startPoint){ + point.controlPoints.inner.y = this.scale.startPoint; + } + },this); + } + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + + helpers.each(pointsWithValues, function(point, index){ + if (index === 0){ + ctx.moveTo(point.x, point.y); + } + else{ + if(this.options.bezierCurve){ + var previous = previousPoint(point, pointsWithValues, index); + + ctx.bezierCurveTo( + previous.controlPoints.outer.x, + previous.controlPoints.outer.y, + point.controlPoints.inner.x, + point.controlPoints.inner.y, + point.x, + point.y + ); + } + else{ + ctx.lineTo(point.x,point.y); + } + } + }, this); + + ctx.stroke(); + + if (this.options.datasetFill && pointsWithValues.length > 0){ + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); + ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); + ctx.fillStyle = dataset.fillColor; + ctx.closePath(); + ctx.fill(); + } + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(pointsWithValues,function(point){ + point.draw(); + }); + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.segments = []; + //Declare segment class as a chart instance specific class, so it can share props for this instance + this.SegmentArc = Chart.Arc.extend({ + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + ctx : this.chart.ctx, + innerRadius : 0, + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + valuesCount: data.length + }); + + this.updateScaleRange(data); + + this.scale.update(); + + helpers.each(data,function(segment,index){ + this.addData(segment,index,true); + },this); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + + this.segments.splice(index, 0, new this.SegmentArc({ + fillColor: segment.color, + highlightColor: segment.highlight || segment.color, + label: segment.label, + value: segment.value, + outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), + circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), + startAngle: Math.PI * 1.5 + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + calculateTotal: function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + this.scale.valuesCount = this.segments.length; + }, + updateScaleRange: function(datapoints){ + var valuesArray = []; + helpers.each(datapoints,function(segment){ + valuesArray.push(segment.value); + }); + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes, + { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + } + ); + + }, + update : function(){ + this.calculateTotal(this.segments); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + + this.reflow(); + this.render(); + }, + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.updateScaleRange(this.segments); + this.scale.update(); + + helpers.extend(this.scale,{ + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.scale.calculateCenterOffset(segment.value) + }); + }, this); + + }, + draw : function(ease){ + var easingDecimal = ease || 1; + //Clear & draw the canvas + this.clear(); + helpers.each(this.segments,function(segment, index){ + segment.transition({ + circumference : this.scale.getCircumference(), + outerRadius : this.scale.calculateCenterOffset(segment.value) + },easingDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + // If we've removed the first segment we need to set the first one to + // start at the top. + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1){ + this.segments[index+1].startAngle = segment.endAngle; + } + segment.draw(); + }, this); + this.scale.draw(); + } + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + + Chart.Type.extend({ + name: "Radar", + defaults:{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }, + + initialize: function(data){ + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx + }); + + this.datasets = []; + + this.buildScale(data); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePointsCollection, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + + this.showTooltip(activePointsCollection); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label: dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + var pointPosition; + if (!this.scale.animation){ + pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); + } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, + y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + },this); + + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + + getPointsAtEvent : function(evt){ + var mousePosition = helpers.getRelativePosition(evt), + fromCenter = helpers.getAngleFromPoint({ + x: this.scale.xCenter, + y: this.scale.yCenter + }, mousePosition); + + var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, + pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), + activePointsCollection = []; + + // If we're at the top, make the pointIndex 0 to get the first of the array. + if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ + pointIndex = 0; + } + + if (fromCenter.distance <= this.scale.drawingArea){ + helpers.each(this.datasets, function(dataset){ + activePointsCollection.push(dataset.points[pointIndex]); + }); + } + + return activePointsCollection; + }, + + buildScale : function(data){ + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + angleLineColor : this.options.angleLineColor, + angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, + // Point labels at the edge of each line + pointLabelFontColor : this.options.pointLabelFontColor, + pointLabelFontSize : this.options.pointLabelFontSize, + pointLabelFontFamily : this.options.pointLabelFontFamily, + pointLabelFontStyle : this.options.pointLabelFontStyle, + height : this.chart.height, + width: this.chart.width, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + labels: data.labels, + valuesCount: data.datasets[0].data.length + }); + + this.scale.setScaleSize(); + this.updateScaleRange(data.datasets); + this.scale.buildYLabels(); + }, + updateScaleRange: function(datasets){ + var valuesArray = (function(){ + var totalDataArray = []; + helpers.each(datasets,function(dataset){ + if (dataset.data){ + totalDataArray = totalDataArray.concat(dataset.data); + } + else { + helpers.each(dataset.points, function(point){ + totalDataArray.push(point.value); + }); + } + }); + return totalDataArray; + })(); + + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes + ); + + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + this.scale.valuesCount++; + helpers.each(valuesArray,function(value,datasetIndex){ + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: pointPosition.x, + y: pointPosition.y, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.labels.push(label); + + this.reflow(); + + this.update(); + }, + removeData : function(){ + this.scale.valuesCount--; + this.scale.labels.shift(); + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.reflow(); + this.update(); + }, + update : function(){ + this.eachPoints(function(point){ + point.save(); + }); + this.reflow(); + this.render(); + }, + reflow: function(){ + helpers.extend(this.scale, { + width : this.chart.width, + height: this.chart.height, + size : helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + this.updateScaleRange(this.datasets); + this.scale.setScaleSize(); + this.scale.buildYLabels(); + }, + draw : function(ease){ + var easeDecimal = ease || 1, + ctx = this.chart.ctx; + this.clear(); + this.scale.draw(); + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + helpers.each(dataset.points,function(point,index){ + if (point.hasValue()){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + } + },this); + + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index === 0){ + ctx.moveTo(point.x,point.y); + } + else{ + ctx.lineTo(point.x,point.y); + } + },this); + ctx.closePath(); + ctx.stroke(); + + ctx.fillStyle = dataset.fillColor; + ctx.fill(); + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + if (point.hasValue()){ + point.draw(); + } + }); + + },this); + + } + + }); + + + + + +}).call(this); \ No newline at end of file diff --git a/web/static/js/datatables.min.js b/web/static/js/datatables.min.js index e774564..01dfb0a 100755 --- a/web/static/js/datatables.min.js +++ b/web/static/js/datatables.min.js @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs4/jq-3.3.1/dt-1.10.18 + * https://datatables.net/download/#bs4/jq-3.3.1/dt-1.10.18/r-2.2.2 * * Included libraries: - * jQuery 3 3.3.1, DataTables 1.10.18 + * jQuery 3 3.3.1, DataTables 1.10.18, Responsive 2.2.2 */ /*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ @@ -164,7 +164,7 @@ Z(n.defaults.column);n.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button", iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal: this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};n.ext=x={buttons:{}, -classes:{},build:"bs4/jq-3.3.1/dt-1.10.18",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager}); +classes:{},build:"bs4/jq-3.3.1/dt-1.10.18/r-2.2.2",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager}); h.extend(n.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled", sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"", sJUIHeader:"",sJUIFooter:""});var Kb=n.ext.pager;h.extend(Kb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,n.ext.renderer,{pageButton:{_:function(a,b,c,d,e, @@ -192,3 +192,43 @@ renderer:"bootstrap"});b.extend(f.ext.classes,{sWrapper:"dataTables_wrapper dt-b {"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("",{href:"#","aria-controls":a.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:a.iTabIndex,"class":"page-link"}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(v){}q(b(h).empty().html('