From cb5d00984d787b6bc5a986821e93074c367acd0d Mon Sep 17 00:00:00 2001 From: buck54321 Date: Fri, 7 Dec 2018 09:34:28 -0600 Subject: [PATCH] ticketpool chart data inline -> AJAX This removes inline data from the ticketpool page, using AJAX instead. Initial chart data is also retrieved instead via AJAX, not just upon changing bar width. The websocket call that retrieves the same data remains in the code, but is not used here. --- api/apirouter.go | 1 + api/apiroutes.go | 46 ++- api/types/apitypes.go | 17 ++ db/dcrpg/pgblockchain.go | 70 +++-- db/dcrsqlite/apisource.go | 6 + explorer/explorer.go | 2 +- explorer/explorerroutes.go | 33 +-- explorer/websockethandlers.go | 30 +- mempool/mempool.go | 8 + mempool/mempoolcache.go | 25 ++ .../js/controllers/ticketpool_controller.js | 276 +++++++++++------- views/ticketpool.tmpl | 65 ++--- 12 files changed, 341 insertions(+), 238 deletions(-) diff --git a/api/apirouter.go b/api/apirouter.go index 805eada39..7397d5cfe 100644 --- a/api/apirouter.go +++ b/api/apirouter.go @@ -198,6 +198,7 @@ func NewAPIRouter(app *appContext, useRealIP bool) apiMux { mux.Route("/ticketpool", func(r chi.Router) { r.Get("/", app.getTicketPoolByDate) r.With(m.TicketPoolCtx).Get("/bydate/{tp}", app.getTicketPoolByDate) + r.Get("/charts", app.getTicketPoolCharts) }) mux.NotFound(func(w http.ResponseWriter, r *http.Request) { diff --git a/api/apiroutes.go b/api/apiroutes.go index 061f33d6b..cfe447f1c 100644 --- a/api/apiroutes.go +++ b/api/apiroutes.go @@ -81,6 +81,7 @@ type DataSourceLite interface { GetAddressTransactionsRawWithSkip(addr string, count, skip int) []*apitypes.AddressTxRaw SendRawTransaction(txhex string) (string, error) GetExplorerAddress(address string, count, offset int64) (*explorer.AddressInfo, txhelpers.AddressType, txhelpers.AddressError) + GetMempoolPriceCountTime() *apitypes.PriceCountTime } // DataSourceAux specifies an interface for advanced data collection using the @@ -97,7 +98,7 @@ type DataSourceAux interface { TxHistoryData(address string, addrChart dbtypes.HistoryChart, chartGroupings dbtypes.TimeBasedGrouping) (*dbtypes.ChartsData, error) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) ( - []*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error) + *dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error) AgendaVotes(agendaID string, chartType int) (*dbtypes.AgendaVoteChoices, error) } @@ -841,6 +842,41 @@ func (c *appContext) getSSTxDetails(w http.ResponseWriter, r *http.Request) { writeJSON(w, sstxDetails, c.getIndentQuery(r)) } +// getTicketPoolCharts pulls the initial data to populate the /ticketpool page +// charts. +func (c *appContext) getTicketPoolCharts(w http.ResponseWriter, r *http.Request) { + if c.LiteMode { + // not available in lite mode + http.Error(w, "not available in lite mode", 422) + return + } + + timeChart, priceChart, donutChart, height, err := c.AuxDataSource.TicketPoolVisualization(dbtypes.AllGrouping) + if dbtypes.IsTimeoutErr(err) { + apiLog.Errorf("TicketPoolVisualization: %v", err) + http.Error(w, "Database timeout.", http.StatusServiceUnavailable) + return + } + if err != nil { + apiLog.Errorf("Unable to get ticket pool charts: %v", err) + http.Error(w, http.StatusText(http.StatusUnprocessableEntity), http.StatusUnprocessableEntity) + return + } + + mp := c.BlockData.GetMempoolPriceCountTime() + + response := &apitypes.TicketPoolChartsData{ + ChartHeight: height, + TimeChart: timeChart, + PriceChart: priceChart, + DonutChart: donutChart, + Mempool: mp, + } + + writeJSON(w, response, c.getIndentQuery(r)) + +} + func (c *appContext) getTicketPoolByDate(w http.ResponseWriter, r *http.Request) { if c.LiteMode { // not available in lite mode @@ -858,7 +894,7 @@ func (c *appContext) getTicketPoolByDate(w http.ResponseWriter, r *http.Request) // TicketPoolVisualization here even though it returns a lot of data not // needed by this request. interval := dbtypes.TimeGroupingFromStr(tp) - barCharts, _, height, err := c.AuxDataSource.TicketPoolVisualization(interval) + timeChart, _, _, height, err := c.AuxDataSource.TicketPoolVisualization(interval) if dbtypes.IsTimeoutErr(err) { apiLog.Errorf("TicketPoolVisualization: %v", err) http.Error(w, "Database timeout.", http.StatusServiceUnavailable) @@ -871,11 +907,11 @@ func (c *appContext) getTicketPoolByDate(w http.ResponseWriter, r *http.Request) } tpResponse := struct { - Height uint64 `json:"height"` - PoolByDate *dbtypes.PoolTicketsData `json:"ticket_pool_data"` + Height uint64 `json:"height"` + TimeChart *dbtypes.PoolTicketsData `json:"time_chart"` }{ height, - barCharts[0], // purchase time distribution + timeChart, // purchase time distribution } writeJSON(w, tpResponse, c.getIndentQuery(r)) diff --git a/api/types/apitypes.go b/api/types/apitypes.go index eac807f22..13002fe75 100644 --- a/api/types/apitypes.go +++ b/api/types/apitypes.go @@ -435,3 +435,20 @@ type MempoolTicketDetails struct { // TicketsDetails is an array of pointers of TicketDetails used in // MempoolTicketDetails type TicketsDetails []*TicketDetails + +// TicketPoolChartsData is for data used to display ticket pool statistics at +// /ticketpool. +type TicketPoolChartsData struct { + ChartHeight uint64 `json:"height"` + TimeChart *dbtypes.PoolTicketsData `json:"time_chart"` + PriceChart *dbtypes.PoolTicketsData `json:"price_chart"` + DonutChart *dbtypes.PoolTicketsData `json:"donut_chart"` + Mempool *PriceCountTime `json:"mempool"` +} + +// PriceCountTime is a basic set of information about ticket in the mempool. +type PriceCountTime struct { + Price float64 `json:"price"` + Count int `json:"count"` + Time dbtypes.TimeDef `json:"time"` +} diff --git a/db/dcrpg/pgblockchain.go b/db/dcrpg/pgblockchain.go index e16c2b3b3..b0f96d1af 100644 --- a/db/dcrpg/pgblockchain.go +++ b/db/dcrpg/pgblockchain.go @@ -74,10 +74,9 @@ func (d *DevFundBalance) Balance() *explorer.AddressBalance { // fetching the same information. type ticketPoolDataCache struct { sync.RWMutex - Height map[dbtypes.TimeBasedGrouping]uint64 - // BarGraphsCache persists data for the Ticket purchase distribution chart - // and Ticket Price Distribution chart - BarGraphsCache map[dbtypes.TimeBasedGrouping][]*dbtypes.PoolTicketsData + Height map[dbtypes.TimeBasedGrouping]uint64 + TimeGraphCache map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData + PriceGraphCache map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData // DonutGraphCache persist data for the Number of tickets outputs pie chart. DonutGraphCache map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData } @@ -85,21 +84,23 @@ type ticketPoolDataCache struct { // ticketPoolGraphsCache persists the latest ticketpool data queried from the db. var ticketPoolGraphsCache = &ticketPoolDataCache{ Height: make(map[dbtypes.TimeBasedGrouping]uint64), - BarGraphsCache: make(map[dbtypes.TimeBasedGrouping][]*dbtypes.PoolTicketsData), + TimeGraphCache: make(map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData), + PriceGraphCache: make(map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData), DonutGraphCache: make(map[dbtypes.TimeBasedGrouping]*dbtypes.PoolTicketsData), } // TicketPoolData is a thread-safe way to access the ticketpool graphs data // stored in the cache. -func TicketPoolData(interval dbtypes.TimeBasedGrouping, height uint64) (barGraphs []*dbtypes.PoolTicketsData, - donutChart *dbtypes.PoolTicketsData, actualHeight uint64, intervalFound, isStale bool) { +func TicketPoolData(interval dbtypes.TimeBasedGrouping, height uint64) (timeGraph *dbtypes.PoolTicketsData, + priceGraph *dbtypes.PoolTicketsData, donutChart *dbtypes.PoolTicketsData, actualHeight uint64, intervalFound, isStale bool) { ticketPoolGraphsCache.RLock() defer ticketPoolGraphsCache.RUnlock() - var found bool - barGraphs, intervalFound = ticketPoolGraphsCache.BarGraphsCache[interval] - donutChart, found = ticketPoolGraphsCache.DonutGraphCache[interval] - intervalFound = intervalFound && found + var tFound, pFound, dFound bool + timeGraph, tFound = ticketPoolGraphsCache.TimeGraphCache[interval] + priceGraph, pFound = ticketPoolGraphsCache.PriceGraphCache[interval] + donutChart, dFound = ticketPoolGraphsCache.DonutGraphCache[interval] + intervalFound = tFound && pFound && dFound actualHeight = ticketPoolGraphsCache.Height[interval] isStale = ticketPoolGraphsCache.Height[interval] != height @@ -110,13 +111,14 @@ func TicketPoolData(interval dbtypes.TimeBasedGrouping, height uint64) (barGraph // UpdateTicketPoolData updates the ticket pool cache with the latest data fetched. // This is a thread-safe way to update ticket pool cache data. TryLock helps avoid // stacking calls to update the cache. -func UpdateTicketPoolData(interval dbtypes.TimeBasedGrouping, barGraphs []*dbtypes.PoolTicketsData, - donutcharts *dbtypes.PoolTicketsData, height uint64) { +func UpdateTicketPoolData(interval dbtypes.TimeBasedGrouping, timeGraph *dbtypes.PoolTicketsData, + priceGraph *dbtypes.PoolTicketsData, donutcharts *dbtypes.PoolTicketsData, height uint64) { ticketPoolGraphsCache.Lock() defer ticketPoolGraphsCache.Unlock() ticketPoolGraphsCache.Height[interval] = height - ticketPoolGraphsCache.BarGraphsCache[interval] = barGraphs + ticketPoolGraphsCache.TimeGraphCache[interval] = timeGraph + ticketPoolGraphsCache.PriceGraphCache[interval] = priceGraph ticketPoolGraphsCache.DonutGraphCache[interval] = donutcharts } @@ -987,14 +989,14 @@ func (pgb *ChainDB) TimeBasedIntervals(timeGrouping dbtypes.TimeBasedGrouping, // a query and updates the cache. If there is no cached data for the interval, // this will launch a new query for the data if one is not already running, and // if one is running, it will wait for the query to complete. -func (pgb *ChainDB) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) ([]*dbtypes.PoolTicketsData, - *dbtypes.PoolTicketsData, uint64, error) { +func (pgb *ChainDB) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) (*dbtypes.PoolTicketsData, + *dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error) { // Attempt to retrieve data for the current block from cache. heightSeen := pgb.Height() // current block seen *by the ChainDB* - barcharts, donutCharts, height, intervalFound, stale := TicketPoolData(interval, heightSeen) + timeChart, priceChart, donutCharts, height, intervalFound, stale := TicketPoolData(interval, heightSeen) if intervalFound && !stale { // The cache was fresh. - return barcharts, donutCharts, height, nil + return timeChart, priceChart, donutCharts, height, nil } // Cache is stale or empty. Attempt to gain updater status. @@ -1009,12 +1011,12 @@ func (pgb *ChainDB) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) defer pgb.tpUpdatePermission[interval].Unlock() // Try again to pull it from cache now that the update is completed. heightSeen = pgb.Height() - barcharts, donutCharts, height, intervalFound, stale = TicketPoolData(interval, heightSeen) + timeChart, priceChart, donutCharts, height, intervalFound, stale = TicketPoolData(interval, heightSeen) // We waited for the updater of this interval, so it should be found // at this point. If not, this is an error. if !intervalFound { log.Errorf("Charts data for interval %v failed to update.", interval) - return nil, nil, 0, fmt.Errorf("no charts data available") + return nil, nil, nil, 0, fmt.Errorf("no charts data available") } if stale { log.Warnf("Charts data for interval %v updated, but still stale.", interval) @@ -1022,23 +1024,23 @@ func (pgb *ChainDB) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) } // else return the stale data instead of waiting. - return barcharts, donutCharts, height, nil + return timeChart, priceChart, donutCharts, height, nil } // This goroutine is now the cache updater. defer pgb.tpUpdatePermission[interval].Unlock() // Retrieve chart data for best block in DB. var err error - barcharts, donutCharts, height, err = pgb.ticketPoolVisualization(interval) + timeChart, priceChart, donutCharts, height, err = pgb.ticketPoolVisualization(interval) if err != nil { log.Errorf("Failed to fetch ticket pool data: %v", err) - return nil, nil, 0, err + return nil, nil, nil, 0, err } // Update the cache with the new ticket pool data. - UpdateTicketPoolData(interval, barcharts, donutCharts, height) + UpdateTicketPoolData(interval, timeChart, priceChart, donutCharts, height) - return barcharts, donutCharts, height, nil + return timeChart, priceChart, donutCharts, height, nil } // ticketPoolVisualization fetches the following ticketpool data: tickets @@ -1046,35 +1048,31 @@ func (pgb *ChainDB) TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) // counts by ticket type (solo, pool, other split). The interval may be one of: // "mo", "wk", "day", or "all". The data is needed to populate the ticketpool // graphs. The data grouped by time and price are returned in a slice. -func (pgb *ChainDB) ticketPoolVisualization(interval dbtypes.TimeBasedGrouping) (byTimeAndPrice []*dbtypes.PoolTicketsData, - byInputs *dbtypes.PoolTicketsData, height uint64, err error) { +func (pgb *ChainDB) ticketPoolVisualization(interval dbtypes.TimeBasedGrouping) (timeChart *dbtypes.PoolTicketsData, + priceChart *dbtypes.PoolTicketsData, byInputs *dbtypes.PoolTicketsData, height uint64, err error) { // Ensure DB height is the same before and after queries since they are not // atomic. Initial height: height = pgb.Height() - for { // Latest block where mature tickets may have been mined. maturityBlock := pgb.TicketPoolBlockMaturity() // Tickets grouped by time interval - ticketsByTime, err := pgb.TicketPoolByDateAndInterval(maturityBlock, interval) + timeChart, err = pgb.TicketPoolByDateAndInterval(maturityBlock, interval) if err != nil { - return nil, nil, 0, err + return nil, nil, nil, 0, err } // Tickets grouped by price - ticketsByPrice, err := pgb.TicketsByPrice(maturityBlock) + priceChart, err = pgb.TicketsByPrice(maturityBlock) if err != nil { - return nil, nil, 0, err + return nil, nil, nil, 0, err } - // Return time- and price-grouped data in a slice - byTimeAndPrice = []*dbtypes.PoolTicketsData{ticketsByTime, ticketsByPrice} - // Tickets grouped by number of inputs. byInputs, err = pgb.TicketsByInputCount() if err != nil { - return nil, nil, 0, err + return nil, nil, nil, 0, err } heightEnd := pgb.Height() diff --git a/db/dcrsqlite/apisource.go b/db/dcrsqlite/apisource.go index 39850f7a8..99691a444 100644 --- a/db/dcrsqlite/apisource.go +++ b/db/dcrsqlite/apisource.go @@ -874,6 +874,12 @@ func (db *wiredDB) GetMempoolSSTxDetails(N int) *apitypes.MempoolTicketDetails { return &mpTicketDetails } +// GetMempoolPriceCountTime retreives from mempool: the ticket price, the number +// of tickets in mempool, the time of the first ticket. +func (db *wiredDB) GetMempoolPriceCountTime() *apitypes.PriceCountTime { + return db.MPC.GetTicketPriceCountTime(int(db.params.MaxFreshStakePerBlock)) +} + // GetAddressTransactionsWithSkip returns an apitypes.Address Object with at most the // last count transactions the address was in func (db *wiredDB) GetAddressTransactionsWithSkip(addr string, count, skip int) *apitypes.Address { diff --git a/explorer/explorer.go b/explorer/explorer.go index d813278fd..e6ff8db98 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -83,7 +83,7 @@ type explorerDataSource interface { BlockStatus(hash string) (dbtypes.BlockStatus, error) BlockFlags(hash string) (bool, bool, error) AddressMetrics(addr string) (*dbtypes.AddressMetrics, error) - TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) ([]*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error) + TicketPoolVisualization(interval dbtypes.TimeBasedGrouping) (*dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, *dbtypes.PoolTicketsData, uint64, error) TransactionBlocks(hash string) ([]*dbtypes.BlockStatus, []uint32, error) Transaction(txHash string) ([]*dbtypes.Tx, error) VinsForTx(*dbtypes.Tx) (vins []dbtypes.VinTxProperty, prevPkScripts []string, scriptVersions []uint16, err error) diff --git a/explorer/explorerroutes.go b/explorer/explorerroutes.go index 8051e2259..002d68a3b 100644 --- a/explorer/explorerroutes.go +++ b/explorer/explorerroutes.go @@ -637,44 +637,13 @@ func (exp *explorerUI) Ticketpool(w http.ResponseWriter, r *http.Request) { "Ticketpool page cannot run in lite mode", "", ExpStatusNotSupported) return } - interval := dbtypes.AllGrouping - - barGraphs, donutChart, height, err := exp.explorerSource.TicketPoolVisualization(interval) - if exp.timeoutErrorPage(w, err, "TicketPoolVisualization") { - return - } - if err != nil { - log.Errorf("Template execute failure: %v", err) - exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, "", ExpStatusError) - return - } - - var mp dbtypes.PoolTicketsData - exp.MempoolData.RLock() - if len(exp.MempoolData.Tickets) > 0 { - t := time.Unix(exp.MempoolData.Tickets[0].Time, 0) - mp.Time = append(mp.Time, dbtypes.TimeDef{T: t}) - mp.Price = append(mp.Price, exp.MempoolData.Tickets[0].TotalOut) - mp.Mempool = append(mp.Mempool, uint64(len(exp.MempoolData.Tickets))) - } else { - log.Debug("No tickets exist in the mempool") - } - exp.MempoolData.RUnlock() str, err := exp.templates.execTemplateToString("ticketpool", struct { *CommonPageData - NetName string - ChartsHeight uint64 - ChartData []*dbtypes.PoolTicketsData - GroupedData *dbtypes.PoolTicketsData - Mempool *dbtypes.PoolTicketsData + NetName string }{ CommonPageData: exp.commonData(), NetName: exp.NetName, - ChartsHeight: height, - ChartData: barGraphs, - GroupedData: donutChart, - Mempool: &mp, }) if err != nil { diff --git a/explorer/websockethandlers.go b/explorer/websockethandlers.go index 54880e5fb..6c88c98f0 100644 --- a/explorer/websockethandlers.go +++ b/explorer/websockethandlers.go @@ -13,6 +13,7 @@ import ( "strings" "time" + apitypes "github.com/decred/dcrdata/v3/api/types" "github.com/decred/dcrdata/v3/db/dbtypes" "golang.org/x/net/websocket" ) @@ -120,7 +121,7 @@ func (exp *explorerUI) RootWebsocket(w http.ResponseWriter, r *http.Request) { // Chart height is returned since the cache may be stale, // although it is automatically updated by the first caller // who requests data from a stale cache. - cData, gData, chartHeight, err := exp.explorerSource.TicketPoolVisualization(interval) + timeChart, priceChart, donutChart, chartHeight, err := exp.explorerSource.TicketPoolVisualization(interval) if dbtypes.IsTimeoutErr(err) { log.Warnf("TicketPoolVisualization DB timeout: %v", err) webData.Message = "Error: DB timeout" @@ -138,28 +139,25 @@ func (exp *explorerUI) RootWebsocket(w http.ResponseWriter, r *http.Request) { break } - var mp dbtypes.PoolTicketsData + mp := new(apitypes.PriceCountTime) exp.MempoolData.RLock() + if len(exp.MempoolData.Tickets) > 0 { - t := time.Unix(exp.MempoolData.Tickets[0].Time, 0) - mp.Time = append(mp.Time, dbtypes.TimeDef{T: t}) - mp.Price = append(mp.Price, exp.MempoolData.Tickets[0].TotalOut) - mp.Mempool = append(mp.Mempool, uint64(len(exp.MempoolData.Tickets))) + mp.Price = exp.MempoolData.Tickets[0].TotalOut + mp.Count = len(exp.MempoolData.Tickets) + mp.Time = dbtypes.TimeDef{T: time.Unix(exp.MempoolData.Tickets[0].Time, 0)} } else { log.Debug("No tickets exists in the mempool") } + exp.MempoolData.RUnlock() - var data = struct { - ChartHeight uint64 `json:"chartHeight"` - BarGraphs []*dbtypes.PoolTicketsData `json:"barGraphs"` - DonutChart *dbtypes.PoolTicketsData `json:"donutChart"` - Mempool *dbtypes.PoolTicketsData `json:"mempool"` - }{ - chartHeight, - cData, - gData, - &mp, + data := &apitypes.TicketPoolChartsData{ + ChartHeight: chartHeight, + TimeChart: timeChart, + PriceChart: priceChart, + DonutChart: donutChart, + Mempool: mp, } msg, err := json.Marshal(data) diff --git a/mempool/mempool.go b/mempool/mempool.go index b35499605..6130ffb26 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -289,6 +289,7 @@ type MempoolData struct { Ticketfees *dcrjson.TicketFeeInfoResult MinableFees *MinableFeeInfo AllTicketsDetails TicketsDetails + StakeDiff float64 } // GetHeight returns the mempool height @@ -344,6 +345,12 @@ func (t *mempoolDataCollector) Collect() (*MempoolData, error) { } numVotes := len(mempoolVotes) + // Grab the current stake difficulty (ticket price). + stakeDiff, err := c.GetStakeDifficulty() + if err != nil { + return nil, err + } + // Fee info var numFeeWindows, numFeeBlocks uint32 = 0, 0 feeInfo, err := c.TicketFeeInfo(&numFeeBlocks, &numFeeWindows) @@ -432,6 +439,7 @@ func (t *mempoolDataCollector) Collect() (*MempoolData, error) { MinableFees: mineables, NumVotes: uint32(numVotes), AllTicketsDetails: allTicketsDetails, + StakeDiff: stakeDiff.CurrentStakeDifficulty, } return mpoolData, err diff --git a/mempool/mempoolcache.go b/mempool/mempoolcache.go index 30aed43c5..3d9d4d9bf 100644 --- a/mempool/mempoolcache.go +++ b/mempool/mempoolcache.go @@ -9,6 +9,7 @@ import ( "github.com/decred/dcrd/dcrjson" apitypes "github.com/decred/dcrdata/v3/api/types" + "github.com/decred/dcrdata/v3/db/dbtypes" ) // MempoolDataCache models the basic data for the mempool cache @@ -22,6 +23,7 @@ type MempoolDataCache struct { allFeeRates []float64 lowestMineableByFeeRate float64 allTicketsDetails TicketsDetails + stakeDiff float64 } // StoreMPData stores info from data in the mempool cache @@ -37,6 +39,7 @@ func (c *MempoolDataCache) StoreMPData(data *MempoolData, timestamp time.Time) e c.allFeeRates = data.MinableFees.allFeeRates c.lowestMineableByFeeRate = data.MinableFees.lowestMineableFee c.allTicketsDetails = data.AllTicketsDetails + c.stakeDiff = data.StakeDiff return nil } @@ -151,3 +154,25 @@ func (c *MempoolDataCache) GetTicketsDetails(N int) (uint32, int64, int, Tickets return c.height, c.timestamp.Unix(), numSSTx, details } + +// GetTicketPriceCountTime gathers the nominal info for mempool tickets. +func (c *MempoolDataCache) GetTicketPriceCountTime(feeAvgLength int) *apitypes.PriceCountTime { + c.RLock() + defer c.RUnlock() + + numFees := len(c.allFees) + if numFees < feeAvgLength { + feeAvgLength = numFees + } + var feeAvg float64 + for i := 0; i < feeAvgLength; i++ { + feeAvg += c.allFees[numFees-i-1] + } + feeAvg = feeAvg / float64(feeAvgLength) + + return &apitypes.PriceCountTime{ + Price: c.stakeDiff + feeAvg, + Count: numFees, + Time: dbtypes.TimeDef{T: c.timestamp}, + } +} diff --git a/public/js/controllers/ticketpool_controller.js b/public/js/controllers/ticketpool_controller.js index 31f747bcc..53ee729a9 100644 --- a/public/js/controllers/ticketpool_controller.js +++ b/public/js/controllers/ticketpool_controller.js @@ -10,7 +10,7 @@ import { barChartPlotter } from '../helpers/chart_helper' function legendFormatter (data) { if (data.x == null) return '' var html = this.getLabels()[0] + ': ' + data.xHTML - data.series.map(function (series) { + data.series.map((series) => { var labeledData = ' ' + series.labelHTML + ': ' + series.yHTML html += '
' + series.dashHTML + labeledData + '
' }) @@ -31,14 +31,13 @@ function purchasesGraphData (items, memP) { var s = [] var finalDate = '' - items.time.map(function (n, i) { + items.time.map((n, i) => { finalDate = new Date(n) s.push([finalDate, 0, items.immature[i], items.live[i], items.price[i]]) }) - if (!isNaN(memP.time)) { - let memPdate = new Date(memP.time[0]) - s.push([new Date().setDate(memPdate.getMinutes() + 1), memP.mempool[0], 0, 0, memP.price[0]]) // add mempool + if (memP) { + s.push([new Date(memP.time), memP.count, 0, 0, memP.price]) // add mempool } origDate = s[0][0] - new Date(0) @@ -53,9 +52,9 @@ function priceGraphData (items, memP) { var mCount = 0 var p = [] - if (!isNaN(memP.price)) { - mPrice = memP.price[0] - mCount = memP.mempool[0] + if (memP) { + mPrice = memP.price + mCount = memP.count } items.price.map((n, i) => { @@ -107,120 +106,99 @@ var commonOptions = { legend: 'follow' } -function purchasesGraph () { - var d = purchasesGraphData(window.graph[0], window.mpl) - var p = { - - labels: ['Date', 'Mempool Tickets', 'Immature Tickets', 'Live Tickets', 'Ticket Value'], - colors: ['#FF8C00', '#006600', '#2971FF', '#ff0090'], - title: 'Tickets Purchase Distribution', - y2label: 'A.v.g. Tickets Value (DCR)', - dateWindow: getWindow('day'), - series: { - 'Ticket Value': { - axis: 'y2', - plotter: Dygraph.Plotters.linePlotter - } - }, - axes: { y2: { axisLabelFormatter: function (d) { return d.toFixed(1) } } } - } - return new Dygraph( - document.getElementById('tickets_by_purchase_date'), - d, { ...commonOptions, ...p } - ) -} - -function priceGraph () { - var d = priceGraphData(window.graph[1], window.mpl) - var p = { - labels: ['Price', 'Mempool Tickets', 'Immature Tickets', 'Live Tickets'], - colors: ['#FF8C00', '#006600', '#2971FF'], - title: 'Ticket Price Distribution', - labelsKMB: true, - xlabel: 'Ticket Price (DCR)' - } - return new Dygraph( - document.getElementById('tickets_by_purchase_price'), - d, { ...commonOptions, ...p } - ) -} - -function outputsGraph () { - var d = outputsGraphData(window.chart) - return new Chart( - document.getElementById('doughnutGraph'), { - options: { - width: 200, - height: 200, - responsive: false, - animation: { animateScale: true }, - legend: { position: 'bottom' }, - title: { - display: true, - text: 'Number of Ticket Outputs' - }, - tooltips: { - callbacks: { - label: function (tooltipItem, data) { - var sum = 0 - var currentValue = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index] - d.map((u) => { sum += u }) - return currentValue + ' Tickets ( ' + ((currentValue / sum) * 100).toFixed(2) + '% )' - } - } - } - }, - type: 'doughnut', - data: { - labels: ['Solo', 'VSP Tickets', 'TixSplit'], - datasets: [{ - data: d, - label: 'Solo Tickets', - backgroundColor: ['#2971FF', '#FF8C00', '#41BF53'], - borderColor: ['white', 'white', 'white'], - borderWidth: 0.5 - }] - } - }) -} - export default class extends Controller { static get targets () { - return [ 'zoom', 'bars', 'age' ] + return [ 'zoom', 'bars', 'age', 'wrapper' ] } initialize () { - this.zoom = 'day' - this.bars = 'all' + var controller = this + controller.mempool = false + controller.tipHeight = 0 + controller.purchasesGraph = null + controller.priceGraph = null + controller.outputsGraph = null + controller.graphData = { + 'time_chart': null, + 'price_chart': null, + 'donut_chart': null + } + controller.zoom = 'day' + controller.bars = 'all' $.getScript('/js/vendor/dygraphs.min.js', () => { - this.purchasesGraph = purchasesGraph() - this.priceGraph = priceGraph() + controller.chartCount += 2 + controller.purchasesGraph = controller.makePurchasesGraph() + controller.priceGraph = controller.makePriceGraph() }) $.getScript('/js/vendor/charts.min.js', () => { - this.outputsGraph = outputsGraph() + controller.chartCount += 1 + controller.outputsGraph = controller.makeOutputsGraph() }) } connect () { + var controller = this ws.registerEvtHandler('newblock', () => { - ws.send('getticketpooldata', this.bars) + ws.send('getticketpooldata', controller.bars) }) ws.registerEvtHandler('getticketpooldataResp', (evt) => { if (evt === '') { return } - var v = JSON.parse(evt) - window.mpl = v.mempool - this.purchasesGraph.updateOptions({ 'file': purchasesGraphData(v.barGraphs[0], window.mpl), - dateWindow: getWindow(this.zoom) }) - this.priceGraph.updateOptions({ 'file': priceGraphData(v.barGraphs[1], window.mpl) }) - - this.outputsGraph.data.datasets[0].data = outputsGraphData(v.donutChart) - this.outputsGraph.update() + var data = JSON.parse(evt) + controller.processData(data) + }) + + controller.fetchAll() + } + + fetchAll () { + var controller = this + controller.wrapperTarget.classList.add('loading') + $.ajax({ + type: 'GET', + url: '/api/ticketpool/charts', + success: (data) => { + controller.processData(data) + }, + complete: () => { + controller.wrapperTarget.classList.remove('loading') + } }) } + processData (data) { + var controller = this + if (data['mempool']) { + // If mempool data is included, assume the data height is the tip. + controller.mempool = data['mempool'] + controller.tipHeight = data['height'] + } + if (data['time_chart']) { + // Only append the mempool data if this data goes to the tip. + let mempool = controller.tipHeight === data['height'] ? controller.mempool : false + controller.graphData['time_chart'] = purchasesGraphData(data['time_chart'], mempool) + if (controller.purchasesGraph !== null) { + controller.purchasesGraph.updateOptions({ 'file': controller.graphData['time_chart'] }) + controller.purchasesGraph.resetZoom() + } + } + if (data['price_chart']) { + controller.graphData['price_chart'] = priceGraphData(data['price_chart'], controller.mempool) + if (controller.pricesGraph !== null) { + controller.priceGraph.updateOptions({ 'file': controller.graphData['price_chart'] }) + } + } + if (data['donut_chart']) { + controller.graphData['donut_chart'] = outputsGraphData(data['donut_chart']) + if (controller.outputsGraph !== null) { + controller.outputsGraph.data.datasets[0].data = controller.graphData['donut_chart'] + controller.outputsGraph.update() + } + } + } + disconnect () { this.purchasesGraph.destroy() this.priceGraph.destroy() @@ -239,25 +217,97 @@ export default class extends Controller { } onBarsChange (e) { - $(this.barsTargets).each((i, barsTarget) => { + var controller = this + $(controller.barsTargets).each((i, barsTarget) => { $(barsTarget).removeClass('btn-active') }) - this.bars = e.target.name + controller.bars = e.target.name $(e.target).addClass('btn-active') - $('body').addClass('loading') - var _this = this - + controller.wrapperTarget.classList.add('loading') $.ajax({ type: 'GET', - url: '/api/ticketpool/bydate/' + this.bars, - beforeSend: function () {}, - error: function () { - $('body').removeClass('loading') - }, - success: function (data) { - _this.purchasesGraph.updateOptions({ 'file': purchasesGraphData(data.ticket_pool_data, window.mpl) }) - $('body').removeClass('loading') + url: '/api/ticketpool/bydate/' + controller.bars, + beforeSend: () => {}, + complete: () => { controller.wrapperTarget.classList.remove('loading') }, + success: (data) => { + controller.purchasesGraph.updateOptions({ 'file': purchasesGraphData(data['time_chart']) }) } }) } + + makePurchasesGraph () { + var d = this.graphData['price_chart'] || [[0, 0, 0, 0, 0]] + var p = { + labels: ['Date', 'Mempool Tickets', 'Immature Tickets', 'Live Tickets', 'Ticket Value'], + colors: ['#FF8C00', '#006600', '#2971FF', '#ff0090'], + title: 'Tickets Purchase Distribution', + y2label: 'A.v.g. Tickets Value (DCR)', + dateWindow: getWindow('day'), + series: { + 'Ticket Value': { + axis: 'y2', + plotter: Dygraph.Plotters.linePlotter + } + }, + axes: { y2: { axisLabelFormatter: (d) => { return d.toFixed(1) } } } + } + return new Dygraph( + document.getElementById('tickets_by_purchase_date'), + d, { ...commonOptions, ...p } + ) + } + + makePriceGraph () { + var d = this.graphData['price_chart'] || [[0, 0, 0, 0]] + var p = { + labels: ['Price', 'Mempool Tickets', 'Immature Tickets', 'Live Tickets'], + colors: ['#FF8C00', '#006600', '#2971FF'], + title: 'Ticket Price Distribution', + labelsKMB: true, + xlabel: 'Ticket Price (DCR)' + } + return new Dygraph( + document.getElementById('tickets_by_purchase_price'), + d, { ...commonOptions, ...p } + ) + } + + makeOutputsGraph () { + var d = this.graphData['donut_chart'] || [] + return new Chart( + document.getElementById('doughnutGraph'), { + options: { + width: 200, + height: 200, + responsive: false, + animation: { animateScale: true }, + legend: { position: 'bottom' }, + title: { + display: true, + text: 'Number of Ticket Outputs' + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + var sum = 0 + var currentValue = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index] + d.map((u) => { sum += u }) + return currentValue + ' Tickets ( ' + ((currentValue / sum) * 100).toFixed(2) + '% )' + } + } + } + }, + type: 'doughnut', + data: { + labels: ['Solo', 'VSP Tickets', 'TixSplit'], + datasets: [{ + data: d, + label: 'Solo Tickets', + backgroundColor: ['#2971FF', '#FF8C00', '#41BF53'], + borderColor: ['white', 'white', 'white'], + borderWidth: 0.5 + }] + } + }) + } } diff --git a/views/ticketpool.tmpl b/views/ticketpool.tmpl index 5455f6671..169bd572d 100644 --- a/views/ticketpool.tmpl +++ b/views/ticketpool.tmpl @@ -15,48 +15,43 @@
- -
- - - - -
+
+ + +
+ + + + +
- -
- - - - -
+ +
+ + + + +
-
+
-
-
+
+
-
-
- - - -
- 3 Outputs: - typically solo.

- 5 Outputs: - typically Voting Service Provider (VSP).

- More than 5 Outputs: - likely multi-party (split) tickets.
+
+
+ + + +
+ 3 Outputs: - typically solo.

+ 5 Outputs: - typically Voting Service Provider (VSP).

+ More than 5 Outputs: - likely multi-party (split) tickets.
+
- - - - {{ template "footer" . }}