├── utils
├── formatPrice.js
└── formatDate.js
├── previews
├── 01-bitcoin.png
└── 02-ethereum.png
├── components
├── ethereum
│ ├── index.js
│ ├── chart
│ │ ├── TimeMarker.js
│ │ ├── Volume.js
│ │ ├── HoverMarkers.js
│ │ ├── Details.js
│ │ └── Chart.js
│ ├── footer
│ │ └── Footer.js
│ ├── topcorner
│ │ └── TopCorner.js
│ └── banner
│ │ └── Banner.js
├── background.js
├── maxprice.js
├── minprice.js
├── tooltips.js
├── hoverline.js
├── bitcoinprice.js
└── chart.js
├── README.md
├── package.json
├── LICENSE
├── .gitignore
└── pages
├── bitcoin.js
└── ethereum.js
/utils/formatPrice.js:
--------------------------------------------------------------------------------
1 | import { format } from 'd3-format';
2 | export default format('$,.2f');
3 |
--------------------------------------------------------------------------------
/previews/01-bitcoin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hshoff/viewsource/HEAD/previews/01-bitcoin.png
--------------------------------------------------------------------------------
/previews/02-ethereum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hshoff/viewsource/HEAD/previews/02-ethereum.png
--------------------------------------------------------------------------------
/utils/formatDate.js:
--------------------------------------------------------------------------------
1 | import { timeFormat } from 'd3-time-format';
2 | export default timeFormat('%b %d');
3 |
--------------------------------------------------------------------------------
/components/ethereum/index.js:
--------------------------------------------------------------------------------
1 | export { default as TopCorner } from './topcorner/TopCorner';
2 | export { default as Banner } from './banner/Banner';
3 | export { default as Chart } from './chart/Chart';
4 | export { default as Footer } from './footer/Footer';
5 |
--------------------------------------------------------------------------------
/components/background.js:
--------------------------------------------------------------------------------
1 | import { LinearGradient } from '@vx/gradient';
2 |
3 | export default function Background({ width, height }) {
4 | return (
5 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/components/maxprice.js:
--------------------------------------------------------------------------------
1 | import { LinePath } from '@vx/shape';
2 |
3 | export default ({ data, label, yText, yScale, xScale, x, y }) => {
4 | return (
5 |
6 |
17 |
18 | {label}
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/minprice.js:
--------------------------------------------------------------------------------
1 | import { LinePath } from '@vx/shape';
2 |
3 | export default ({ data, yScale, xScale, yText, label, x, y }) => {
4 | return (
5 |
6 |
17 |
18 | {label}
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/ethereum/chart/TimeMarker.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from '@vx/tooltip';
2 |
3 | export default function TimeMarker({ top, time, formatTime, xScale }) {
4 | return (
5 |
17 |
18 | {formatTime(time)}
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # View Source
2 | a collection of examples by [@hshoff](https://twitter.com/hshoff)
3 |
4 |
5 | ## 1. Bitcoin price chart
6 |
7 | 
8 |
9 | - demo: https://viewsource.now.sh/bitcoin
10 | - youtube: https://www.youtube.com/watch?v=oeE2tuspdHg
11 |
12 | ## 2. Ethereum candlestick chart
13 |
14 | 
15 |
16 | - demo: https://viewsource.now.sh/ethereum
17 | - youtube: coming soon
18 |
19 | ## Setup
20 |
21 | ```bash
22 | git clone https://github.com/hshoff/viewsource.git
23 | cd viewsource
24 | npm install
25 | npm run dev
26 |
27 | # => localhost:3000/bitcoin
28 | ```
29 |
--------------------------------------------------------------------------------
/components/ethereum/chart/Volume.js:
--------------------------------------------------------------------------------
1 | import { Group } from '@vx/group';
2 | import { Bar } from '@vx/shape';
3 |
4 | export default function Volume({ top, scale, xScale, height, data }) {
5 | return (
6 |
7 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/components/tooltips.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from '@vx/tooltip';
2 |
3 | export default ({ yTop, yLeft, yLabel, xTop, xLeft, xLabel }) => {
4 | return (
5 |
6 |
13 | {xLabel}
14 |
15 |
23 | {yLabel}
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/components/ethereum/footer/Footer.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | return (
3 |
4 |
5 | made with vx
6 |
7 |
12 |
17 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/components/hoverline.js:
--------------------------------------------------------------------------------
1 | import { Line } from '@vx/shape';
2 |
3 | export default ({ from, to, tooltipLeft, tooltipTop }) => {
4 | return (
5 |
6 |
14 |
22 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hshoff/funcodetime",
3 | "version": "1.0.0",
4 | "description": "FunTimeCode episodes ",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "next",
8 | "build": "next build",
9 | "start": "next start"
10 | },
11 | "author": "@hshoff",
12 | "license": "MIT",
13 | "dependencies": {
14 | "@vx/axis": "0.0.134",
15 | "@vx/event": "0.0.127",
16 | "@vx/gradient": "0.0.129",
17 | "@vx/grid": "0.0.131",
18 | "@vx/group": "0.0.127",
19 | "@vx/mock-data": "0.0.127",
20 | "@vx/pattern": "0.0.127",
21 | "@vx/responsive": "0.0.127",
22 | "@vx/scale": "0.0.127",
23 | "@vx/shape": "0.0.131",
24 | "@vx/tooltip": "0.0.134",
25 | "d3-array": "^1.2.0",
26 | "d3-format": "^1.2.0",
27 | "d3-time-format": "^2.0.5",
28 | "isomorphic-fetch": "^2.2.1",
29 | "next": "^3.0.1-beta.20",
30 | "react": "^15.6.1",
31 | "react-dom": "^15.6.1"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/components/ethereum/topcorner/TopCorner.js:
--------------------------------------------------------------------------------
1 | export default function Banner({ width, height }) {
2 | return (
3 |
4 |
26 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Harrison Shoff
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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | .next
61 | .DS_Store
62 |
63 |
--------------------------------------------------------------------------------
/components/ethereum/chart/HoverMarkers.js:
--------------------------------------------------------------------------------
1 | import { Bar } from '@vx/shape';
2 |
3 | export default function HoverMarker({
4 | yScale,
5 | xScale,
6 | height,
7 | width,
8 | margin,
9 | time,
10 | yPoint,
11 | formatPrice
12 | }) {
13 | return (
14 |
15 |
22 |
30 |
37 |
43 |
44 | {formatPrice(yScale.invert(yPoint))}
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/pages/bitcoin.js:
--------------------------------------------------------------------------------
1 | import { withScreenSize } from '@vx/responsive';
2 | import Background from '../components/background';
3 | import BitcoinPrice from '../components/bitcoinprice';
4 |
5 | class App extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | data: {}
10 | };
11 | }
12 | componentDidMount() {
13 | fetch('https://api.coindesk.com/v1/bpi/historical/close.json')
14 | .then(res => {
15 | return res.json();
16 | })
17 | .then(json => {
18 | this.setState({
19 | data: json
20 | });
21 | });
22 | }
23 | render() {
24 | const { screenWidth, screenHeight } = this.props;
25 | const { data } = this.state;
26 | return (
27 |
28 |
29 |
30 |
31 |
32 | {data.disclaimer}
33 |
34 |
35 |
59 |
60 | );
61 | }
62 | }
63 |
64 | export default withScreenSize(App);
65 |
--------------------------------------------------------------------------------
/components/ethereum/chart/Details.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from '@vx/tooltip';
2 |
3 | export default function Details({
4 | formatPrice,
5 | formatNumber,
6 | bucket,
7 | xScale,
8 | yScale
9 | }) {
10 | const left = xScale(bucket.closeTime) + xScale.bandwidth() + 5;
11 | const halfway = xScale.range()[1] / 2;
12 |
13 | return (
14 | halfway ? 'translate(-104%)' : ''
21 | }}
22 | top={yScale(bucket.lowPrice)}
23 | left={left}
24 | >
25 |
26 |
27 |
high
28 |
29 | {formatPrice(bucket.highPrice)}
30 |
31 |
32 |
33 |
low
34 |
35 | {formatPrice(bucket.lowPrice)}
36 |
37 |
38 |
39 |
open
40 |
41 | {formatPrice(bucket.openPrice)}
42 |
43 |
44 |
45 |
close
46 |
47 | {formatPrice(bucket.closePrice)}
48 |
49 |
50 |
51 |
volume
52 |
53 | {formatNumber(bucket.volume)}
54 |
55 |
56 |
57 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/components/bitcoinprice.js:
--------------------------------------------------------------------------------
1 | import Chart from './chart';
2 | import formatPrice from '../utils/formatPrice';
3 |
4 | export default function BitcoinPrice({ data = {}, width, height }) {
5 | if (!data.bpi) return loading...
;
6 |
7 | const prices = Object.keys(data.bpi).map(k => ({
8 | time: k,
9 | price: data.bpi[k]
10 | }));
11 |
12 | const currentPrice = prices[prices.length - 1].price;
13 | const firstPrice = prices[0].price;
14 | const diffPrice = currentPrice - firstPrice;
15 | const hasIncreased = diffPrice > 0;
16 |
17 | return (
18 |
19 |
20 |
21 | Bitcoin Price
22 | last 30 days
23 |
24 |
25 |
26 |
27 | {formatPrice(currentPrice)}
28 |
29 |
30 | {hasIncreased ? '+' : '-'}
31 | {formatPrice(diffPrice)}
32 |
33 |
34 |
35 |
36 |
47 |
48 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/components/ethereum/banner/Banner.js:
--------------------------------------------------------------------------------
1 | import { withParentSize } from '@vx/responsive';
2 |
3 | const Logo = withParentSize(function({ parentWidth, parentHeight }) {
4 | const margin = {
5 | top: 5,
6 | bottom: 5,
7 | right: 22,
8 | left: 22
9 | };
10 | const width = parentWidth - margin.left - margin.right;
11 | const wCenter = parentWidth / 2;
12 | const hCenter = parentHeight / 2;
13 | const topCenter = hCenter - hCenter * 0.25;
14 | const bottomCenter = hCenter + hCenter * 0.25;
15 | return (
16 |
50 | );
51 | });
52 |
53 | export default function Banner({
54 | increaseNumItems,
55 | decreaseNumItems,
56 | numItems
57 | }) {
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {numItems}
67 |
68 |
69 |
70 |
120 |
121 | );
122 | }
123 |
--------------------------------------------------------------------------------
/pages/ethereum.js:
--------------------------------------------------------------------------------
1 | import 'isomorphic-fetch';
2 | import { TopCorner, Banner, Chart, Footer } from '../components/ethereum';
3 |
4 | class Ethereum extends React.Component {
5 | static async getInitialProps() {
6 | const res = await fetch(
7 | `https://api.cryptowat.ch/markets/gdax/ethusd/ohlc?period=180`
8 | );
9 | const json = await res.json();
10 | return { data: json.result['180'] };
11 | }
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = { numItems: 180 };
16 | this.increaseNumItems = this.increaseNumItems.bind(this);
17 | this.decreaseNumItems = this.decreaseNumItems.bind(this);
18 | }
19 |
20 | increaseNumItems() {
21 | if (this.state.numItems === 500) return;
22 | this.setState(() => ({ numItems: this.state.numItems + 20 }));
23 | }
24 |
25 | decreaseNumItems() {
26 | if (this.state.numItems === 40) return;
27 | this.setState(() => ({ numItems: this.state.numItems - 20 }));
28 | }
29 |
30 | render() {
31 | const { data } = this.props;
32 |
33 | const unix = d => new Date(d * 1000);
34 |
35 | const buckets = data
36 | .map(b => {
37 | const [
38 | closeTime,
39 | openPrice,
40 | highPrice,
41 | lowPrice,
42 | closePrice,
43 | volume
44 | ] = b;
45 | return {
46 | closeTime: unix(closeTime),
47 | openPrice,
48 | highPrice,
49 | lowPrice,
50 | closePrice,
51 | volume,
52 | hollow: closePrice > openPrice
53 | };
54 | })
55 | .reverse()
56 | .slice(0, this.state.numItems);
57 |
58 | const sortedBuckets = buckets.sort((a, b) => {
59 | return a.closeTime - b.closeTime;
60 | });
61 |
62 | const maxHighPrice = Math.max(
63 | ...buckets.map(b => Math.max(...[b.highPrice, b.openPrice, b.closePrice]))
64 | );
65 | const minLowPrice = Math.min(
66 | ...buckets.map(b => Math.min(...[b.lowPrice, b.openPrice, b.closePrice]))
67 | );
68 | const maxVolume = Math.max(...buckets.map(b => b.volume));
69 |
70 | const start = sortedBuckets[0].closeTime;
71 | const end = sortedBuckets[sortedBuckets.length - 1].closeTime;
72 |
73 | return (
74 |
75 |
76 |
77 |
87 |
88 |
89 |
94 |
95 |
96 |
129 |
130 | );
131 | }
132 | }
133 |
134 | export default Ethereum;
135 |
--------------------------------------------------------------------------------
/components/chart.js:
--------------------------------------------------------------------------------
1 | import { Group } from '@vx/group';
2 | import { AreaClosed, LinePath, Bar } from '@vx/shape';
3 | import { withParentSize } from '@vx/responsive';
4 | import { scaleTime, scaleLinear } from '@vx/scale';
5 | import { LinearGradient } from '@vx/gradient';
6 | import { PatternLines } from '@vx/pattern';
7 | import { AxisBottom } from '@vx/axis';
8 | import { withTooltip } from '@vx/tooltip';
9 | import { localPoint } from '@vx/event';
10 | import { bisector } from 'd3-array';
11 |
12 | import Tooltips from './tooltips';
13 | import HoverLine from './hoverline';
14 | import MaxPrice from './maxprice';
15 | import MinPrice from './minprice';
16 | import formatPrice from '../utils/formatPrice';
17 | import formatDate from '../utils/formatDate';
18 |
19 | class Chart extends React.Component {
20 | constructor(props) {
21 | super(props);
22 | }
23 | render() {
24 | const {
25 | data,
26 | parentWidth = 600,
27 | parentHeight = 400,
28 | margin = {},
29 | tooltipData,
30 | tooltipLeft,
31 | tooltipTop,
32 | showTooltip,
33 | hideTooltip
34 | } = this.props;
35 |
36 | const width = parentWidth - margin.left - margin.right;
37 | const height = parentHeight - margin.top - margin.bottom;
38 |
39 | const x = d => new Date(d.time);
40 | const y = d => d.price;
41 | const bisectDate = bisector(d => x(d)).left;
42 |
43 | const firstPoint = data[0];
44 | const currentPoint = data[data.length - 1];
45 | const minPrice = Math.min(...data.map(y));
46 | const maxPrice = Math.max(...data.map(y));
47 | const firstPrice = y(firstPoint);
48 | const currentPrice = y(currentPoint);
49 | const maxData = [
50 | { time: x(firstPoint), price: maxPrice },
51 | { time: x(currentPoint), price: maxPrice }
52 | ];
53 | const minData = [
54 | { time: x(firstPoint), price: minPrice },
55 | { time: x(currentPoint), price: minPrice }
56 | ];
57 |
58 | const xScale = scaleTime({
59 | range: [0, width],
60 | domain: [x(firstPoint), x(currentPoint)]
61 | });
62 | const yScale = scaleLinear({
63 | range: [height, 0],
64 | domain: [minPrice, maxPrice + 100]
65 | });
66 |
67 | return (
68 |
69 |
190 | {tooltipData &&
191 |
}
199 |
200 | );
201 | }
202 | }
203 |
204 | export default withTooltip(Chart);
205 |
--------------------------------------------------------------------------------
/components/ethereum/chart/Chart.js:
--------------------------------------------------------------------------------
1 | import { Bar } from '@vx/shape';
2 | import { Group } from '@vx/group';
3 | import { localPoint } from '@vx/event';
4 | import { withTooltip, Tooltip } from '@vx/tooltip';
5 | import { LinearGradient } from '@vx/gradient';
6 | import { withParentSize } from '@vx/responsive';
7 | import { GridRows, GridColumns } from '@vx/grid';
8 | import { AxisLeft, AxisBottom, AxisRight } from '@vx/axis';
9 | import { scaleTime, scaleLinear, scaleBand } from '@vx/scale';
10 | import { format } from 'd3-format';
11 | import { timeFormat } from 'd3-time-format';
12 | import Volume from './Volume';
13 | import Details from './Details';
14 | import TimeMarker from './TimeMarker';
15 | import HoverMarkers from './HoverMarkers';
16 |
17 | const formatPrice = format('$,.2f');
18 | const formatNumber = format(',.0f');
19 | const formatTime = timeFormat('%I:%M%p');
20 |
21 | class Chart extends React.Component {
22 | constructor(props) {
23 | super(props);
24 | this.state = {
25 | activeBucket: undefined,
26 | yPoint: undefined
27 | };
28 | }
29 | render() {
30 | const { parentWidth, parentHeight, data } = this.props;
31 | const {
32 | buckets,
33 | start,
34 | end,
35 | maxHighPrice,
36 | minLowPrice,
37 | maxVolume,
38 | showTooltip,
39 | hideTooltip,
40 | tooltipLeft,
41 | tooltipTop,
42 | tooltipData
43 | } = data;
44 |
45 | const { activeBucket, yPoint } = this.state;
46 |
47 | const margin = {
48 | top: 0,
49 | left: 0,
50 | right: 0,
51 | bottom: 80
52 | };
53 |
54 | const width = parentWidth;
55 | const height = parentHeight;
56 |
57 | const xScale = scaleBand({
58 | range: [0, width - 50],
59 | domain: buckets.map(b => b.closeTime),
60 | padding: 0.3
61 | });
62 | const timeScale = scaleTime({
63 | range: [0, width - 50],
64 | domain: [start, end]
65 | });
66 | const yScale = scaleLinear({
67 | range: [height - margin.bottom, 20],
68 | domain: [minLowPrice - 3, maxHighPrice]
69 | });
70 |
71 | const volumeHeight = (height - margin.bottom) * 0.25;
72 | const yVolumeScale = scaleLinear({
73 | range: [volumeHeight, 0],
74 | domain: [0, maxVolume]
75 | });
76 |
77 | return (
78 |
79 |
222 | {activeBucket &&
223 |
224 |
230 |
237 |
}
238 |
239 | );
240 | }
241 | }
242 |
243 | export default withParentSize(withTooltip(Chart));
244 |
--------------------------------------------------------------------------------