diff --git a/src/app/app.js b/src/app/app.js index 67233bd..5787228 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -1,51 +1,51 @@ -/* eslint no-unused-vars: [2, {"args": "after-used"}] */ -'use strict'; +/* eslint no-unused-vars: [1, {"args": "after-used"}] */ +'use strict' // HACK to disable transitions // when the doc is not in view function flushD3Transitions() { - var now = Date.now; + var now = Date.now Date.now = function() { - return Infinity; - }; + return Infinity + } - d3.timer.flush(); - Date.now = now; + d3.timer.flush() + Date.now = now } -var D3transition = d3.selection.prototype.transition; +var D3transition = d3.selection.prototype.transition d3.selection.prototype.transition = function() { if (document.hidden) { - setImmediate(flushD3Transitions); + setImmediate(flushD3Transitions) } - return D3transition.apply(this, arguments); -}; + return D3transition.apply(this, arguments) +} // TODO: change landing.js and elsewhere // to use local version function commas(number, precision) { if (number === 0) { - return 0; + return 0 } else if (!number) { - return null; + return null } - var parts = number.toString().split('.'); + var parts = number.toString().split('.') - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') if (precision && parts[1]) { - parts[1] = parts[1].substring(0, precision); + parts[1] = parts[1].substring(0, precision) while (precision > parts[1].length) { - parts[1] += '0'; + parts[1] += '0' } } else if (precision === 0) { - return parts[0]; + return parts[0] } - return parts.join('.'); + return parts.join('.') } angular.element(document).ready(function() { @@ -81,189 +81,182 @@ angular.element(document).ready(function() { 'jsonFormatter' ]) .config(function myAppConfig($urlRouterProvider) { - $urlRouterProvider.otherwise('/'); + $urlRouterProvider.otherwise('/') }) .run(function($window, $rootScope) { if (typeof navigator.onLine !== 'undefined') { - $rootScope.online = navigator.onLine; + $rootScope.online = navigator.onLine $window.addEventListener('offline', function() { $rootScope.$apply(function() { - $rootScope.online = false; - }); - }, false); + $rootScope.online = false + }) + }, false) $window.addEventListener('online', function() { $rootScope.$apply(function() { - $rootScope.online = true; - }); - }, false); + $rootScope.online = true + }) + }, false) } }) .controller('AppCtrl', function AppCtrl($scope, gateways) { - var last; + var last function checkLast() { if (last && moment().diff(last) > 6000) { - $scope.connectionStatus = 'disconnected'; - last = null; + $scope.connectionStatus = 'disconnected' + last = null } } function handleLedger(d) { if (d) { - var drops = d.totalDrops; - var totalXRP = [ - commas(Number(drops.slice(0, -6))), - drops.slice(-6, -4) - ].join('.'); - - $scope.connectionStatus = 'connected'; - $scope.ledgerLabel = 'Ledger #'; - $scope.ledgerIndex = commas(d.ledgerVersion); - $scope.totalCoins = totalXRP; - $scope.totalXRP = parseFloat(drops) / 1000000.0; - $scope.$apply(); + $scope.connectionStatus = 'connected' + $scope.ledgerLabel = 'Ledger #' + $scope.ledgerIndex = d.ledgerVersion + $scope.totalXRP = parseFloat(d.totalDrops) / 1000000.0 + $scope.$apply() } } - $scope.theme = store.get('theme') || Options.theme || 'dark'; + $scope.theme = store.get('theme') || Options.theme || 'dark' $scope.$watch('theme', function() { - store.set('theme', $scope.theme); - }); + store.set('theme', $scope.theme) + }) $scope.toggleTheme = function() { if ($scope.theme === 'dark') { - $scope.theme = 'light'; + $scope.theme = 'light' } else { - $scope.theme = 'dark'; + $scope.theme = 'dark' } - }; + } $scope.snapOptions = { disable: 'right', maxPosition: 267 - }; + } // disable touch drag for desktop devices if (!Modernizr.touch) { - $scope.snapOptions.touchToDrag = false; + $scope.snapOptions.touchToDrag = false } $scope.$on('$stateChangeSuccess', function(event, toState) { if (ga) { - ga('send', 'pageview', toState.name); + ga('send', 'pageview', toState.name) } if (angular.isDefined(toState.data.pageTitle)) { - $scope.pageTitle = toState.data.pageTitle + ' | Ripple Charts'; + $scope.pageTitle = toState.data.pageTitle + ' | Ripple Charts' } else { - $scope.pageTitle = 'Ripple Charts'; + $scope.pageTitle = 'Ripple Charts' } - }); + }) - // connect to the ripple network; - remote = new ripple.RippleAPI(Options.ripple); + // connect to the ripple network + remote = new ripple.RippleAPI(Options.ripple) remote.connect() .then(function() { - $scope.connectionStatus = 'connected'; - $scope.$apply(); + $scope.connectionStatus = 'connected' + $scope.$apply() }) .catch(function(e) { - console.log(e.stack); - }); + console.log(e.stack) + }) - $scope.ledgerLabel = 'connecting...'; - $scope.ledgerIndex = ''; - $scope.connectionStatus = 'disconnected'; + $scope.ledgerLabel = 'connecting...' + $scope.ledgerIndex = '' + $scope.connectionStatus = 'disconnected' // get ledger number and total coins remote.on('ledger', function(d) { - last = moment(); + last = moment() remote.getLedger({ ledgerVersion: d.ledgerVersion }).then(handleLedger) .catch(function(e) { - console.log(e.stack); - }); - }); + console.log(e.stack) + }) + }) remote.on('error', function(e) { - console.log(e); - }); + console.log(e) + }) - setInterval(checkLast, 2000); + setInterval(checkLast, 2000) // remove loader after gateways resolves gateways.promise.then(function() { - var loading = d3.select('#loading'); + var loading = d3.select('#loading') loading.transition() .duration(600) .style('opacity', 0) .each('end', function() { - loading.style('display', 'none'); - }); - }); + loading.style('display', 'none') + }) + }) // reconnect when coming back online $scope.$watch('online', function(online) { if (online) { - remote.connect(); + remote.connect() } - }); - }); + }) + }) - var api; - var banner; - var wrap; - var maintenance; - var bannerPads; - var started = false; + var api + var banner + var wrap + var maintenance + var bannerPads + var started = false function checkStatus() { api.getMaintenanceStatus(function(err, resp) { - var mode = 'maintenance'; - var title = 'This site is under maintenance.'; - var html = ''; - var style = ''; - var height; + var mode = 'maintenance' + var title = 'This site is under maintenance.' + var html = '' + var style = '' + var height if (err) { - console.log(err); + console.log(err) if (err.status === 0) { - title = 'Unable to connect to the data service.'; + title = 'Unable to connect to the data service.' } else { - title = err.message || err.text; - html += err.status; + title = err.message || err.text + html += err.status } } else { - mode = resp && resp.mode ? resp.mode : 'normal'; - html = resp && resp.html ? resp.html : ''; - style = resp && resp.style ? resp.style : ''; + mode = resp && resp.mode ? resp.mode : 'normal' + html = resp && resp.html ? resp.html : '' + style = resp && resp.style ? resp.style : '' } // start the app if (!started && mode !== 'maintenance') { - angular.bootstrap(document, ['ripplecharts']); - started = true; + angular.bootstrap(document, ['ripplecharts']) + started = true } // show maintenance if (mode === 'maintenance') { maintenance.select('.title') - .html(title); + .html(title) maintenance.select('.subtitle') - .html(html); + .html(html) maintenance .style('display', 'block') .transition() .duration(1000) - .style('opacity', 1); + .style('opacity', 1) // hide maintenance } else { @@ -272,63 +265,63 @@ angular.element(document).ready(function() { .duration(1000) .style('opacity', 0) .each('end', function() { - maintenance.style('display', 'none'); - }); + maintenance.style('display', 'none') + }) } // show banner if (mode === 'banner') { - height = banner.style('height'); + height = banner.style('height') banner.html(html) - .style(style); + .style(style) wrap.style('height', height) .transition() .delay(2000) .duration(1000) - .style('height', banner.style('height')); + .style('height', banner.style('height')) banner .transition() .delay(2000) .duration(1000) - .style('opacity', 1); + .style('opacity', 1) bannerPads .transition() .delay(2000) .duration(1000) - .style('height', banner.style('height')); + .style('height', banner.style('height')) // hide banner } else { wrap.transition() .duration(1000) - .style('height', '0px'); + .style('height', '0px') bannerPads.transition() .duration(1000) - .style('height', '0px'); + .style('height', '0px') banner.transition() .duration(1000) .style('opacity', 0) .each('end', function() { - banner.html(''); - }); + banner.html('') + }) } - }); + }) } setTimeout(function() { - api = new ApiHandler(API); - wrap = d3.select('.banner-wrap'); - banner = wrap.select('.banner'); - maintenance = d3.select('#maintenance'); - bannerPads = d3.selectAll('.banner-pad'); - checkStatus(); - }); - - setInterval(checkStatus, 2 * 60 * 1000); -}); + api = new ApiHandler(API) + wrap = d3.select('.banner-wrap') + banner = wrap.select('.banner') + maintenance = d3.select('#maintenance') + bannerPads = d3.selectAll('.banner-pad') + checkStatus() + }) + + setInterval(checkStatus, 2 * 60 * 1000) +}) diff --git a/src/app/landing/landing.js b/src/app/landing/landing.js index 5b8ad72..bb8d00d 100644 --- a/src/app/landing/landing.js +++ b/src/app/landing/landing.js @@ -1,9 +1,14 @@ /* global MultiMarket, ValueSummary */ -'use strict'; +'use strict' angular.module('ripplecharts.landing', [ 'ui.state' ]) +.filter('trust', ['$sce', function($sce) { + return function(htmlCode) { + return $sce.trustAsHtml(htmlCode) + } +}]) .config(function config($stateProvider) { $stateProvider.state('landing', { url: '/', @@ -16,154 +21,123 @@ angular.module('ripplecharts.landing', [ data: {}, resolve: { gateInit: function(gateways) { - return gateways.promise; + return gateways.promise } } - }); + }) }) .controller('LandingCtrl', function LandingCtrl($scope, $state, gateways) { - var api = new ApiHandler(API); + var api = new ApiHandler(API) var donut = new ValueSummary({ id: 'metricDetail', gateways: gateways - }); + }) + + var exchangeRates = {} + var refreshInterval - var exchangeRates = {}; var valueCurrencies = { USD: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', // bitstamp EUR: 'rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq', // gatehub JPY: 'r94s8px6kSw1uZ1MV98dhSRTvc6VMPoPcN', // tokoyo jpy CNY: 'rKiCet8SdvWxPXnAgYarFUXMh1zCPz432Y', // ripplefox XRP: '' - }; + } - var totalAccounts; - var paymentVolumeXRP; - var tradeVolumeXRP; - var valueInterval; + $scope.metrics = { + totalTradeVolume: { + label: 'Total XRP Trade Volume (All Exchanges)' + }, + tradeVolumeRCL: { + label: 'Ripple Network Trade Volume', + link: '#/trade-volume' + }, + paymentVolumeRCL: { + label: 'Ripple Network Payment Volume' + }, + capitalizationXRP: { + label: 'XRP Capitalization' + }, + numAccounts: { + label: '# of Ripple Accounts' + } + } + + for (var key in $scope.metrics) { + $scope.metrics[key].key = key + } - var formatNumber = d3.format(',g'); + $scope.currencies = Object.keys(valueCurrencies) + $scope.selectedCurrency = 'USD' - // present amount in human readable format - function commas(d, precision) { - return formatNumber(Number(d.toFixed(precision || 0))); + + $scope.showMetricDetails = function(name) { + + if (name) { + $scope.selectedMetric = $scope.metrics[name] + } + + donut.load($scope.selectedMetric, { + rate: 1 / $scope.valueRate, + currency: $scope.selectedCurrency + }) } + /** + * getTotalAccounts + */ - // get num accounts function getTotalAccounts() { api.getTotalAccounts(null, function(err, total) { if (err) { - console.log(err); - } - - if (total) { - totalAccounts = total; // save for new account updates; + console.log(err) } - $scope.totalAccounts = total ? commas(total) : ' '; - $scope.$apply(); - }); + $scope.metrics.numAccounts.total = total + $scope.$apply() + }) } - // look for new accounts from the websocket feed + /** + * handleNewAccount + */ + function handleNewAccount(tx) { - var meta = tx.meta; + var meta = tx.meta if (meta.TransactionResult !== 'tesSUCCESS') { - return; + return } meta.AffectedNodes.forEach(function(affNode) { if (affNode.CreatedNode && affNode.CreatedNode.LedgerEntryType === 'AccountRoot') { - $scope.totalAccounts = totalAccounts ? commas(++totalAccounts) : ' '; - $scope.$apply(); - } - }); - } - - // display the selected metric on the page, if its ready - function showValue(metric) { - var ex = { - rate: $scope.valueRate, - currency: $scope.valueCurrency - }; - var sign; - var value; - var precision; - - if (typeof $scope.valueRate === 'undefined') { - return; - } - - if (metric === 'paymentVolume') { - if (typeof paymentVolumeXRP === 'undefined') { - return; + $scope.metrics.numAccounts.total++ + $scope.$apply() } - - if (metric === $scope.metricDetail) { - donut.load(paymentVolumeXRP, ex); - } - - value = paymentVolumeXRP.total / $scope.valueRate; - precision = 2; - - } else if (metric === 'tradeVolume') { - if (typeof tradeVolumeXRP === 'undefined') { - return; - } - - if (metric === $scope.metricDetail) { - donut.load(tradeVolumeXRP, ex); - } - - value = tradeVolumeXRP.total / $scope.valueRate; - precision = 2; - - } else if (metric === 'xrpCapitalization') { - if (!$scope.totalXRP) { - return; - } - - value = $scope.totalXRP / $scope.valueRate; - precision = 0; - } - - switch ($scope.valueCurrency) { - case 'USD': sign = '$'; break; - case 'JPY': sign = '¥'; break; - case 'CNY': sign = '¥'; break; - case 'EUR': sign = '€'; break; - case 'XRP': sign = ''; break; - default: sign = ''; - } - - $scope[metric] = value ? sign + commas(value, precision) : ' '; - $scope.$apply(); + }) } // get the exchange rate from the API function getExchangeRate(c, callback) { api.exchangeRate({ base: { - currency: c.currency, - issuer: c.issuer + currency: 'XRP' }, counter: { - currency: 'XRP' + currency: c.currency, + issuer: c.issuer } }, function(err, rate) { if (err) { - callback(err); - return; + callback(err) + return } // cache for future reference - exchangeRates[c.currency + '.' + c.issuer] = rate; - - callback(null, rate); - }); + exchangeRates[c.currency + '.' + c.issuer] = Number(rate) + callback(null, rate) + }) } // set the value rate for the selected @@ -171,116 +145,220 @@ angular.module('ripplecharts.landing', [ // API if its not cached or // if we are updating the cache function setValueRate(currency, useCached, callback) { - var issuer = valueCurrencies[currency]; + var issuer = valueCurrencies[currency] + $scope.valueRate = undefined + $scope.valueRatePair = '' + + function apply() { + $scope.valueRate = exchangeRates[currency + '.' + issuer] + $scope.valueRate = $scope.valueRate.toPrecision(4) + $scope.valueRatePair = 'XRP/' + currency + callback() + } if (currency === 'XRP') { - $scope.valueRate = 1; - $scope.valueRateDisplay = ''; - callback(); - return; - } + $scope.valueRate = 1 + $scope.valueRatePair = '' + callback() + return // check for cached - if (useCached && exchangeRates[currency + '.' + issuer]) { - $scope.valueRate = exchangeRates[currency + '.' + issuer]; - $scope.valueRateDisplay = commas(1 / $scope.valueRate, 4) + - ' XRP/' + currency; - callback(); - return; + } else if (useCached && exchangeRates[currency + '.' + issuer]) { + apply() + return } - getExchangeRate({ currency: currency, issuer: issuer }, function(err) { if (err) { - console.log(err); - $scope.valueRate = 0; - callback(err); - return; + console.log(err) + callback(err) + $scope.$apply() + return } - $scope.valueRate = exchangeRates[currency + '.' + issuer] || 0; - if ($scope.valueRate) { - $scope.valueRateDisplay = commas(1 / $scope.valueRate, 4) + - ' XRP/' + currency; + apply() + $scope.$apply() + }) + } + + /** + * setMetricValue + */ + + function setMetricValue(metric, value) { + if (value) { + $scope.metrics[metric].total = value + } + + if ($scope.metrics[metric].total && $scope.valueRate) { + $scope.metrics[metric].converted = + $scope.valueRate * $scope.metrics[metric].total + } + } + + /** + * filterXRPVolume + */ + + function filterXRPVolume(components) { + var total = 0 + var count = 0 + + components.forEach(function(c) { + if (c.base.currency === 'XRP' || + c.counter.currency === 'XRP') { + total += c.converted_amount + count += c.count } - callback(); - }); + }) + + return { + source: 'rcl', + base_volume: total, + count: count + } } - // get values for the various metrics - function getValues() { + /** + * getMetricValues + */ - setValueRate($scope.valueCurrency, false, function() { - showValue('paymentVolume'); - showValue('tradeVolume'); - showValue('xrpCapitalization'); - }); + function getMetricValues() { + // get payments api.getPaymentVolume({}, function(err, resp) { - var data; - if (err || !resp || !resp.rows) { - console.log(err); - data = {total: 0}; + var total = 0 + var components + + if (err || !resp || !resp.rows || !resp.rows.length) { + console.log(err) } else { - data = resp.rows[0]; + components = resp.rows[0].components + total = resp.rows[0].total } - paymentVolumeXRP = data; - showValue('paymentVolume'); - }); + $scope.metrics.paymentVolumeRCL.components = components + setMetricValue('paymentVolumeRCL', total) + if ($scope.selectedMetric === $scope.metrics.paymentVolumeRCL) { + $scope.showMetricDetails() + } + $scope.$apply() + }) + // get RCL exchanges api.getExchangeVolume({}, function(err, resp) { - var data; - if (err || !resp || !resp.rows) { - console.log(err); - data = {total: 0}; + var total = 0 + var components + var xrpVolume + + if (err || !resp || !resp.rows || !resp.rows.length) { + console.log(err) + + } else { + components = resp.rows[0].components + total = resp.rows[0].total + } + + $scope.metrics.tradeVolumeRCL.components = components + xrpVolume = filterXRPVolume(components) + + // add RCL XRP volume to total metric + if ($scope.metrics.totalTradeVolume.components && + !$scope.metrics.totalTradeVolume.withRCL) { + $scope.metrics.totalTradeVolume.components.unshift(xrpVolume) + setMetricValue('totalTradeVolume', + $scope.metrics.totalTradeVolume.total + xrpVolume.base_volume) + } + + setMetricValue('tradeVolumeRCL', total) + if ($scope.selectedMetric === $scope.metrics.tradeVolumeRCL) { + $scope.showMetricDetails() + } + $scope.$apply() + }) + + // get external exchanges + api.getExternalMarkets({}, function(err, resp) { + var total = 0 + var components + var xrpVolume + + if (err || !resp) { + console.log(err) + } else { - data = resp.rows[0]; + components = resp.data.components + total = Number(resp.data.total) } - tradeVolumeXRP = data; - showValue('tradeVolume'); - }); + $scope.metrics.totalTradeVolume.withRCL = false + $scope.metrics.totalTradeVolume.components = components + + + // add RCL XRP volume + if ($scope.metrics.tradeVolumeRCL.components) { + xrpVolume = filterXRPVolume($scope.metrics.tradeVolumeRCL.components) + $scope.metrics.totalTradeVolume.components.unshift(xrpVolume) + total += xrpVolume.base_volume + $scope.metrics.totalTradeVolume.withRCL = true + } + + setMetricValue('totalTradeVolume', total) + if ($scope.selectedMetric === $scope.metrics.totalTradeVolume) { + $scope.showMetricDetails() + } + $scope.$apply() + }) } - $scope.valueCurrency = 'USD'; - $scope.metricDetail = 'tradeVolume'; - $scope.metricDetailTitle = 'Trade Volume (last 24 hours)'; - - // dropdown to change currency for metrics - var valueSelect = d3.select('#valueCurrency') - .on('change', function() { - var currency = this.value; - setValueRate(currency, true, function() { - $scope.valueCurrency = currency; - showValue('paymentVolume'); - showValue('tradeVolume'); - showValue('xrpCapitalization'); - }); - }); - - valueSelect.selectAll('option') - .data(d3.keys(valueCurrencies)) - .enter().append('option') - .html(function(d) { - return d; + $scope.$watch('selectedCurrency', function(d) { + var name + + switch (d) { + case 'USD': + $scope.sign = '$' + break + case 'JPY': + $scope.sign = '¥' + break + case 'CNY': + $scope.sign = '¥' + break + case 'EUR': + $scope.sign = '€' + break + case 'XRP': + $scope.sign = '' + break + default: + $scope.sign = '' + } + + for (name in $scope.metrics) { + $scope.metrics[name].converted = undefined + } + + setValueRate(d, true, function() { + for (name in $scope.metrics) { + setMetricValue(name) + } + + $scope.showMetricDetails() + }) }) - .attr('selected', function(d) { - return d === $scope.valueCurrency.currency; - }); // add to new accounts total - remote.on('transaction_all', handleNewAccount); + remote.on('transaction_all', handleNewAccount) remote.on('connect', function() { - getTotalAccounts(); - }); + getTotalAccounts() + }) - getTotalAccounts(); + getTotalAccounts() // get 'fixed' multimarket charts for // the most important markets @@ -291,9 +369,9 @@ angular.module('ripplecharts.landing', [ clickable: true, updateInterval: 60, gateways: gateways - }); + }) - markets.list(9); + markets.list(9) markets.on('chartClick', function(chart) { $state.transitionTo('markets.pair', { @@ -304,64 +382,46 @@ angular.module('ripplecharts.landing', [ interval: '5m', range: '1d', type: store.get('chartType') || 'line' - }); - }); + }) + }) // show the helper text the first time we visit the page if (!store.get('returning')) { setTimeout(function() { - d3.select('#helpButton_new').node().click(); - }, 100); + d3.select('#helpButton_new').node().click() + }, 100) } - $scope.$watch('totalCoins', function() { - setTimeout(function() { - showValue('xrpCapitalization'); - }); - }); - - $scope.$watch('metricDetail', function() { - - var ex = { - rate: $scope.valueRate, - currency: $scope.valueCurrency - }; - - if ($scope.metricDetail === 'paymentVolume') { - $scope.metricDetailTitle = 'Payment Volume (last 24 hours)'; - donut.load(paymentVolumeXRP, ex); - - } else if ($scope.metricDetail === 'tradeVolume') { - $scope.metricDetailTitle = 'Trade Volume (last 24 hours)'; - donut.load(tradeVolumeXRP, ex); - } - }); + $scope.$watch('totalXRP', function(d) { + setMetricValue('capitalizationXRP', d) + }) // stuff to do when leaving the page $scope.$on('$destroy', function() { - markets.list([]); + markets.list([]) if (!store.get('returning') && $scope.showHelp) { setTimeout(function() { - d3.select('#helpButton_new').node().click(); - }, 50); + d3.select('#helpButton_new').node().click() + }, 50) } - store.set('returning', true); - clearInterval(valueInterval); - }); + store.set('returning', true) + clearInterval(refreshInterval) + }) // reload data when coming back online $scope.$watch('online', function(online) { if (online) { - markets.reload(); + markets.reload() } - }); + }) // get value metrics at load time and every 5 minutes - getValues(); - valueInterval = setInterval(getValues, 300000); -}); + getMetricValues() + refreshInterval = setInterval(getMetricValues, 60 * 5 * 1000) + $scope.showMetricDetails('totalTradeVolume') +}) diff --git a/src/app/landing/landing.less b/src/app/landing/landing.less index e1d3eba..6fa0dd9 100644 --- a/src/app/landing/landing.less +++ b/src/app/landing/landing.less @@ -14,101 +14,92 @@ } .landing h5 { - font-size:14px; + font-size:12px; font-weight:bold; text-align:left; - margin:5px; - margin-top:15px; - overflow:hidden; + margin:15px 5px 5px 5px; +} + +.landing sign { + margin-right: -4px; +} + +.landing h5 b { + margin-top: 5px; + display: inline-block +} + +.landing h5 small { + font-size:90%; } .landing .stats ul { list-style:none; - margin:0; + margin:10px 5px 30px 5px; display:table; width:100%; - height:250px; border:1px solid #eee; background:#f8f8f8; - border-radius:2px; color:#555; } .landing .stats li { - padding:8px; - border-radius:2px; - margin:3px; - line-height:25px; + line-height:27px; + font-size: 13px; display:table-row; + color:#999; +} + +.landing .stats .volume-stats li:hover { + background: #eee; +} + +.landing .stats .status { + width: 5px; + display: table-cell; + border-bottom: 1px solid #e5e5e5; +} + +.landing .stats li.selected { + background: #e5e5e5 !important; + color: #000; +} + +.landing .stats li.selected .status { + background: #2A98D0; } .landing .stats label { vertical-align:middle; display:table-cell; text-align:left; - color:#888; - padding:5px 10px 5px 20px; + padding:5px 12px; border-bottom:1px solid #e5e5e5; + cursor: pointer; +} + +.landing .stats .volume-stats label { + padding-left: 7px; } .landing .stats label small { - font-size:70%; - color:#aaa; + font-size: 85%; + opacity: .65; } .landing .stat { vertical-align: middle; display: table-cell; - font-size: 18px; + font-size: 16px; text-align: right; - padding: 5px; + padding: 5px 10px; border-bottom: 1px solid #e5e5e5; - font-weight: bold; -} - -.landing .details { - vertical-align:middle; - display:table-cell; - font-size:10px; - cursor:pointer; - color:#ccc; - min-width: 65px; - border-bottom:1px solid #e5e5e5; - font-weight:bold; - - img { - width:21px; - margin-bottom:2px; - opacity:.15; - padding: 3px; - border-radius: 20px; - background:#aaa; - } -} - -.landing .details:hover { - text-decoration:underline; - color:#999; - - img { - width: 21px; - opacity: .4; - background: #aaa; - } -} - -.landing .details .selected { - color:#666; - - img { - opacity:1; - background:#ccc; - } + cursor: pointer; + color: #555; } -.landing .stat small { - color:#777; - font-weight:normal; +.landing li.selected .stat { + color: #111; } .landing .stat .loader { @@ -118,13 +109,14 @@ } .landing .stats li:last-child .stat, +.landing .stats li:last-child .status, .landing .stats li:last-child label, .landing .stats li:last-child .details { border-bottom:none; } .landing #metricDetail { - height:250px; + height:320px; margin-top:20px; } @@ -169,13 +161,26 @@ } .dark .landing ul { - background:#1a1a1a; - border-color:#252525; + background:#171717; + border-color:#333; +} + +.dark .landing .stats .volume-stats li:hover { + background: #343638; +} + +.dark .landing .stats li.selected { + background: #485057 !important; + color: #ddd; +} + +.dark .landing .stats li.selected .stat { + color: #fff; } .dark .landing .stat, .dark .stats label, -.dark .stats .details { +.dark .stats .status { border-bottom-color:#2a2a2a; } @@ -206,9 +211,6 @@ } } -.dark .landing .stats label small { - color:#666; -} .dark .landing .stat small { font-weight:bold; diff --git a/src/app/landing/landing.tpl.html b/src/app/landing/landing.tpl.html index 44b4928..f68a00b 100644 --- a/src/app/landing/landing.tpl.html +++ b/src/app/landing/landing.tpl.html @@ -9,78 +9,89 @@

