diff --git a/src/Template/Main/blocks.ctp b/src/Template/Main/blocks.ctp
index b662378..38b8543 100644
--- a/src/Template/Main/blocks.ctp
+++ b/src/Template/Main/blocks.ctp
@@ -134,236 +134,7 @@
-
+
end(); ?>
diff --git a/src/Template/Main/stats.ctp b/src/Template/Main/stats.ctp
index 36e056b..e10122d 100644
--- a/src/Template/Main/stats.ctp
+++ b/src/Template/Main/stats.ctp
@@ -1,19 +1,34 @@
assign('title', 'Stats & Rich List') ?>
-start('script'); ?>
-
-end(); ?>
-
element('header') ?>
+start('script'); ?>
+
+
+
+
+
+end(); ?>
+
+start('css');
+ echo $this->Html->css('/css/mining-inflation-chart.css');
+ echo $this->Html->css('https://www.amcharts.com/lib/3/plugins/export/export.css');
+ $this->end();
+ ?>
LBRY Stats
-
+
+
+
+
Mining Inflation Chart
+
+
+
+
LBRY Rich List (Top 500)
diff --git a/webroot/css/mining-inflation-chart.css b/webroot/css/mining-inflation-chart.css
new file mode 100644
index 0000000..92f1b34
--- /dev/null
+++ b/webroot/css/mining-inflation-chart.css
@@ -0,0 +1,8 @@
+.mining-inflation-chart-container { width: 1200px; margin: 0 auto 48px auto; box-shadow: 0 2px 6px rgba(0,0,0,.175); border: 1px solid rgba(0,0,0,.15); padding: 24px 36px; position: relative; overflow: hidden }
+.mining-inflation-chart-container .load-progress { position: absolute; top: 0; left: 0; width: 100%; height: 3px; background: #1e88e5; animation: indeterminate 4s linear infinite; }
+.mining-inflation-chart-container .chart { height: 414px }
+.mining-inflation-chart-container .btn-chart-export { position: absolute; right: 40px; bottom: 36px }
+@keyframes indeterminate {
+ from { left: -70%; }
+ to { left: 100% }
+}
\ No newline at end of file
diff --git a/webroot/js/block-size-chart.js b/webroot/js/block-size-chart.js
new file mode 100644
index 0000000..24aba9a
--- /dev/null
+++ b/webroot/js/block-size-chart.js
@@ -0,0 +1,228 @@
+var chart;
+var chartData = [];
+var chartLoadInProgress = false;
+var minPeriod = 'hh';
+var validPeriods = ['24h', '72h', '168h', '30d', '90d', '1y'];
+var defaultPeriod = (validPeriods.indexOf(localStorage.getItem('chartPeriod')) > -1) ? localStorage.getItem('chartPeriod') : '24h';
+var periodGridCounts = {'24h': 24, '72h': 24, '168h': 14, '30d': 30, '90d': 45, '1y': 12 };
+AmCharts.ready(function() {
+ chart = AmCharts.makeChart('block-size-chart', {
+ type: 'serial',
+ theme: 'light',
+ mouseWheelZoomEnabled: true,
+ categoryField: 'date',
+ synchronizeGrid: true,
+ dataProvider: chartData,
+ valueAxes: [
+ {
+ id: 'v-block-size',
+ axisColor: '#1e88e5',
+ axisThickness: 2,
+ labelFunction: function(value) {
+ return (Math.round((value / 1000) * 100)/100).toFixed(2) + ' KB';
+ }
+ },
+ {
+ id: 'v-price',
+ axisColor: '#00e676',
+ offset: 75,
+ gridAlpha: 0,
+ axisThickness: 2,
+ labelFunction: function(value) {
+ return '$' + value.toFixed(2);
+ }
+ }
+ ],
+ categoryAxis: {
+ parseDates: true,
+ minPeriod: minPeriod, // DD for daily
+ autoGridCount: false,
+ minorGridEnabled: true,
+ minorGridAlpha: 0.04,
+ axisColor: '#dadada',
+ twoLineMode: true,
+ dateFormats: [{
+ period: 'fff',
+ format: 'JJ:NN:SS'
+ }, {
+ period: 'ss',
+ format: 'JJ:NN:SS'
+ }, {
+ period: 'mm',
+ format: 'JJ:NN'
+ }, {
+ period: 'hh',
+ format: 'JJ:NN'
+ }, {
+ period: 'DD',
+ format: 'DD'
+ }, {
+ period: 'WW',
+ format: 'DD MMM'
+ }, {
+ period: 'MM',
+ format: 'MMM'
+ }, {
+ period: 'YYYY',
+ format: 'YYYY'
+ }]
+ },
+ graphs: [
+ {
+ id: 'g-block-size',
+ valueAxis: 'v-block-size', // we have to indicate which value axis should be used
+ title: 'Avg Block Size',
+ valueField: 'AvgBlockSize',
+ bullet: 'round',
+ bulletBorderThickness: 1,
+ bulletBorderAlpha: 1,
+ bulletColor: '#ffffff',
+ bulletSize: 5,
+ useLineColorForBulletBorder: true,
+ lineColor: '#1e88e5',
+ hideBulletsCount: 101,
+ balloonText: '[[AvgBlockSize]] KB',
+ switchable: false,
+ balloonFunction: function(item, graph) {
+ var result = graph.balloonText;
+ return result.replace('[[AvgBlockSize]]', (Math.round((item.dataContext.AvgBlockSize / 1000) * 100)/100).toFixed(2));
+ }
+ },
+ {
+ id: 'g-price',
+ valueAxis: 'v-price',
+ title: 'Average Price',
+ valueField: 'AvgUSD',
+ bullet: 'round',
+ bulletBorderThickness: 1,
+ bulletBorderAlpha: 1,
+ bulletColor: '#ffffff',
+ bulletSize: 5,
+ useLineColorForBulletBorder: true,
+ lineColor: '#00e676',
+ balloonText: '$[[AvgUSD]]',
+ balloonFunction: function(item, graph) {
+ var result = graph.balloonText;
+ if (!item.dataContext.AvgUSD) {
+ return '';
+ }
+ return result.replace('[[AvgUSD]]', item.dataContext.AvgUSD.toFixed(2));
+ },
+ hideBulletsCount: 101,
+ labelFunction: function(value) {
+ return '$' + value;
+ },
+ }
+ ],
+ chartCursor: {
+ cursorAlpha: 0.1,
+ fullWidth: true,
+ valueLineBalloonEnabled: true,
+ categoryBalloonColor: '#333333',
+ cursorColor: '#1e88e5',
+ categoryBalloonDateFormat: minPeriod === 'hh' ? 'D MMM HH:NN ' : 'D MMM'
+ },
+ chartScrollbar: {
+ scrollbarHeight: 36,
+ color: '#888888',
+ gridColor: '#bbbbbb'
+ },
+ legend: {
+ marginLeft: 110,
+ useGraphSettings: true,
+ valueAlign: 'right',
+ valueWidth: 60,
+ spacing: 64,
+ valueFunction: function(item, formatted) {
+ if (item.dataContext) {
+ var g = item.graph;
+ if (g.id === 'g-block-size' && item.dataContext.AvgBlockSize > 0) {
+ return g.balloonText.replace('[[AvgBlockSize]]', (Math.round((item.dataContext.AvgBlockSize / 1000) * 100)/100).toFixed(2) );
+ }
+ if (g.id === 'g-price' && item.dataContext.AvgUSD) {
+ return g.balloonText.replace('[[AvgUSD]]', item.dataContext.AvgUSD.toFixed(2));
+ }
+ }
+
+ return formatted;
+ }
+ },
+ export: {
+ enabled: true,
+ fileName: 'lbry-block-size-chart',
+ position: 'bottom-right',
+ divId: 'chart-export'
+ }
+ });
+
+ loadChartData(defaultPeriod);
+});
+
+var loadChartData = function(dataPeriod) {
+ var loadProgress = $('.block-size-chart-container .load-progress');
+ // clear previous chart data
+ $.ajax({
+ url: '/api/v1/charts/blocksize/' + dataPeriod,
+ type: 'get',
+ dataType: 'json',
+ beforeSend: function() {
+ chartLoadInProgress = true;
+ loadProgress.css({ display: 'block' });
+ },
+ success: function(response) {
+ if (response.success) {
+ chartData = [];
+ var data = response.data;
+ for (var period in data) {
+ if (data.hasOwnProperty(period)) {
+ chartData.push({
+ date: Date.parse(period),
+ AvgBlockSize: data[period].AvgBlockSize,
+ AvgUSD: data[period].AvgUSD
+ });
+ }
+ }
+
+ // save selcted period to localStorage
+ localStorage.setItem('chartPeriod', dataPeriod);
+
+ if (chart) {
+ var isHourly = (dataPeriod.indexOf('h') > -1);
+ var gridCount = periodGridCounts[dataPeriod];
+ chart.categoryAxis.minPeriod = isHourly ? 'hh' : 'DD';
+ chart.categoryAxis.dateFormats[4].format = isHourly ? 'DD MMM' : 'DD';
+ chart.chartCursor.categoryBalloonDateFormat = isHourly ? 'D MMM HH:NN ' : 'D MMM YYYY';
+ chart.categoryAxis.gridCount = gridCount;
+ chart.chartScrollbar.gridCount = gridCount;
+ chart.dataProvider = chartData;
+ chart.validateNow();
+ chart.validateData();
+ }
+ }
+ },
+ complete: function() {
+ chartLoadInProgress = false;
+ loadProgress.css({ display: 'none' });
+ }
+ });
+};
+
+$(document).ready(function() {
+ $('.block-size-data-links a').on('click', function(evt) {
+ evt.preventDefault();
+ if (chartLoadInProgress) {
+ return;
+ }
+
+ var link = $(this);
+ if (link.hasClass('active')) {
+ return;
+ }
+
+ link.addClass('active').siblings().removeClass('active');
+ var period = link.attr('data-period');
+ loadChartData(period);
+ });
+
+ $('a[data-period="' + defaultPeriod + '"]').addClass('active').siblings().removeClass('active');
+});
\ No newline at end of file
diff --git a/webroot/js/mining-inflation-chart.js b/webroot/js/mining-inflation-chart.js
new file mode 100644
index 0000000..e43411c
--- /dev/null
+++ b/webroot/js/mining-inflation-chart.js
@@ -0,0 +1,265 @@
+function getReward(blockHeight) {
+ if (blockHeight == 0) {
+ return 400000000;
+ }
+ else if (blockHeight <= 5100) {
+ return 1;
+ }
+ else if (blockHeight <= 55000) {
+ return 1 + Math.floor((blockHeight - 5001) / 100);
+ }
+ else {
+ var level = Math.floor((blockHeight - 55001) / 32);
+ var reduction = Math.floor((Math.floor(Math.sqrt((8 * level) + 1)) - 1) / 2);
+ while(!(withinLevelBounds(reduction, level))) {
+ if(Math.floor((reduction * reduction + reduction) / 2) > level) {
+ reduction--;
+ }
+ else {
+ reduction++;
+ }
+ }
+ return Math.max(0, 500 - reduction);
+ }
+}
+
+function withinLevelBounds(reduction, level) {
+ if(Math.floor((reduction * reduction + reduction) / 2) > level) {
+ return false;
+ }
+ reduction += 1;
+ if(Math.floor((reduction * reduction + reduction) / 2) <= level) {
+ return false;
+ }
+ return true;
+}
+
+function getAverageBlockTime(blocks) {
+ var numBlocks = blocks.length;
+ var windowSize = 100;
+ var sum = 0;
+ for(i = numBlocks - windowSize; i < numBlocks; i++) {
+ sum += blocks[i].block_time - blocks[i-1].block_time;
+ }
+ return sum / windowSize;
+}
+
+function buildChartData(blockData) {
+ var chartData = [];
+ var supply = 0;
+ var reward = 0;
+ var averageBlockTime = getAverageBlockTime(blockData);
+ var blockTime = 0;
+ var lastBlock = 4071017; // Last block with reward
+ var skip = 100;
+ var blocksPerYear = Math.floor((3600*24*365) / averageBlockTime);
+ var historicalSupply = {};
+ var lastYearSupply = 0;
+ var lastYearBlock = 0;
+ var inflationRate = 0;
+ for(var i = 0; i < lastBlock; i++) {
+ reward = getReward(i);
+ supply += reward;
+ historicalSupply[i + 1] = supply;
+ if(i == 0) { // Reward for 1st block set to 0 for scale
+ reward = 0;
+ }
+ if(i < blockData.length) {
+ // Historical Data
+ var b = blockData[i];
+ blockTime = b.block_time;
+ }
+ else {
+ // Future blocks
+ skip = 1000;
+ blockTime += averageBlockTime;
+ }
+ // Inflation Rate
+ if(i + 1 - blocksPerYear <= 0) {
+ lastYearBlock = 1;
+ }
+ else {
+ lastYearBlock = i + 1 - blocksPerYear;
+ }
+ lastYearSupply = historicalSupply[lastYearBlock];
+ inflationRate = ((supply - lastYearSupply) / lastYearSupply) * 100;
+ if(i % skip == 0) { // Only push 1/ of all blocks to optimize data loading
+ chartData.push({
+ date: new Date(blockTime * 1000),
+ date: new Date(blockTime * 1000),
+ AvailableSupply: supply,
+ RewardLBC: reward,
+ InflationRate: inflationRate,
+ BlockId: i + 1
+ });
+ }
+ }
+ return chartData;
+}
+
+function loadChartData() {
+ var api_url = "https://chainquery.lbry.io/api/sql?query=";
+ var query = "SELECT id, block_time FROM block";
+ var url = api_url + query;
+ var loadProgress = $('.mining-inflation-chart-container .load-progress');
+ $.ajax({
+ url: url,
+ type: 'get',
+ dataType: 'json',
+ beforeSend: function() {
+ chartLoadInProgress = true;
+ loadProgress.css({ display: 'block' });
+ },
+ success: function(response) {
+ if(response.success) {
+ chartData = buildChartData(response.data);
+ if(chart) {
+ chart.dataProvider = chartData;
+ chart.validateNow();
+ chart.validateData();
+ }
+ }
+ else {
+ console.log("Could not fetch block data.");
+ }
+ },
+ complete: function() {
+ chartLoadInProgress = false;
+ loadProgress.css({ display: 'none' });
+ }
+});
+}
+
+var chart;
+var chartData = [];
+var chartLoadInProgress = false;
+AmCharts.ready(function() {
+chart = AmCharts.makeChart('mining-inflation-chart', {
+type: 'serial',
+theme: 'light',
+mouseWheelZoomEnabled: true,
+height: '100%',
+categoryField: 'date',
+synchronizeGrid: true,
+dataProvider: chartData,
+responsive: {
+ enabled: true,
+},
+valueAxes: [
+{
+ id: 'v-supply',
+ axisColor: '#1e88e5',
+ axisThickness: 2,
+ position: 'left',
+ labelFunction: function(value) {
+ return (Math.round((value / 1000000) * 1000000)/1000000).toFixed(2);
+ }
+},
+{
+ id: 'v-reward',
+ axisColor: '#0b7a06',
+ axisThickness: 2,
+ position: 'left',
+ offset: 75,
+},
+{
+ id: 'v-inflation-rate',
+ axisColor: '#ff9900',
+ axisThickness: 2,
+ position: 'right',
+ labelFunction: function(value) {
+ return value.toFixed(2);
+ }
+},
+],
+categoryAxis: {
+parseDates: true,
+autoGridCount: false,
+minorGridEnabled: true,
+minorGridAlpha: 0.04,
+axisColor: '#dadada',
+twoLineMode: true
+},
+graphs: [
+{
+ id: 'g-supply',
+ valueAxis: 'v-supply', // we have to indicate which value axis should be used
+ title: 'Available supply (millions LBC)',
+ valueField: 'AvailableSupply',
+ bullet: 'round',
+ bulletBorderThickness: 1,
+ bulletBorderAlpha: 1,
+ bulletColor: '#ffffff',
+ bulletSize: 5,
+ useLineColorForBulletBorder: true,
+ lineColor: '#1e88e5',
+ hideBulletsCount: 101,
+ balloonText: '[[AvailableSupply]]',
+ balloonFunction: function(item, graph) {
+ var result = graph.balloonText;
+ return result.replace('[[AvailableSupply]]', (Math.round((item.dataContext.AvailableSupply / 1000000) * 1000000)/1000000).toFixed(2));
+ }
+},
+{
+ id: 'g-reward',
+ valueAxis: 'v-reward',
+ title: 'Block Reward (LBC)',
+ valueField: 'RewardLBC',
+ bullet: 'round',
+ bulletBorderThickness: 1,
+ bulletBorderAlpha: 1,
+ bulletColor: '#ffffff',
+ bulletSize: 5,
+ useLineColorForBulletBorder: true,
+ lineColor: '#0b7a06',
+ balloonText: '[[RewardLBC]] LBC
Block [[BlockId]]',
+ hideBulletsCount: 101
+},
+{
+ id: 'g-inflation-rate',
+ valueAxis: 'v-inflation-rate',
+ title: 'Annualized Inflation Rate',
+ valueField: 'InflationRate',
+ bullet: 'round',
+ bulletBorderThickness: 1,
+ bulletBorderAlpha: 1,
+ bulletColor: '#ffffff',
+ bulletSize: 5,
+ useLineColorForBulletBorder: true,
+ lineColor: '#ff9900',
+ balloonText: '[[InflationRate]]%',
+ hideBulletsCount: 101,
+ balloonFunction: function(item, graph) {
+ var result = graph.balloonText;
+ return result.replace('[[InflationRate]]', item.dataContext.InflationRate.toFixed(2));
+ }
+}
+],
+chartCursor: {
+cursorAlpha: 0.1,
+fullWidth: true,
+valueLineBalloonEnabled: true,
+categoryBalloonColor: '#333333',
+cursorColor: '#1e88e5'
+},
+chartScrollbar: {
+scrollbarHeight: 36,
+color: '#888888',
+gridColor: '#bbbbbb'
+},
+legend: {
+marginLeft: 110,
+useGraphSettings: true,
+valueText: "",
+spacing: 64,
+
+},
+export: {
+enabled: true,
+fileName: 'lbry-supply-chart',
+position: 'bottom-right',
+divId: 'chart-export'
+}
+});
+loadChartData();
+});
\ No newline at end of file