Skip to content

Commit 4085d6c

Browse files
authored
docs: Add asset charts to coin detail page (#3009)
1 parent d9eda63 commit 4085d6c

File tree

7 files changed

+158
-16
lines changed

7 files changed

+158
-16
lines changed

examples/coin-app/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const home = css`
1616
margin: -${margin};
1717
display: flex;
1818
align-items: center;
19-
justify-content: center;
2019
position: relative;
2120
flex-direction: column;
2221
text-align: center;
@@ -31,6 +30,7 @@ const home = css`
3130
3231
> main {
3332
margin-top: 44px;
33+
text-align: left;
3434
}
3535
`;
3636

examples/coin-app/src/pages/AssetDetail/AssetChart.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import { useDLE } from '@data-client/react';
2-
import { useMemo } from 'react';
1+
import { useDLE, useSubscription } from '@data-client/react';
32
import { getCandles } from 'resources/Candles';
43

5-
import { formatPrice } from '../../components/formatPrice';
4+
import LineChart from './LineChart';
65

76
export default function AssetChart({ product_id }: Props) {
87
const { data: candles, loading } = useDLE(getCandles, { product_id });
8+
useSubscription(getCandles, { product_id });
9+
const width = 600;
10+
const height = 400;
11+
912
// Don't block page from loading
1013
// TODO: put correct height item here
11-
if (loading || !candles) return <span>&nbsp;</span>;
14+
if (loading || !candles) return <div style={{ width, height }}>&nbsp;</div>;
1215

13-
return <span>&nbsp;</span>;
16+
return <LineChart data={candles} width={width} height={height} />;
1417
}
1518

1619
interface Props {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { scaleLinear, scaleTime, line, extent, max, min } from 'd3';
2+
import { memo, useMemo } from 'react';
3+
4+
import { formatPrice } from '../../components/formatPrice';
5+
6+
const TICK_LENGTH = 5;
7+
const AXIS_HEIGHT = 20;
8+
9+
function LineChart({ data, width, height }: Props) {
10+
const graphDetails = {
11+
xScale: scaleTime().range([0, width]),
12+
yScale: scaleLinear().range([height - AXIS_HEIGHT, 0]),
13+
lineGenerator: line<{ timestamp: number; price_open: number }>(),
14+
};
15+
const xRange: [number, number] = extent(
16+
data,
17+
candle => new Date(1000 * candle.timestamp),
18+
) as any;
19+
const yExtent = [
20+
min(data, candle => candle.price_open),
21+
max(data, candle => candle.price_open),
22+
] as [number, number];
23+
graphDetails.xScale.domain(xRange);
24+
graphDetails.yScale.domain(yExtent);
25+
graphDetails.lineGenerator.x((d, i) =>
26+
graphDetails.xScale(new Date(1000 * d.timestamp)),
27+
);
28+
graphDetails.lineGenerator.y(d => graphDetails.yScale(d.price_open));
29+
const path = graphDetails.lineGenerator(data);
30+
31+
const ticks = useMemo(() => {
32+
const pixelsPerTick = 5000;
33+
const width = xRange[1] - xRange[0];
34+
const numberOfTicksTarget = 5;
35+
36+
return graphDetails.xScale.ticks(numberOfTicksTarget).map(value => ({
37+
value,
38+
xOffset: graphDetails.xScale(value),
39+
}));
40+
}, [graphDetails.xScale]);
41+
42+
if (!path) return <div>Failed to generate path</div>;
43+
44+
return (
45+
<svg
46+
viewBox={`0 0 ${width} ${height}`}
47+
width="100%"
48+
height={height}
49+
preserveAspectRatio="none"
50+
>
51+
<path d={path} fill="none" stroke="currentColor" strokeWidth="2" />
52+
{/* <text
53+
stroke="#fff"
54+
style={{
55+
fontSize: '10px',
56+
textAnchor: 'middle',
57+
transform: 'translate(30px,10px)',
58+
}}
59+
>
60+
{formatPrice.format(yExtent[1])}
61+
</text>
62+
<text
63+
stroke="#fff"
64+
style={{
65+
fontSize: '10px',
66+
textAnchor: 'middle',
67+
transform: `translate(30px,${height - AXIS_HEIGHT}px)`,
68+
}}
69+
>
70+
{formatPrice.format(yExtent[0])}
71+
</text> */}
72+
{ticks.map(({ value, xOffset }, i) => (
73+
<g key={i} transform={`translate(${xOffset}, ${height - AXIS_HEIGHT})`}>
74+
<line y2={TICK_LENGTH} stroke="currentColor" />
75+
<text
76+
stroke="currentColor"
77+
style={{
78+
fontSize: '10px',
79+
textAnchor: 'middle',
80+
transform: 'translateY(20px)',
81+
}}
82+
>
83+
{formatter.format(value)}
84+
</text>
85+
</g>
86+
))}
87+
</svg>
88+
);
89+
}
90+
91+
LineChart.defaultProps = {
92+
width: 500,
93+
height: 400,
94+
};
95+
96+
interface Props {
97+
data: { timestamp: number; price_open: number }[];
98+
width: number;
99+
height: number;
100+
}
101+
102+
const formatter = new Intl.DateTimeFormat('en-US', {
103+
timeStyle: 'medium',
104+
});
105+
106+
export default memo(LineChart);

examples/coin-app/src/pages/AssetDetail/Stats.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ import { formatPrice, formatLargePrice } from '../../components/formatPrice';
66
export default function Stats({ id }: { id: string }) {
77
const stats = useSuspense(StatsResource.get, { id });
88
return (
9-
<p>
10-
high: {formatPrice.format(stats.high)}
11-
<br />
12-
low: {formatPrice.format(stats.low)}
13-
<br />
14-
volume: {formatLargePrice.format(stats.volume_usd)}
15-
</p>
9+
<table>
10+
<tr>
11+
<th align="right">high</th>
12+
<td>{formatPrice.format(stats.high)}</td>
13+
</tr>
14+
<tr>
15+
<th align="right">low</th>
16+
<td>{formatPrice.format(stats.low)}</td>
17+
</tr>
18+
<tr>
19+
<th align="right">volume</th>
20+
<td>{formatLargePrice.format(stats.volume_usd)}</td>
21+
</tr>
22+
</table>
1623
);
1724
}
Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useSuspense } from '@data-client/react';
2+
import { styled } from '@linaria/react';
23
import { CurrencyResource } from 'resources/Currency';
34

45
import AssetChart from './AssetChart';
@@ -9,10 +10,24 @@ export default function AssetDetail({ id }: { id: string }) {
910
const currency = useSuspense(CurrencyResource.get, { id });
1011
return (
1112
<>
12-
<h1>{currency.name}</h1>
13-
<AssetPrice product_id={`${currency.id}-USD`} />
14-
<Stats id={`${currency.id}-USD`} />
13+
<header>
14+
<h1>{currency.name}</h1>
15+
<h2>
16+
<AssetPrice product_id={`${currency.id}-USD`} />
17+
</h2>
18+
</header>
1519
<AssetChart product_id={`${currency.id}-USD`} />
20+
<StatSection>
21+
<Stats id={`${currency.id}-USD`} />
22+
</StatSection>
1623
</>
1724
);
1825
}
26+
27+
const StatSection = styled.section`
28+
display: flex;
29+
align-items: center;
30+
justify-content: center;
31+
width: 100%;
32+
margin-top: 20px;
33+
`;

examples/coin-app/src/resources/Candles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const getCandles = new RestEndpoint({
1515
price_open: candle[3],
1616
}));
1717
},
18+
pollFrequency: 60 * 1000,
1819
});
1920

2021
type CandleTuple = [

examples/coin-app/src/style/main.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ a {
2020
a:hover {
2121
color: gray;
2222
}
23+
24+
h1 {
25+
margin-block-end: .4em;
26+
}
27+
h2 {
28+
margin-block-start: .4em;
29+
}
30+
h1 + h2 {
31+
margin-block-start: 0;
32+
}

0 commit comments

Comments
 (0)