Welcome to Ripple Charts

-
+
- Ripple Network Stats - - + Volume Stats (24 hours) + + +
-
    -
  • - +
      +
    • +
      +
      - - + + +
      -
    • -
    • - +
    • +
      +
      - - + + +
      -
      -
      - Detail - -
      +
    • +
    • +
      + +
      + + +
    • +
    +
    + Ripple Network Stats +
    +
    • - +
      - - -
      -
      -
      - Detail - -
      + +
    • - +
      - - + + +
      -
    • - +
      - - + +
      -
    • - +
      - - + +
      -
-
- - Explore -
diff --git a/src/common/apiHandler.js b/src/common/apiHandler.js index 24eaf74..335a34c 100644 --- a/src/common/apiHandler.js +++ b/src/common/apiHandler.js @@ -1,73 +1,123 @@ -ApiHandler = function (baseURL) { - var self = this; - var timeFormat = 'YYYY-MM-DDTHH:mm:ss'; +/* eslint no-unused-vars: 1 */ +'use strict' - self.url = baseURL; +function ApiHandler(baseURL) { + var self = this + var timeFormat = 'YYYY-MM-DDTHH:mm:ss' + + self.url = baseURL + + /** + * formatTime + */ function formatTime(time) { - return moment.utc(time).format(timeFormat); + return moment.utc(time).format(timeFormat) } - this.getTx = function (hash, callback) { - var url = self.url + '/transactions/' + hash; + /** + * getMetric + */ + function getMetric(params, callback) { + var url = self.url + '/network/' + params.type + '?' + + var start = params.start ? + '&start=' + formatTime(params.start) : '' + var end = params.end ? + '&end=' + formatTime(params.end) : '' + var interval = params.interval ? + '&interval=' + params.interval : '' + var currency = params.currency ? + '&exchange_currency=' + params.currency : '' + var issuer = params.issuer ? + '&exchange_issuer=' + params.issuer : '' + var limit = '&limit=' + (params.limit || 1000) + + url += start + end + interval + limit + currency + issuer return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - callback(null, resp); + resp.rows.forEach(function(row) { + row.total = Number(row.total) + row.exchange_rate = Number(row.exchange_rate) + + row.components.forEach(function(c) { + c.rate = Number(c.rate) + c.amount = Number(c.amount) + c.converted_amount = Number(c.converted_amount || '0') + }) + }) + callback(null, resp) } - }); + }) } - this.getAccountTx = function (params, callback) { - var url = self.url + '/accounts/' + params.account + '/transactions'; + this.getTx = function(hash, callback) { + var url = self.url + '/transactions/' + hash + + return d3.json(url, function(err, resp) { + if (err) { + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) + + } else { + callback(null, resp) + } + }) + } + + this.getAccountTx = function(params, callback) { + var url = self.url + '/accounts/' + params.account + '/transactions' var limit = params.limit ? '&limit=' + params.limit : '' var marker = params.marker ? - '&marker=' + params.marker : ''; + '&marker=' + params.marker : '' var descending = params.descending ? - '&descending=true' : ''; + '&descending=true' : '' - url += '?' + limit + marker + descending; + url += '?' + limit + marker + descending return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - callback(null, resp); + callback(null, resp) } - }); + }) } - this.getTopMarkets = function (limit, callback) { + this.getTopMarkets = function(limit, callback) { var order = [ 'XAU', 'XAG', 'BTC', 'ETH', 'LTC', 'XRP', 'EUR', 'USD', 'GBP', 'AUD', - 'NZD', 'USD', 'CAD', 'CHF', 'JPY', 'CNY']; + 'NZD', 'USD', 'CAD', 'CHF', 'JPY', 'CNY'] var url = self.url + '/network/top_markets' + (limit ? '?limit=' + limit : '') return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - var markets = []; + var markets = [] resp.markets.forEach(function(m, i) { if (limit && i >= limit) { - return; + return } var pair = { @@ -79,381 +129,361 @@ ApiHandler = function (baseURL) { currency: m.counter_currency, issuer: m.counter_issuer } - }; + } - var o1 = order.indexOf(pair.base.currency); - var o2 = order.indexOf(pair.counter.currency); + var o1 = order.indexOf(pair.base.currency) + var o2 = order.indexOf(pair.counter.currency) // order by priority if (o1 > o2 || o1 === -1) { pair = { base: pair.counter, counter: pair.base - }; + } } - markets.push(pair); - }); + markets.push(pair) + }) - callback(null, markets); + callback(null, markets) } - }); + }) } - this.offersExercised = function (params, load, error) { + this.offersExercised = function(params, load, error) { - var url = self.url + '/exchanges/'; + var url = self.url + '/exchanges/' var base = params.base.currency + - (params.base.issuer ? '+' + params.base.issuer : ''); + (params.base.issuer ? '+' + params.base.issuer : '') var counter = params.counter.currency + - (params.counter.issuer ? '+' + params.counter.issuer : ''); - var limit = params.timeIncrement === 'all' ? '' : 'limit=' + (params.limit || 1000); + (params.counter.issuer ? '+' + params.counter.issuer : '') + var limit = params.timeIncrement === 'all' ? + '' : 'limit=' + (params.limit || 1000) var interval = params.timeIncrement && params.timeIncrement !== 'all' ? - '&interval=' + (params.timeMultiple || 1) + params.timeIncrement : ''; + '&interval=' + (params.timeMultiple || 1) + params.timeIncrement : '' var start = params.startTime ? - '&start=' + formatTime(params.startTime) : ''; + '&start=' + formatTime(params.startTime) : '' var end = params.endTime ? - '&end=' + formatTime(params.endTime) : ''; - var descending = params.descending ? '&descending=true' : ''; + '&end=' + formatTime(params.endTime) : '' + var descending = params.descending ? '&descending=true' : '' var reduce = params.reduce === true || params.timeIncrement === 'all' ? - '&reduce=true' : ''; + '&reduce=true' : '' url += base + '/' + counter + '?' + limit + - interval + start + end + descending + reduce; + interval + start + end + descending + reduce return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - error(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + error(e) } else if (params.reduce === false) { load(resp.exchanges.map(function(d) { return { - time : moment.utc(d.executed_time), - price : Number(d.rate), - amount : Number(d.base_amount), - amount2 : Number(d.counter_amount), - tx : d.tx_hash, - type : d.taker === d.buyer ? 'buy' : 'sell' + time: moment.utc(d.executed_time), + price: Number(d.rate), + amount: Number(d.base_amount), + amount2: Number(d.counter_amount), + tx: d.tx_hash, + type: d.taker === d.buyer ? 'buy' : 'sell' } - })); + })) } else { load(resp.exchanges.map(function(d) { return { - startTime : moment.utc(d.start), - baseVolume : Number(d.base_volume), - counterVolume : Number(d.counter_volume), - count : d.count, - open : Number(d.open), - high : Number(d.high), - low : Number(d.low), - close : Number(d.close), - vwap : Number(d.vwap), - openTime : d.open_time, - closeTime : d.close_time - }; - })); + startTime: moment.utc(d.start), + baseVolume: Number(d.base_volume), + counterVolume: Number(d.counter_volume), + count: d.count, + open: Number(d.open), + high: Number(d.high), + low: Number(d.low), + close: Number(d.close), + vwap: Number(d.vwap), + openTime: d.open_time, + closeTime: d.close_time + } + })) } - }); + }) } - this.paymentVolume = function (params, load, error) { - var url = self.url + '/payments/'; + this.paymentVolume = function(params, load, error) { + var url = self.url + '/payments/' var currency = params.currency ? params.currency + - (params.issuer ? '+' + params.issuer : '') : ''; - var limit = params.limit || 1000; + (params.issuer ? '+' + params.issuer : '') : '' + var limit = params.limit || 1000 var interval = params.timeIncrement ? - '&interval=' + params.timeIncrement : ''; + '&interval=' + params.timeIncrement : '' var start = params.startTime ? - '&start=' + formatTime(params.startTime) : ''; + '&start=' + formatTime(params.startTime) : '' var end = params.endTime ? - '&end=' + formatTime(params.endTime) : ''; - var descending = params.descending ? '&descending=true' : ''; + '&end=' + formatTime(params.endTime) : '' + var descending = params.descending ? '&descending=true' : '' url += currency + '?limit=' + limit + interval + - start + end + descending; + start + end + descending return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - error(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + error(e) } else { - load(resp); + load(resp) } - }); + }) } - this.issuerCapitalization = function (params, load, error) { + this.issuerCapitalization = function(params, load, error) { var url = self.url + '/capitalization/' + params.currency + - '+' + params.issuer; - var limit = params.limit || 1000; + '+' + params.issuer + var limit = params.limit || 1000 var interval = params.interval ? - '&interval=' + params.interval : ''; + '&interval=' + params.interval : '' var start = params.start ? - '&start=' + formatTime(params.start) : ''; + '&start=' + formatTime(params.start) : '' var end = params.end ? - '&end=' + formatTime(params.end) : ''; - var descending = params.descending ? '&descending=true' : ''; - var adjusted = params.adjusted ? '&adjusted=true' : ''; + '&end=' + formatTime(params.end) : '' + var descending = params.descending ? '&descending=true' : '' + var adjusted = params.adjusted ? '&adjusted=true' : '' url += '?limit=' + limit + interval + - start + end + descending + adjusted; + start + end + descending + adjusted return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - error(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + error(e) } else { - load(resp); + load(resp) } - }); - }; + }) + } this.getTotalAccounts = function(time, callback) { - var url = self.url + '/accounts?reduce=true&start=2013-01-01'; + var url = self.url + '/accounts?reduce=true&start=2013-01-01' if (time) { - url += '&end=' + formatTime(time); + url += '&end=' + formatTime(time) } return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - callback(null, resp ? (resp.count || 0) : 0); + callback(null, resp ? (resp.count || 0) : 0) } - }); - }; + }) + } - this.accountsCreated = function (params, callback) { - var url = self.url + '/accounts?'; + this.accountsCreated = function(params, callback) { + var url = self.url + '/accounts?' var start = params.startTime ? - '&start=' + formatTime(params.startTime) : ''; + '&start=' + formatTime(params.startTime) : '' var end = params.endTime ? - '&end=' + formatTime(params.endTime) : ''; + '&end=' + formatTime(params.endTime) : '' var interval = params.timeIncrement ? - '&interval=' + params.timeIncrement : ''; - var limit = '&limit=' + (params.limit || 1000); + '&interval=' + params.timeIncrement : '' + var limit = '&limit=' + (params.limit || 1000) - url += start + end + interval + limit; + url += start + end + interval + limit return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - callback(null, resp); + callback(null, resp) } - }); + }) + } + + this.getExchangeVolume = function(params, callback) { + params.type = 'exchange_volume' + getMetric(params, callback) } - this.getExchangeVolume = function (params, callback) { - params.type = 'exchange_volume'; - getMetric(params, callback); + this.getPaymentVolume = function(params, callback) { + params.type = 'payment_volume' + getMetric(params, callback) } - this.getPaymentVolume = function (params, callback) { - params.type = 'payment_volume'; - getMetric(params, callback); + this.getIssuedValue = function(params, callback) { + params.type = 'issued_value' + getMetric(params, callback) } - this.getIssuedValue = function (params, callback) { - params.type = 'issued_value'; - getMetric(params, callback); + this.getExternalMarkets = function(params, callback) { + var url = self.url + '/network/external_markets?' + var period = params.period ? + '&period=' + params.period : '' + + url += period + return d3.json(url, function(err, resp) { + if (err) { + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) + + } else { + callback(null, resp) + } + }) } - this.exchangeRate = function (params, callback) { - var url = self.url + '/exchange_rates'; + this.exchangeRate = function(params, callback) { + var url = self.url + '/exchange_rates' var base = '/' + params.base.currency + - (params.base.issuer ? '+' + params.base.issuer : ''); + (params.base.issuer ? '+' + params.base.issuer : '') var counter = '/' + params.counter.currency + - (params.counter.issuer ? '+' + params.counter.issuer : ''); + (params.counter.issuer ? '+' + params.counter.issuer : '') var date = params.date ? - '?date=' + formatTime(params.date) : ''; - url += base + counter + date; + '?date=' + formatTime(params.date) : '' + url += base + counter + date return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - callback(null, resp.rate || 0); + callback(null, resp.rate || 0) } - }); - }; + }) + } - this.activeAccounts = function (params, callback) { - var url = self.url + '/active_accounts/'; + this.activeAccounts = function(params, callback) { + var url = self.url + '/active_accounts/' var base = params.base.currency + - (params.base.issuer ? '+' + params.base.issuer : ''); + (params.base.issuer ? '+' + params.base.issuer : '') var counter = params.counter.currency + - (params.counter.issuer ? '+' + params.counter.issuer : ''); + (params.counter.issuer ? '+' + params.counter.issuer : '') var period = params.period ? - '&period=' + params.period : ''; + '&period=' + params.period : '' var tx = params.transactions ? - '&include_exchanges=true' : ''; - url += base + '/' + counter + '?' + period + tx; + '&include_exchanges=true' : '' + url += base + '/' + counter + '?' + period + tx return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - resp.accounts.forEach(function(a){ - a.base_volume = Number(a.base_volume); - a.counter_volume = Number(a.counter_volume); - a.exchanges.forEach(function(ex){ - ex.base_amount = Number(ex.base_amount); - ex.counter_amount = Number(ex.counter_amount); - }); - }); - callback(null, resp); + resp.accounts.forEach(function(a) { + a.base_volume = Number(a.base_volume) + a.counter_volume = Number(a.counter_volume) + a.exchanges.forEach(function(ex) { + ex.base_amount = Number(ex.base_amount) + ex.counter_amount = Number(ex.counter_amount) + }) + }) + callback(null, resp) } - }); + }) } - this.getValidators = function (callback) { - var url = self.url + '/network/validator_reports'; + this.getValidators = function(callback) { + var url = self.url + '/network/validator_reports' return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - callback(null, resp.reports); + callback(null, resp.reports) } - }); + }) } - this.getValidator = function (pubkey, callback) { - var url = self.url + '/network/validators/' + pubkey; + this.getValidator = function(pubkey, callback) { + var url = self.url + '/network/validators/' + pubkey return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - callback(null, resp); + callback(null, resp) } - }); + }) } - this.getValidatorReports = function (options, callback) { + this.getValidatorReports = function(options, callback) { var url = self.url + '/network/validators/' + - options.pubkey + '/reports?descending=true'; + options.pubkey + '/reports?descending=true' return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - callback(null, resp.reports); + callback(null, resp.reports) } - }); + }) } this.getMaintenanceStatus = function(callback) { - var url = self.url + '/maintenance/ripplecharts'; - - var xhr = d3.json(url, function(err, resp) { - clearTimeout(timeout); - - if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); - } else { - callback(null, resp); - } - }); - + var url = self.url + '/maintenance/ripplecharts' + var xhr var timeout = setTimeout(function() { - console.log(xhr); - xhr.abort(); + xhr.abort() callback({ status: 500, text: 'Data Response Timeout' - }); - }, 15000); - - return xhr; - }; - - function getMetric (params, callback) { - var url = self.url + '/network/' + params.type + '?'; + }) + }, 15000) - var start = params.start ? - '&start=' + formatTime(params.start) : ''; - var end = params.end ? - '&end=' + formatTime(params.end) : ''; - var interval = params.interval ? - '&interval=' + params.interval : ''; - var currency = params.currency ? - '&exchange_currency=' + params.currency : ''; - var issuer = params.issuer ? - '&exchange_issuer=' + params.issuer : ''; - var limit = '&limit=' + (params.limit || 1000); + xhr = d3.json(url, function(err, resp) { + clearTimeout(timeout) - url += start + end + interval + limit + currency + issuer; - return d3.json(url, function(err, resp) { if (err) { - var e = err.response ? JSON.parse(err.response) : err; - e.status = err.status; - e.text = err.statusText || 'Unable to load data'; - callback(e); - + var e = err.response ? JSON.parse(err.response) : err + e.status = err.status + e.text = err.statusText || 'Unable to load data' + callback(e) } else { - resp.rows.forEach(function(row) { - row.total = Number(row.total); - row.exchange_rate = Number(row.exchange_rate); - - row.components.forEach(function(c){ - c.rate = Number(c.rate); - c.amount = Number(c.amount); - c.converted_amount = Number(c.converted_amount || '0'); - }); - }); - callback(null, resp); + callback(null, resp) } - }); + }) + + return xhr } } diff --git a/src/common/valueSummary.js b/src/common/valueSummary.js index 6b2b300..f82274d 100644 --- a/src/common/valueSummary.js +++ b/src/common/valueSummary.js @@ -1,340 +1,479 @@ -var ValueSummary = function (options) { - - var self = this; - var outer = options.id ? - d3.select("#"+options.id).attr("class","valueSummary") : - d3.select("body").append("div").attr("class","valueSummary"); +/* eslint no-unused-vars: 0 */ +'use strict' + +function ValueSummary(options) { + + var commas = d3.format(',.2f') + var outer = options.id ? + d3.select('#' + options.id).attr('class', 'valueSummary') : + d3.select('body').append('div').attr('class', 'valueSummary') + + var title = outer.append('h5') + var inner = outer.append('div').attr('class', 'inner') + var width = parseInt(outer.style('width'), 10) + var height = parseInt(outer.style('height'), 10) || width + var radius = (Math.min(width, height)) / 2 + var margin = { + top: radius / 10, + bottom: radius / 10, + left: radius / 10, + right: radius / 10 + } - var inner = outer.append("div").attr("class","inner"); - var width = parseInt(outer.style("width"), 10); - var height = parseInt(outer.style("height"), 10) || width; - var radius = (Math.min(width, height)) / 2; - var margin = {top:radius/10, bottom:radius/10, left:radius/10, right:radius/10}; + inner.style({ + width: (radius * 2) + 'px', + height: (radius * 2) + 'px' + }) - inner.style({width:(radius*2)+"px", height:(radius*2)+"px"}); - radius -= margin.top; + radius -= margin.top var chart = inner.append('svg') - .attr("width", radius*2.3) - .attr("height", radius*2.3) - .append("g") - .attr("transform", "translate(" + (radius+margin.left) + "," + (radius+margin.top) + ")"); - - var toggle = outer.append("label").attr("class","xrpToggle"); - var hideXRP = true; - var currencyOrder = ['XAU', 'XAG', 'BTC', 'LTC', 'XRP', 'EUR', 'USD', 'GBP', 'AUD', 'NZD', 'USD', 'CAD', 'CHF', 'JPY', 'CNY']; - - toggle.append("input").attr("type", "checkbox") - .property("checked", !hideXRP) - .on('click', function(){ - hideXRP = !d3.select(this).property("checked"); - self.load(null, exchange, true); - }); - - toggle.append("b"); - toggle.append("span").html("include XRP"); - - //var color = d3.scale.category20(); - var color = function (d) { - var currency = ""; - if (d.data.base) { - currency = d.data.base.currency; - } else if (d.data.currency) { - currency = d.data.currency; - } - - var colors = { - 'XRP' : '#346aa9', - 'USD' : [20,150,30], - 'BTC' : [240,150,50], - 'EUR' : [220,210,50], - 'CNY' : [180,30,35], - 'JPY' : [140,80,170], - 'CAD' : [130,100,190], - 'other' : [100, 150, 200] - }; - var c = colors[currency] || colors.other; - var rank = d.data.rank - 1; - if (typeof c !== 'string') { - c[0] -= Math.floor(c[0] * (rank%3*0.1)); - c[1] -= Math.floor(c[1] * (rank%3*0.15)); - c[2] -= Math.floor(c[2] * (rank%3*0.2)); - - c = 'rgb('+c[0]+','+c[1]+','+c[2]+')'; - } + .attr('width', radius * 2.3) + .attr('height', radius * 2.3) + .append('g') + .attr('transform', 'translate(' + + (radius + margin.left) + ',' + + (radius + margin.top) + ')') + + var currencyOrder = [ + 'XAU', 'XAG', 'BTC', + 'LTC', 'XRP', 'EUR', + 'USD', 'GBP', 'AUD', + 'NZD', 'USD', 'CAD', + 'CHF', 'JPY', 'CNY' + ] + + var sourceLabels = { + rcl: 'Ripple Network', + 'poloniex.com': 'Poloniex', + 'kraken.com': 'Kraken', + 'btc38.com': 'BTC38', + 'jubi.com': 'Jubi' + } - return c; + var currencyColors = { + 'XRP': '#346aa9', + 'USD': [20, 150, 30], + 'BTC': [240, 150, 50], + 'EUR': [220, 210, 50], + 'CNY': [180, 30, 35], + 'JPY': [140, 80, 170], + 'CAD': [130, 100, 190], + 'other': [100, 150, 200] } + var blues = [ + '#2a98D0', + '#98c8eb', + '#1A3964', + '#3665B0' + ] + var arc = d3.svg.arc() - .outerRadius(radius*0.9) - .innerRadius(radius*0.55); + .outerRadius(radius * 0.9) + .innerRadius(radius * 0.6) var labelArc = d3.svg.arc() - .outerRadius(radius*1.15) - .innerRadius(radius); - - //arc paths - var path = chart.selectAll("path"); - var label = inner.selectAll("label"); - var tooltip = outer.append("div").attr("class","tooltip"); - var transitioning = false; - var gateways = options.gateways; - var data = []; - var exchange; - var current; - var total; - - //load a specific metric - this.load = function (z, ex, xrpToggle) { - - if (z && z.components) { - total = z.total || 0; - data = []; - z.components.forEach(function(d){ - if (d.converted_amount) data.push(JSON.parse(JSON.stringify(d))); - }); - } else if (!data) return; - - if (!data.length) { - tooltip.html(""); - path.data([]).exit().remove(); - inner.selectAll("label").data([]).exit().remove(); - return; + .outerRadius(radius * 1.15) + .innerRadius(radius) + + var path = chart.selectAll('path') + var label = inner.selectAll('label') + var tooltip = outer.append('div').attr('class', 'tooltip') + var transitioning = false + var gateways = options.gateways + var exchange + var total + + /** + * color + */ + + function color(currency, rank) { + var c + var r + var rgb + + if (currency && currency === 'XRP') { + return currencyColors.XRP + + } else if (!currency) { + return blues[(rank || 0) % 4] } - //indicate we are in the midst of transition - transitioning = true; - exchange = ex; + c = currencyColors[currency] || + currencyColors.other + r = rank ? rank - 1 : 0 - //check for XRP, set the percentages - var XRPObj, currencies = {}; - data.forEach(function(d) { - if (d.currency=='XRP') XRPObj = d; + rgb = { + r: Math.floor(c[0] - c[0] * (r % 3 * 0.1)), + g: Math.floor(c[1] - c[1] * (r % 3 * 0.15)), + b: Math.floor(c[2] - c[2] * (r % 3 * 0.2)) + } - d.percent = total ? d.converted_amount/total*100 : 0.00; - }); + return 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')' + } - //XRP wont be present for trade volume, so add it at 0 - if (!XRPObj) data.push({currency:'XRP', converted_amount:0.0}); + /** + * prepareTradeVolume + */ + + function prepareTradeVolume(z) { + var data = [] + z.components.forEach(function(d, i) { + data.push({ + key: sourceLabels[d.source] || d.source, + sub: d.base_currency ? + d.base_currency + '/' + d.counter_currency : undefined, + value: Number(d.base_volume), + color: color(null, i), + row: d + }) + }) - //if the XRP toggle is active and XRP should be hidden - //adjust the total, set the XRP amount to 0, and - //recalculate the percentages - else if (xrpToggle && hideXRP) { - var adjusted = total - XRPObj.amount; - data.forEach(function(d){ - if (d.currency=='XRP') d.converted_amount = 0; - d.percent = adjusted ? d.converted_amount/adjusted*100 : 0.00; - }); + return data + } - //otherwise, reset the converted amount and set percentage - } else { - XRPObj.converted_amount = XRPObj.amount || 0.0; - XRPObj.percent = total ? XRPObj.amount/total*100 : 0.00; - } + /** + * prepareRCLTradeVolume + */ - //sort by issuer, reversed - data.sort(function(a, b){ - var i1 = a.base ? a.base.currency+a.base.issuer : a.currency+a.issuer || "Z"; //make XRP first - var i2 = b.base ? b.base.currency+b.base.issuer : b.currency+b.issuer || "Z"; //make XRP first - return i2 ? i2.localeCompare(i1) : 0; - }); + function prepareRCLTradeVolume(z) { + var data = [] + var keys = {} - //rank based on order of apperance. we could - //do by percent, but then the colors would - //change between metrics. - data.forEach(function(d) { - var c = d.currency || d.base.currency; - if (currencies[c]) currencies[c]++; - else currencies[c] = 1; - d.rank = currencies[c]; - }); + z.components.forEach(function(d) { + var row = { + value: Number(d.converted_amount), + row: d + } - var pie = d3.layout.pie() - .sort(null) - .startAngle(1.1*Math.PI) - .endAngle(3.1*Math.PI) - .value(function(d) { return d.converted_amount; }); - - //add arcs - path = path.data(pie(data)); - path.enter().append("path") - .on('mouseover', function(d, i) { - showTooltip(d, i); - if (!transitioning) { - d3.select(this) - .transition() - .attr('transform', 'scale(1.05)'); + var co1 = currencyOrder.indexOf(d.base.currency) + var co2 = currencyOrder.indexOf(d.counter.currency) + + row.sort = d.base.currency + row.sub = gateways.getName(d.base.issuer, d.base.currency) || + d.base.issuer + row.key = co2 < co1 ? + d.counter.currency + '/' + d.base.currency : + d.base.currency + '/' + d.counter.currency + + if (co2 < co1) { + row.link = '#/markets/' + + d.counter.currency + + (d.counter.issuer ? + ':' + d.counter.issuer : '') + '/' + + d.base.currency + + (d.base.issuer ? + ':' + d.base.issuer : '') + } else { + row.link = '#/markets/' + + d.base.currency + + (d.base.issuer ? + ':' + d.base.issuer : '') + '/' + + d.counter.currency + + (d.counter.issuer ? + ':' + d.counter.issuer : '') } + + data.push(row) }) - .on('mouseout', function(){ - path.classed('fade', false); - label.classed('fade', false); - if (!transitioning) { - d3.select(this) - .transition() - .attr('transform', 'scale(1)'); - } + + data.sort(function(a, b) { + var first = a.row.base.currency + var second = b.row.base.currency + return second.localeCompare(first) }) - .on('click', function(d) { - if (d.data.base) { - var co1 = currencyOrder.indexOf(d.data.base.currency); - var co2 = currencyOrder.indexOf(d.data.counter.currency); - var market; - - if (co2 < co1) { - market = d.data.counter.currency + - (d.data.counter.issuer ? ':' + d.data.counter.issuer : '') + '/' + - d.data.base.currency + - (d.data.base.issuer ? ':' + d.data.base.issuer : ''); - } else { - market = d.data.base.currency + - (d.data.base.issuer ? ':' + d.data.base.issuer : '') + '/' + - d.data.counter.currency + - (d.data.counter.issuer ? ':' + d.data.counter.issuer : ''); - } - - window.location.hash = '#/markets/' + market; + + data.forEach(function(d) { + var key = d.row.base.currency + if (keys[key]) { + keys[key]++ + } else { + keys[key] = 1 } - }) - path.classed('pair', function(d) { - return d.data.base ? true : false; + d.rank = keys[key] + d.color = color(key, d.rank) }) - .style("fill", function(d, i) { return color(d); }) - .style("stroke", function(d, i) { return color(d); }) - .style("stroke-width", ".35px") - .transition().duration(750).attrTween("d", arcTween) - .attr("id", function(d, i){return "arc_"+i}) - .each("end", function(){transitioning = false}); - - path.exit().remove(); - - //show data for the first item, - //unless it has no volume - current = null; - var i = 0; - var d = path.data()[i]; - while (d && !d.data.converted_amount) { - i++; - d = path.data()[i]; - } - //add labels - label = label.data(path.data()); - - label.enter().append("label"); - - label.html(function(d){ - if (!d.data.currency && !d.data.base) return ""; - if (!d.data.converted_amount) return ""; - if (d.data.percent<2) return ""; - - var l; - var co1; - var co2; - if (d.data.base) { - co1 = currencyOrder.indexOf(d.data.base.currency); - co2 = currencyOrder.indexOf(d.data.counter.currency); - - if (co1 < co2) { - l = d.data.base.currency+"/"+d.data.counter.currency; - } else { - l = d.data.counter.currency+"/"+d.data.base.currency; - } - } else { - l = d.data.currency; - } - - return l+""+commas(d.data.percent,0)+"%"; - }) - .style("margin-top", function(d){ - return ((0 - parseInt(d3.select(this).style("height"), 10))/2)+"px"; + return data + } + + /** + * prepareRCLPaymentVolume + */ + + function prepareRCLPaymentVolume(z) { + var data = [] + var keys = {} + z.components.forEach(function(d) { + data.push({ + key: d.currency, + sub: gateways.getName(d.issuer, d.currency) || d.issuer, + value: Number(d.converted_amount), + color: color(d.currency), + row: d }) - .style("margin-left", function(d){ - return ((0 - parseInt(d3.select(this).style("width"), 10))/2)+"px"; + }) + + data.sort(function(a, b) { + return b.key.localeCompare(a.key) + }) + + data.forEach(function(d) { + if (keys[d.key]) { + keys[d.key]++ + } else { + keys[d.key] = 1 + } + + d.rank = keys[d.key] + d.color = color(d.key, d.rank) + }) + + return data + } + + /** + * showTooltip + */ + + function showTooltip(d, init) { + + if (!init) { + path.classed('fade', function(row) { + return row !== d }) - .transition().duration(500) - .style("top", function(d){ - return (labelArc.centroid(d)[1]+125)+"px"; + + label.classed('fade', function(row) { + return row !== d }) - .style("left", function(d){ - return (labelArc.centroid(d)[0]+125)+"px"; - }); + } + + var currency = d.data.row.base ? + d.data.row.base.currency : d.data.row.currency || 'XRP' + var amount = d.data.row.amount || d.value + + tooltip.html('') + + var head = tooltip.append('div') + .attr('class', 'title') + .html(d.data.key) + + if (d.data.sub) { + head.append('small') + .html(d.data.sub) + .style('color', d.data.color) + } - label.exit().remove(); + if (d.value) { + tooltip.append('div') + .attr('class', 'value') + .html(' ' + + commas(d.value / exchange.rate, 2) + + ' ' + exchange.currency + '') + } + + if (amount && + currency && + exchange.currency !== currency) { + tooltip.append('div') + .attr('class', 'amount') + .html(' ' + + commas(amount, 2) + + ' ' + currency + '') + } - toggle.style("display", xrpToggle ? "block" : "none"); + if (d.data.row.count) { + tooltip.append('div') + .attr('class', 'count') + .html(' ' + d.data.row.count) + } } + /** + * arcTween + */ - //function for determining arc angles. function arcTween(b) { - var c = this._current; + var c = this._current + if (!c) { + if (chart.select('path:nth-last-child(2)')[0][0]) { + c = chart.select('path:nth-last-child(2)')[0][0]._current + } + + if (c) { + c.startAngle = c.endAngle + } + } + if (!c) { - if (chart.select("path:nth-last-child(2)")[0][0]) - c = chart.select("path:nth-last-child(2)")[0][0]._current; - if (c) c.startAngle = c.endAngle; + c = { + startAngle: 1.1 * Math.PI, + endAngle: 1.1 * Math.PI + } } - if (!c) c = {startAngle: 1.1*Math.PI, endAngle: 1.1*Math.PI}; - var i = d3.interpolate(c, b); - this._current = i(0); + var i = d3.interpolate(c, b) + this._current = i(0) return function(t) { - return arc(i(t)); - }; + return arc(i(t)) + } } + /** + * load + */ - function showTooltip(d, i, init) { + this.load = function(z, ex) { + var data - if (!init) { - path.classed('fade', function(row) { - return row !== d; - }); + title.html(z.label) + total = z.total || 0 - label.classed('fade', function(row) { - return row !== d; - }); + if (z.link) { + title.append('a') + .attr('href', z.link) + .html('See Details >') + } + + if (!z.components) { + return + } + + switch (z.key) { + case 'totalTradeVolume': + data = prepareTradeVolume(z) + break + case 'tradeVolumeRCL': + data = prepareRCLTradeVolume(z) + break + case 'paymentVolumeRCL': + data = prepareRCLPaymentVolume(z) + break + default: + console.log('invalid mode') + return } - if (current===i) return; - current = i; + if (!data.length) { + tooltip.html('') + path.data([]).exit().remove() + inner.selectAll('label').data([]).exit().remove() + return + } - var l; - var co1; - var co2; + // indicate we are in the midst of transition + transitioning = true + exchange = ex - if (d.data.base) { - co1 = currencyOrder.indexOf(d.data.base.currency); - co2 = currencyOrder.indexOf(d.data.counter.currency); + data.forEach(function(d) { + d.percent = total ? d.value / total * 100 : 0.00 + }) - if (co1 < co2) { - l = d.data.base.currency+"/"+d.data.counter.currency; - } else { - l = d.data.counter.currency+"/"+d.data.base.currency; + var pie = d3.layout.pie() + .sort(null) + .startAngle(1.1 * Math.PI) + .endAngle(3.1 * Math.PI) + .value(function(d) { + return d.value + }) + + // add arcs + path = path.data(pie(data)) + path.enter().append('path') + .on('mouseover', function(d) { + showTooltip(d) + if (!transitioning) { + d3.select(this) + .transition() + .attr('transform', 'scale(1.05)') } + }) + .on('mouseout', function() { + path.classed('fade', false) + label.classed('fade', false) + if (!transitioning) { + d3.select(this) + .transition() + .attr('transform', 'scale(1)') + } + }) + .on('click', function(d) { + if (d.data.link) { + window.location.hash = d.data.link + } + }) + + path.classed('clickable', function(d) { + return Boolean(d.data.link) + }) + .style('fill', function(d) { + return d.data.color + }) + .style('stroke', function(d) { + return d.data.color + }) + .style('stroke-width', '.35px') + .transition().duration(750).attrTween('d', arcTween) + .attr('id', function(d, i) { + return 'arc_' + i + }) + .each('end', function() { + transitioning = false + }) + + path.exit() + .transition().duration(400) + .style('opacity', 0) + .each('end', function() { + d3.select(this).remove() + }) + + // add labels + label = label.data(path.data()) + + label.enter().append('label') + + label.html(function(d) { + if (d.data.percent < 2) { + return '' + } + + return d.data.key + + '' + commas(d.data.percent, 0) + '%' + }) + .style('margin-top', function() { + var h = parseInt(d3.select(this).style('height'), 10) + return ((0 - h) / 2) + 'px' + }) + .style('margin-left', function() { + var w = parseInt(d3.select(this).style('width'), 10) + return ((0 - w) / 2) + 'px' + }) + .transition().duration(500) + .style('top', function(d) { + return (labelArc.centroid(d)[1] + radius + margin.top) + 'px' + }) + .style('left', function(d) { + return (labelArc.centroid(d)[0] + radius + margin.left) + 'px' + }) + + label.exit().remove() + + // show data for the largest item + var current + path.data().forEach(function(d) { + if (!current || current.value < d.value) { + current = d + } + }) + + if (current) { + showTooltip(current, true) } else { - l = d.data.currency; + tooltip.html('') } - - var currency = d.data.base ? d.data.base.currency : d.data.currency; - var issuer = d.data.base ? d.data.base.issuer : d.data.issuer; - var gateway = gateways.getName(issuer, currency) || issuer; - var amount = commas(d.data.amount,2); - var value = currency === exchange.currency || !exchange.rate ? "" : commas(d.value/exchange.rate,2); - var count = d.data.count; - - tooltip.html(""); - tooltip.append("div").attr("class","title").html(l+(gateway ? " · "+gateway+"" : "")); - if (value) tooltip.append("div").attr("class","value") - .html(" "+value+" "+exchange.currency+""); - tooltip.append("div").attr("class","amount") - .html(" "+amount+" "+currency+""); - if (count) tooltip.append("div").attr("class","count") - .html(" "+count); - - tooltip.select(".title small").style("color", color(d)); } } diff --git a/src/less/dark.less b/src/less/dark.less index 0dae93e..4c9426c 100644 --- a/src/less/dark.less +++ b/src/less/dark.less @@ -288,7 +288,6 @@ .valueRateDisplay { color:#555; - font-weight:bold; } .valueCurrencySelect { diff --git a/src/less/main.less b/src/less/main.less index 148035e..affcd23 100644 --- a/src/less/main.less +++ b/src/less/main.less @@ -3,13 +3,13 @@ * stylesheets used throughout the application as this is the only stylesheet in * the Grunt configuration that is automatically processed. */ - @font-face { font-family: "Open Sans Light"; src: url('fonts/OpenSans-Light.ttf'); } @font-face { font-family: "Open Sans Light"; font-style:italic; src: url('fonts/OpenSans-LightItalic.ttf'); } -@font-face { font-family: "Open Sans Light"; font-weight: bold; src: url('fonts/OpenSans-Bold.ttf'); } -@font-face { font-family: "Open Sans Light"; font-weight: bold; font-style:italic; src: url('fonts/OpenSans-BoldItalic.ttf'); } +@font-face { font-family: "Open Sans"; font-weight: bold; src: url('fonts/OpenSans-Bold.ttf'); } +@font-face { font-family: "Open Sans"; font-weight: bold; font-style:italic; src: url('fonts/OpenSans-BoldItalic.ttf'); } @font-face { font-family: "Open Sans"; src: url('fonts/OpenSans-Regular.ttf'); } + /** * First, we include the Twitter Bootstrap LESS files. Only the ones used in the * project should be imported as the rest are just wasting space. @@ -41,7 +41,7 @@ @import 'variables.less'; body { - font-family: "Open Sans Light", Helvetica, Arial, sans-serif; + font-family: "Open Sans", Helvetica, Arial, sans-serif; padding:0 !important; line-height:inherit; width:100%; @@ -101,7 +101,7 @@ h1, h2, h3, h4 { //overflow:hidden; background:#000; //border-bottom:5px solid #222; - height: 59px; + height: 50px; } .headerPad { @@ -124,7 +124,7 @@ h1, h2, h3, h4 { } .header status-check svg { - margin: 20px 12px 12px; + margin: 16px 12px 6px; } .header status-check circle { @@ -245,7 +245,7 @@ h1, h2, h3, h4 { } .nav-toggle { - margin:12px 10px 10px 20px; + margin:10px 10px 10px 20px; float:left; cursor:pointer; color:#ccc; @@ -262,7 +262,7 @@ h1, h2, h3, h4 { .nav .top { background:#f6f6f6; - height:60px; + height:47px; border-right:1px solid #fdfdfd; border-bottom:1px solid #e8e8e8; } @@ -296,7 +296,7 @@ h1, h2, h3, h4 { .nav li a { font-size:14px; border-radius:1px; - padding:11px 20px; + padding:7px 16px; color:#666; display:block; text-decoration:none; @@ -352,7 +352,7 @@ h1, h2, h3, h4 { width:175px; height:30px; background-image:url('images/logo.png'); - margin: 15px 0 15px 15px !important; + margin: 10px 0 10px 15px !important; // Pixel ratio: 2 @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { @@ -480,8 +480,6 @@ h1, h2, h3, h4 { background-repeat: no-repeat; background-position: 90% 50%; background-size: 10px; - font-family: 'Open Sans'; - //font-weight:bold; -webkit-appearance:none; -moz-appearance:none!important; -o-appearance:none; @@ -529,7 +527,6 @@ h1, h2, h3, h4 { .helpbox a { color:#999; - font-family:"Open Sans"; text-decoration:underline; } @@ -582,7 +579,6 @@ h1, h2, h3, h4 { .disclaimer { font-size:11px; - font-family:"Open Sans"; margin-bottom:20px; color:#ccc; } @@ -605,7 +601,6 @@ footer .footerInner { text-align:left; clear:both; padding:10px; - font-family:"Open Sans"; } footer .footerInner .disclaimer { @@ -674,9 +669,9 @@ footer .links a:first-child { } -.landing .valueRateDisplay { +.valueRateDisplay { float: right; - font-size: 12px; + font-size: 11px; font-weight: normal; color: #aaa; padding-top: 5px; @@ -756,17 +751,12 @@ footer .links a:first-child { * New header */ -.main { - padding-top: 20px; -} - - .mobile_only { +.mobile_only { display: none; } .navbar { - font-family: 'Open Sans'; - margin-top: 5px; + margin-top: 8px; float: right; @@ -779,7 +769,7 @@ footer .links a:first-child { } .sub_wrapper { - padding-top: 11px; + padding-top: 10px; height: auto; width: auto; position: relative; @@ -834,7 +824,7 @@ footer .links a:first-child { text-align: center; a { - padding: 14px 5px; + padding: 5px; } } } diff --git a/src/less/multimarkets.less b/src/less/multimarkets.less index 332fdc4..85a18a4 100644 --- a/src/less/multimarkets.less +++ b/src/less/multimarkets.less @@ -166,6 +166,7 @@ } .chart .lastPrice { + stroke-width: .25; font-size:24px; stroke:#333; fill:#333; diff --git a/src/less/tradeFeed.less b/src/less/tradeFeed.less index 54a18ba..cccf433 100644 --- a/src/less/tradeFeed.less +++ b/src/less/tradeFeed.less @@ -14,6 +14,7 @@ .price .amount { font-size:40px; + font-family: "Open Sans Light"; padding:0 5px; color:#444; } diff --git a/src/less/valueSummary.less b/src/less/valueSummary.less index af3dd97..cbcf3a5 100644 --- a/src/less/valueSummary.less +++ b/src/less/valueSummary.less @@ -2,40 +2,15 @@ text-align:center; position:relative; - .xrpToggle { - display:none; - position:absolute; - top:10px; - right:10px; - - input { - display: none; - } - - b:before { - content: ""; - display: inline-block; - line-height: 12px; - margin-right:5px; - width: 14px; - height: 14px; - border:2px solid #888; - color: #666; - text-align: center; - font-size: 14px; - content: "\2713"; - color:transparent - } - - input:checked + b:before { - color:#666 - } + h5 { + margin-bottom: 20px; + font-weight: normal; + font-size: 14px; + } - span { - font-size:11px; - float:right; - color:#aaa; - } + h5 a { + font-weight: normal; + margin: 0 5px; } .inner { @@ -59,7 +34,7 @@ opacity: .4; } - path.pair { + path.clickable { cursor: pointer; } } @@ -128,6 +103,11 @@ .title { border-bottom:1px solid #ccc; margin-bottom: 5px; + font-weight: normal; + } + + .title small { + margin: 0 5px; } } }