├── .gitignore
├── readmeGIF.gif
├── src
├── index.css
├── index.js
├── ToolTip.css
├── App.css
├── LineChart.css
├── InfoBox.css
├── ToolTip.js
├── InfoBox.js
├── App.js
├── registerServiceWorker.js
└── LineChart.js
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── readme.md
├── package.json
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/readmeGIF.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmorelli25/interactive-bitcoin-price-chart/HEAD/readmeGIF.gif
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 50px;
3 | padding: 0;
4 | font-family: 'Oxygen', sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmorelli25/interactive-bitcoin-price-chart/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render( , document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/src/ToolTip.css:
--------------------------------------------------------------------------------
1 | .hover {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 |
6 | position: absolute;
7 | padding: 5px;
8 | border-radius: 4px;
9 | background-color: #2196F3;
10 | color: white;
11 | }
12 |
13 | .hover .price {
14 | font-weight: 700;
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .row {
2 | display: flex;
3 | justify-content: center;
4 | }
5 |
6 | h1 {
7 | padding: 0;
8 | margin: 0;
9 | font-weight: 700;
10 | color: #757575;
11 | }
12 |
13 | #coindesk {
14 | font-weight: 300;
15 | }
16 |
17 | #coindesk a {
18 | text-decoration: none;
19 | color: inherit;
20 | }
21 |
22 | .popup {
23 | min-height: 50px;
24 | }
25 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Interactive Bitcoin Price Chart *built with React & SVG*
2 | ---
3 |
4 | * [Live Demo Here](https://interactive-bitcoin-price-chart-yenswahhtb.now.sh)
5 | * [Project Walk Through / Tutorial](https://codeburst.io/how-i-built-an-interactive-30-day-bitcoin-price-graph-with-react-and-an-api-6fe551c2ab1d)
6 | ---
7 |
8 | 
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/LineChart.css:
--------------------------------------------------------------------------------
1 | .linechart {
2 | padding: 8px;
3 | }
4 | .linechart_path {
5 | stroke-width: 3;
6 | fill: none;
7 | }
8 |
9 | .linechart_axis {
10 | stroke: #bdc3c7;
11 | }
12 |
13 | .linechart_point {
14 | fill: #fff;
15 | stroke-width: 2;
16 | }
17 |
18 | .linechart_area {
19 | padding: 8px;
20 | fill: #64B5F6;
21 | stroke: none;
22 | opacity: .4;
23 | }
24 |
25 | .linechart_label {
26 | fill: #64B5F6;
27 | font-weight: 700;
28 | }
29 |
30 | .hoverLine {
31 | stroke: #7D95B6;
32 | stroke-width: 1;
33 | }
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interactive-bitcoin-price-chart",
3 | "version": "1.0.0",
4 | "dependencies": {
5 | "moment": "^2.18.1",
6 | "react": "^15.6.1",
7 | "react-dom": "^15.6.1",
8 | "serve": "^6.0.3"
9 | },
10 | "devDependencies": {
11 | "react-scripts": "1.0.10"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "now-start": "serve build/",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test --env=jsdom",
18 | "eject": "react-scripts eject"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/InfoBox.css:
--------------------------------------------------------------------------------
1 | #data-container {
2 | width: 100%;
3 | display: flex;
4 | justify-content: center;
5 | }
6 |
7 | .box {
8 | width: 250px;
9 |
10 | display: flex;
11 | flex-direction: column;
12 | align-items: center;
13 | margin: 10px 0;
14 | }
15 |
16 | .heading {
17 | font-size: 2.5em;
18 | color: #2196F3;
19 | }
20 |
21 | .subtext {
22 | color: #64B5F6;
23 | }
24 |
25 | #left {
26 | margin-right: 48px;
27 | }
28 |
29 | #middle {
30 | padding: 0 48px;
31 | border-left: 1px solid #DAE1E9;
32 | border-right: 1px solid #DAE1E9;
33 | }
34 |
35 | #right {
36 | margin-left: 48px;
37 | }
38 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | React-SVG-Meter
12 |
13 |
14 |
15 | You need to enable JavaScript to run this app.
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/ToolTip.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './ToolTip.css';
3 |
4 | class ToolTip extends Component {
5 |
6 | render() {
7 | const {hoverLoc, activePoint} = this.props;
8 | const svgLocation = document.getElementsByClassName("linechart")[0].getBoundingClientRect();
9 |
10 | let placementStyles = {};
11 | let width = 100;
12 | placementStyles.width = width + 'px';
13 | placementStyles.left = hoverLoc + svgLocation.left - (width/2);
14 |
15 | return (
16 |
17 |
{ activePoint.d }
18 |
{activePoint.p }
19 |
20 | )
21 | }
22 | }
23 |
24 | export default ToolTip;
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Brandon Morelli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/InfoBox.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import moment from 'moment';
3 | import './InfoBox.css';
4 |
5 | class InfoBox extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | currentPrice: null,
10 | monthChangeD: null,
11 | monthChangeP: null,
12 | updatedAt: null
13 | }
14 | }
15 | componentDidMount(){
16 | this.getData = () => {
17 | const {data} = this.props;
18 | const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';
19 |
20 | fetch(url).then(r => r.json())
21 | .then((bitcoinData) => {
22 | const price = bitcoinData.bpi.USD.rate_float;
23 | const change = price - data[0].y;
24 | const changeP = (price - data[0].y) / data[0].y * 100;
25 |
26 | this.setState({
27 | currentPrice: bitcoinData.bpi.USD.rate_float,
28 | monthChangeD: change.toLocaleString('us-EN',{ style: 'currency', currency: 'USD' }),
29 | monthChangeP: changeP.toFixed(2) + '%',
30 | updatedAt: bitcoinData.time.updated
31 | })
32 | })
33 | .catch((e) => {
34 | console.log(e);
35 | });
36 | }
37 | this.getData();
38 | this.refresh = setInterval(() => this.getData(), 90000);
39 | }
40 | componentWillUnmount(){
41 | clearInterval(this.refresh);
42 | }
43 | render(){
44 | return (
45 |
46 | { this.state.currentPrice ?
47 |
48 |
{this.state.currentPrice.toLocaleString('us-EN',{ style: 'currency', currency: 'USD' })}
49 |
{'Updated ' + moment(this.state.updatedAt ).fromNow()}
50 |
51 | : null}
52 | { this.state.currentPrice ?
53 |
54 |
{this.state.monthChangeD}
55 |
Change Since Last Month (USD)
56 |
57 | : null}
58 |
59 |
{this.state.monthChangeP}
60 |
Change Since Last Month (%)
61 |
62 |
63 |
64 | );
65 | }
66 | }
67 |
68 | export default InfoBox;
69 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import moment from 'moment';
3 | import './App.css';
4 | import LineChart from './LineChart';
5 | import ToolTip from './ToolTip';
6 | import InfoBox from './InfoBox';
7 |
8 | class App extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | fetchingData: true,
13 | data: null,
14 | hoverLoc: null,
15 | activePoint: null
16 | }
17 | }
18 | handleChartHover(hoverLoc, activePoint){
19 | this.setState({
20 | hoverLoc: hoverLoc,
21 | activePoint: activePoint
22 | })
23 | }
24 | componentDidMount(){
25 | const getData = () => {
26 | const url = 'https://api.coindesk.com/v1/bpi/historical/close.json';
27 |
28 | fetch(url).then( r => r.json())
29 | .then((bitcoinData) => {
30 | const sortedData = [];
31 | let count = 0;
32 | for (let date in bitcoinData.bpi){
33 | sortedData.push({
34 | d: moment(date).format('MMM DD'),
35 | p: bitcoinData.bpi[date].toLocaleString('us-EN',{ style: 'currency', currency: 'USD' }),
36 | x: count, //previous days
37 | y: bitcoinData.bpi[date] // numerical price
38 | });
39 | count++;
40 | }
41 | this.setState({
42 | data: sortedData,
43 | fetchingData: false
44 | })
45 | })
46 | .catch((e) => {
47 | console.log(e);
48 | });
49 | }
50 | getData();
51 | }
52 | render() {
53 | return (
54 |
55 |
56 |
57 |
30 Day Bitcoin Price Chart
58 |
59 |
60 | { !this.state.fetchingData ?
61 |
62 | : null }
63 |
64 |
65 |
66 | {this.state.hoverLoc ? : null}
67 |
68 |
69 |
70 |
71 | { !this.state.fetchingData ?
72 | this.handleChartHover(a,b) }/>
73 | : null }
74 |
75 |
76 |
79 |
80 |
81 | );
82 | }
83 | }
84 |
85 | export default App;
86 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/LineChart.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from "react";
2 | import "./LineChart.css";
3 |
4 | class LineChart extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | hoverLoc: null,
9 | activePoint: null
10 | }
11 | }
12 | // GET X & Y || MAX & MIN
13 | getX(){
14 | const {data} = this.props;
15 | return {
16 | min: data[0].x,
17 | max: data[data.length - 1].x
18 | }
19 | }
20 | getY(){
21 | const {data} = this.props;
22 | return {
23 | min: data.reduce((min, p) => p.y < min ? p.y : min, data[0].y),
24 | max: data.reduce((max, p) => p.y > max ? p.y : max, data[0].y)
25 | }
26 | }
27 | // GET SVG COORDINATES
28 | getSvgX(x) {
29 | const {svgWidth, yLabelSize} = this.props;
30 | return yLabelSize + (x / this.getX().max * (svgWidth - yLabelSize));
31 | }
32 | getSvgY(y) {
33 | const {svgHeight, xLabelSize} = this.props;
34 | const gY = this.getY();
35 | return ((svgHeight - xLabelSize) * gY.max - (svgHeight - xLabelSize) * y) / (gY.max - gY.min);
36 | }
37 | // BUILD SVG PATH
38 | makePath() {
39 | const {data, color} = this.props;
40 | let pathD = "M " + this.getSvgX(data[0].x) + " " + this.getSvgY(data[0].y) + " ";
41 |
42 | pathD += data.map((point, i) => {
43 | return "L " + this.getSvgX(point.x) + " " + this.getSvgY(point.y) + " ";
44 | }).join("");
45 |
46 | return (
47 |
48 | );
49 | }
50 | // BUILD SHADED AREA
51 | makeArea() {
52 | const {data} = this.props;
53 | let pathD = "M " + this.getSvgX(data[0].x) + " " + this.getSvgY(data[0].y) + " ";
54 |
55 | pathD += data.map((point, i) => {
56 | return "L " + this.getSvgX(point.x) + " " + this.getSvgY(point.y) + " ";
57 | }).join("");
58 |
59 | const x = this.getX();
60 | const y = this.getY();
61 | pathD += "L " + this.getSvgX(x.max) + " " + this.getSvgY(y.min) + " "
62 | + "L " + this.getSvgX(x.min) + " " + this.getSvgY(y.min) + " ";
63 |
64 | return
65 | }
66 | // BUILD GRID AXIS
67 | makeAxis() {
68 | const {yLabelSize} = this.props;
69 | const x = this.getX();
70 | const y = this.getY();
71 |
72 | return (
73 |
74 |
78 |
82 |
83 | );
84 | }
85 | makeLabels(){
86 | const {svgHeight, svgWidth, xLabelSize, yLabelSize} = this.props;
87 | const padding = 5;
88 | return(
89 |
90 | {/* Y AXIS LABELS */}
91 |
92 | {this.getY().max.toLocaleString('us-EN',{ style: 'currency', currency: 'USD' })}
93 |
94 |
95 | {this.getY().min.toLocaleString('us-EN',{ style: 'currency', currency: 'USD' })}
96 |
97 | {/* X AXIS LABELS */}
98 |
99 | { this.props.data[0].d }
100 |
101 |
102 | { this.props.data[this.props.data.length - 1].d }
103 |
104 |
105 | )
106 | }
107 | // FIND CLOSEST POINT TO MOUSE
108 | getCoords(e){
109 | const {svgWidth, data, yLabelSize} = this.props;
110 | const svgLocation = document.getElementsByClassName("linechart")[0].getBoundingClientRect();
111 | const adjustment = (svgLocation.width - svgWidth) / 2; //takes padding into consideration
112 | const relativeLoc = e.clientX - svgLocation.left - adjustment;
113 |
114 | let svgData = [];
115 | data.map((point, i) => {
116 | svgData.push({
117 | svgX: this.getSvgX(point.x),
118 | svgY: this.getSvgY(point.y),
119 | d: point.d,
120 | p: point.p
121 | });
122 | });
123 |
124 | let closestPoint = {};
125 | for(let i = 0, c = 500; i < svgData.length; i++){
126 | if ( Math.abs(svgData[i].svgX - this.state.hoverLoc) <= c ){
127 | c = Math.abs(svgData[i].svgX - this.state.hoverLoc);
128 | closestPoint = svgData[i];
129 | }
130 | }
131 |
132 | if(relativeLoc - yLabelSize < 0){
133 | this.stopHover();
134 | } else {
135 | this.setState({
136 | hoverLoc: relativeLoc,
137 | activePoint: closestPoint
138 | })
139 | this.props.onChartHover(relativeLoc, closestPoint);
140 | }
141 | }
142 | // STOP HOVER
143 | stopHover(){
144 | this.setState({hoverLoc: null, activePoint: null});
145 | this.props.onChartHover(null, null);
146 | }
147 | // MAKE ACTIVE POINT
148 | makeActivePoint(){
149 | const {color, pointRadius} = this.props;
150 | return (
151 |
158 | );
159 | }
160 | // MAKE HOVER LINE
161 | createLine(){
162 | const {svgHeight, xLabelSize} = this.props;
163 | return (
164 |
167 | )
168 | }
169 |
170 | render() {
171 | const {svgHeight, svgWidth} = this.props;
172 |
173 | return (
174 | this.stopHover() }
176 | onMouseMove={ (e) => this.getCoords(e) } >
177 |
178 | {this.makeAxis()}
179 | {this.makePath()}
180 | {this.makeArea()}
181 | {this.makeLabels()}
182 | {this.state.hoverLoc ? this.createLine() : null}
183 | {this.state.hoverLoc ? this.makeActivePoint() : null}
184 |
185 |
186 | );
187 | }
188 | }
189 | // DEFAULT PROPS
190 | LineChart.defaultProps = {
191 | data: [],
192 | color: '#2196F3',
193 | pointRadius: 5,
194 | svgHeight: 300,
195 | svgWidth: 900,
196 | xLabelSize: 20,
197 | yLabelSize: 80
198 | }
199 |
200 | export default LineChart;
201 |
--------------------------------------------------------------------------------