Skip to content

Commit 91b161f

Browse files
Copilot0xrinegade
andcommitted
Redesign volume chart with ASCII art theme
Co-authored-by: 0xrinegade <[email protected]>
1 parent d1c9afc commit 91b161f

File tree

3 files changed

+305
-162
lines changed

3 files changed

+305
-162
lines changed

src/components/analytics/VolumePerDayChart.js

Lines changed: 163 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,7 @@
1-
import React, { useEffect, useRef } from 'react';
2-
import {
3-
Chart as ChartJS,
4-
CategoryScale,
5-
LinearScale,
6-
PointElement,
7-
LineElement,
8-
Title,
9-
Tooltip,
10-
Legend,
11-
Filler
12-
} from 'chart.js';
13-
import { Line } from 'react-chartjs-2';
14-
15-
// Register Chart.js components
16-
ChartJS.register(
17-
CategoryScale,
18-
LinearScale,
19-
PointElement,
20-
LineElement,
21-
Title,
22-
Tooltip,
23-
Legend,
24-
Filler
25-
);
1+
import React, { useState } from 'react';
262

273
export default function VolumePerDayChart({ data, network, timeframe }) {
28-
const chartRef = useRef();
4+
const [hoveredPoint, setHoveredPoint] = useState(null);
295

306
const formatDate = (dateString) => {
317
const date = new Date(dateString);
@@ -40,137 +16,91 @@ export default function VolumePerDayChart({ data, network, timeframe }) {
4016
}
4117
};
4218

43-
const chartData = {
44-
labels: data.map(point => formatDate(point.time)),
45-
datasets: [
46-
{
47-
label: 'Protocol Volume (SOL)',
48-
data: data.map(point => point.volume),
49-
borderColor: network.color || '#9945FF',
50-
backgroundColor: `${network.color || '#9945FF'}20`,
51-
borderWidth: 2,
52-
fill: true,
53-
tension: 0.4,
54-
pointRadius: 3,
55-
pointHoverRadius: 6,
56-
pointBackgroundColor: network.color || '#9945FF',
57-
pointBorderColor: '#ffffff',
58-
pointBorderWidth: 2
59-
}
60-
]
61-
};
62-
63-
const chartOptions = {
64-
responsive: true,
65-
maintainAspectRatio: false,
66-
plugins: {
67-
legend: {
68-
display: true,
69-
position: 'top',
70-
labels: {
71-
color: '#1f2937',
72-
font: {
73-
size: 12,
74-
family: 'Inter, system-ui, sans-serif'
75-
}
76-
}
77-
},
78-
title: {
79-
display: false
80-
},
81-
tooltip: {
82-
mode: 'index',
83-
intersect: false,
84-
backgroundColor: 'rgba(0, 0, 0, 0.8)',
85-
titleColor: '#ffffff',
86-
bodyColor: '#ffffff',
87-
borderColor: network.color || '#9945FF',
88-
borderWidth: 1,
89-
cornerRadius: 8,
90-
displayColors: false,
91-
callbacks: {
92-
label: function(context) {
93-
const volume = context.parsed.y;
94-
if (volume >= 1000000) {
95-
return `Volume: ${(volume / 1000000).toFixed(2)}M SOL`;
96-
} else if (volume >= 1000) {
97-
return `Volume: ${(volume / 1000).toFixed(2)}K SOL`;
19+
// ASCII Chart Generation Functions
20+
const generateAsciiChart = (data, width = 60, height = 12) => {
21+
if (!data || data.length === 0) return [];
22+
23+
const volumes = data.map(point => point.volume);
24+
const maxVolume = Math.max(...volumes);
25+
const minVolume = Math.min(...volumes);
26+
const range = maxVolume - minVolume || 1;
27+
28+
// Create chart grid
29+
const chart = Array(height).fill().map(() => Array(width).fill(' '));
30+
31+
// Plot data points
32+
for (let i = 0; i < data.length && i < width; i++) {
33+
const volume = volumes[i];
34+
const normalizedHeight = Math.round(((volume - minVolume) / range) * (height - 1));
35+
const y = height - 1 - normalizedHeight;
36+
37+
if (y >= 0 && y < height) {
38+
chart[y][i] = '*';
39+
40+
// Connect points with lines if not first point
41+
if (i > 0) {
42+
const prevVolume = volumes[i - 1];
43+
const prevNormalizedHeight = Math.round(((prevVolume - minVolume) / range) * (height - 1));
44+
const prevY = height - 1 - prevNormalizedHeight;
45+
46+
// Draw connecting line
47+
const startY = Math.min(y, prevY);
48+
const endY = Math.max(y, prevY);
49+
50+
for (let lineY = startY; lineY <= endY; lineY++) {
51+
if (lineY >= 0 && lineY < height && chart[lineY][i] === ' ') {
52+
if (y > prevY) {
53+
chart[lineY][i] = '/';
54+
} else if (y < prevY) {
55+
chart[lineY][i] = '\\';
56+
} else {
57+
chart[lineY][i] = '-';
58+
}
9859
}
99-
return `Volume: ${volume.toFixed(2)} SOL`;
100-
},
101-
afterLabel: function(context) {
102-
const usdValue = (context.parsed.y * 150).toFixed(0); // Mock SOL price
103-
return `≈ $${Number(usdValue).toLocaleString()} USD`;
10460
}
10561
}
10662
}
107-
},
108-
interaction: {
109-
mode: 'nearest',
110-
axis: 'x',
111-
intersect: false
112-
},
113-
scales: {
114-
x: {
115-
display: true,
116-
title: {
117-
display: true,
118-
text: 'Time',
119-
color: '#6b7280',
120-
font: {
121-
size: 12,
122-
family: 'Inter, system-ui, sans-serif'
123-
}
124-
},
125-
grid: {
126-
color: 'rgba(107, 114, 128, 0.1)',
127-
borderColor: 'rgba(107, 114, 128, 0.2)'
128-
},
129-
ticks: {
130-
color: '#6b7280',
131-
font: {
132-
size: 11
133-
},
134-
maxTicksLimit: 8
135-
}
136-
},
137-
y: {
138-
display: true,
139-
title: {
140-
display: true,
141-
text: 'Volume (SOL)',
142-
color: '#6b7280',
143-
font: {
144-
size: 12,
145-
family: 'Inter, system-ui, sans-serif'
146-
}
147-
},
148-
grid: {
149-
color: 'rgba(107, 114, 128, 0.1)',
150-
borderColor: 'rgba(107, 114, 128, 0.2)'
151-
},
152-
ticks: {
153-
color: '#6b7280',
154-
font: {
155-
size: 11
156-
},
157-
callback: function(value) {
158-
if (value >= 1000000) {
159-
return `${(value / 1000000).toFixed(1)}M`;
160-
} else if (value >= 1000) {
161-
return `${(value / 1000).toFixed(1)}K`;
162-
}
163-
return value.toFixed(0);
164-
}
165-
},
166-
beginAtZero: true
63+
}
64+
65+
return chart;
66+
};
67+
68+
const generateYAxisLabels = (data, height = 12) => {
69+
if (!data || data.length === 0) return [];
70+
71+
const volumes = data.map(point => point.volume);
72+
const maxVolume = Math.max(...volumes);
73+
const minVolume = Math.min(...volumes);
74+
75+
const labels = [];
76+
for (let i = 0; i < height; i++) {
77+
const value = minVolume + ((maxVolume - minVolume) / (height - 1)) * (height - 1 - i);
78+
let formattedValue;
79+
if (value >= 1000000) {
80+
formattedValue = `${(value / 1000000).toFixed(1)}M`;
81+
} else if (value >= 1000) {
82+
formattedValue = `${(value / 1000).toFixed(1)}K`;
83+
} else {
84+
formattedValue = value.toFixed(0);
16785
}
168-
},
169-
elements: {
170-
point: {
171-
hoverBackgroundColor: network.color || '#9945FF'
86+
labels.push(formattedValue.padStart(6));
87+
}
88+
return labels;
89+
};
90+
91+
const generateXAxisLabels = (data, width = 60) => {
92+
if (!data || data.length === 0) return [];
93+
94+
const labels = [];
95+
const step = Math.max(1, Math.floor(data.length / 8)); // Show ~8 labels max
96+
97+
for (let i = 0; i < width; i++) {
98+
if (i < data.length && (i % step === 0 || i === data.length - 1)) {
99+
const formatted = formatDate(data[i].time);
100+
labels.push({ position: i, label: formatted });
172101
}
173102
}
103+
return labels;
174104
};
175105

176106
const formatVolume = (volume) => {
@@ -187,6 +117,13 @@ export default function VolumePerDayChart({ data, network, timeframe }) {
187117
const minVolume = data.length > 0 ? Math.min(...data.map(point => point.volume)) : 0;
188118
const maxVolume = data.length > 0 ? Math.max(...data.map(point => point.volume)) : 0;
189119

120+
// Generate ASCII chart data
121+
const chartWidth = 60;
122+
const chartHeight = 12;
123+
const asciiChart = generateAsciiChart(data, chartWidth, chartHeight);
124+
const yAxisLabels = generateYAxisLabels(data, chartHeight);
125+
const xAxisLabels = generateXAxisLabels(data, chartWidth);
126+
190127
return (
191128
<div className="volume-chart">
192129
<div className="chart-header">
@@ -217,13 +154,83 @@ export default function VolumePerDayChart({ data, network, timeframe }) {
217154
</div>
218155
</div>
219156

220-
<div className="chart-container">
157+
<div className="ascii-chart-container">
221158
{data.length > 0 ? (
222-
<Line
223-
ref={chartRef}
224-
data={chartData}
225-
options={chartOptions}
226-
/>
159+
<div className="ascii-chart">
160+
<div className="chart-legend">
161+
<span className="legend-item">[*] Protocol Volume (SOL)</span>
162+
</div>
163+
164+
<div className="ascii-chart-grid">
165+
{asciiChart.map((row, rowIndex) => (
166+
<div key={rowIndex} className="chart-row">
167+
<span className="y-axis-label">
168+
{yAxisLabels[rowIndex]}
169+
</span>
170+
<span className="y-axis-separator">|</span>
171+
<span className="chart-line">
172+
{row.map((char, colIndex) => (
173+
<span
174+
key={colIndex}
175+
className={`chart-char ${char !== ' ' ? 'chart-point' : ''}`}
176+
onMouseEnter={() => {
177+
if (char !== ' ' && colIndex < data.length) {
178+
setHoveredPoint({
179+
index: colIndex,
180+
volume: data[colIndex].volume,
181+
time: data[colIndex].time
182+
});
183+
}
184+
}}
185+
onMouseLeave={() => setHoveredPoint(null)}
186+
>
187+
{char}
188+
</span>
189+
))}
190+
</span>
191+
</div>
192+
))}
193+
194+
{/* X-axis */}
195+
<div className="chart-row x-axis-row">
196+
<span className="y-axis-label"> </span>
197+
<span className="y-axis-separator">|</span>
198+
<span className="chart-line">
199+
{'_'.repeat(chartWidth)}
200+
</span>
201+
</div>
202+
203+
{/* X-axis labels */}
204+
<div className="x-axis-labels">
205+
<span className="y-axis-label"> </span>
206+
<span className="y-axis-separator"> </span>
207+
<div className="x-labels-container">
208+
{xAxisLabels.map((labelData, index) => (
209+
<span
210+
key={index}
211+
className="x-axis-label"
212+
style={{ left: `${(labelData.position / chartWidth) * 100}%` }}
213+
>
214+
{labelData.label}
215+
</span>
216+
))}
217+
</div>
218+
</div>
219+
</div>
220+
221+
{hoveredPoint && (
222+
<div className="ascii-tooltip">
223+
<div className="tooltip-header">
224+
[VOLUME DATA]
225+
</div>
226+
<div className="tooltip-content">
227+
<div>Time: {formatDate(hoveredPoint.time)}</div>
228+
<div>Volume: {formatVolume(hoveredPoint.volume)}</div>
229+
<div>≈ ${(hoveredPoint.volume * 150).toLocaleString()} USD</div>
230+
</div>
231+
</div>
232+
)}
233+
</div>
227234
) : (
228235
<div className="chart-placeholder">
229236
<div className="placeholder-icon">[CHART]</div>

0 commit comments

Comments
 (0)