Skip to content

Commit

Permalink
chart png can be created regardless the amount of points, added new e…
Browse files Browse the repository at this point in the history
…ndpoint that returns html document with the chart
  • Loading branch information
GuticaStefan committed Sep 6, 2024
1 parent 3def0cc commit 37e1b0f
Show file tree
Hide file tree
Showing 5 changed files with 686 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ export class DuneSimulatorController {
}
}

@Get("/generate/chart/:token_pair")
async generateChart(
@Get("/generate/chart/:token_pair/png")
async generateChartPng(
@Param('token_pair') pair: string,
@Res() res: Response,
): Promise<any> {
try {
const imageBuffer = await this.duneSimulatorService.generateChart(pair);
const imageBuffer = await this.duneSimulatorService.generateChartPng(pair);

res.setHeader('Content-Type', 'image/png');
res.send(imageBuffer);
Expand All @@ -57,4 +57,21 @@ export class DuneSimulatorController {
}

}

@Get("/generate/chart/:token_pair/html")
async generateChartHtml(
@Param('token_pair') pair: string,
@Res() res: Response,
): Promise<any> {
try {
const htmlContent = await this.duneSimulatorService.generateChartHtml(pair);

res.setHeader('Content-Type', 'text/html');
res.send(htmlContent);

} catch (error) {
throw error;
}

}
}
51 changes: 51 additions & 0 deletions libs/services/src/dune-simulator/chart-template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{chartTitle}}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>

<body>
<h2>{{chartTitle}}</h2>
<canvas id="myChart" width="800" height="300"></canvas>
<script>
const ctx = document.getElementById('myChart').getContext('2d');
const myChart = new Chart(ctx, {
type: 'line',
data: {
labels: '{{labels}}',
datasets: [{
label: 'volumeusd',
data: '{{data}}',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1,
fill: true,
pointRadius: 0.5
}]
},
options: {
scales: {
x: {
title: {
display: true,
text: '{{xTitle}}'
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: '{{yTitle}}'
}
}
}
}
});
</script>
</body>

</html>
177 changes: 82 additions & 95 deletions libs/services/src/dune-simulator/dune-simulator.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { CsvFileRepository } from "@libs/database/repositories";
import { CreateTableBody } from "apps/dune-simulator/src/endpoints/dune-simulator/entities";
import { OriginLogger } from "@multiversx/sdk-nestjs-common";

import ChartJSImage from 'chartjs-to-image';
import path from "path";
import fs from "fs";
@Injectable()
export class DuneSimulatorService {
private readonly logger = new OriginLogger(DuneSimulatorService.name);
Expand Down Expand Up @@ -74,138 +76,123 @@ export class DuneSimulatorService {
return { 'rows_written': rowsWritten, 'bytes_written': bytesWritten };
}

async generateChart(tableName: string) {
// Example data to create the chart
const ChartJSImage = require('chartjs-to-image')
let csvFile;
async generateChartPng(tableName: string) {
try {
csvFile = await this.csvFileRepository.getDocumentByTableName(tableName);

const [points, xTitle, yTitle] = await this.getCsvDataFromDB(tableName);
const chart = this.createChart(points, xTitle, yTitle, tableName, 800, 600);
const buffer = await chart.toBinary();
return buffer;

} catch (error) {
throw error;
}
}

const records = csvFile.records || [];
const headers = csvFile.headers ? csvFile.headers.split(',', 2) : [];
async generateChartHtml(tableName: string) {
try {
const [points, xTitle, yTitle] = await this.getCsvDataFromDB(tableName);

// Process the records
const timestamps = [];
const volumes: number[] = [];
const templatePath = path.join(process.cwd(), 'libs/services/src/dune-simulator', 'chart-template.html');
const htmlTemplate = fs.readFileSync(templatePath, 'utf8');

for (const record of records) {
try {
const [timestamp, volumeusd] = record.split(',', 2);
const value = parseFloat(volumeusd);
if (!isNaN(value)) {
if (volumes.length === 0 || volumes[volumes.length - 1] !== value) {


timestamps.push(timestamp.slice(0, 10));
volumes.push(value);
}
}
} catch (error) {
console.error(`Skipping invalid record: ${record}`);
}
}
const updatedHtml = htmlTemplate
.replace(/{{chartTitle}}/g, tableName)
.replace(/'{{labels}}'/g, JSON.stringify(points.map(point => new Date(point[0]).toISOString())))
.replace(/'{{data}}'/g, JSON.stringify(points.map(point => point[1])))
.replace(/{{xTitle}}/g, xTitle)
.replace(/{{yTitle}}/g, yTitle);

if (timestamps.length === 0 || volumes.length === 0) {
throw new HttpException('No valid data found for chart', HttpStatus.BAD_REQUEST);
return updatedHtml;
} catch (error) {
throw error;
}
}


createChart(points: number[][],
xTitle: string,
yTitle: string,
chartName: string,
width: number,
height: number
): ChartJSImage {
const downsampler = require('downsample-lttb');

const downsampledData: number[][] = points.length > 2500 ? downsampler.processData(points, 2500) : points;

// Generate the chart
// const chart = await new ChartJSImage()
// .chart({
// type: 'line',
// data: {
// labels: timestamps,
// datasets: [{
// label: tableName,
// borderColor: 'rgb(75, 192, 192)',
// backgroundColor: 'rgba(75, 192, 192, 0.2)',
// data: volumes,
// fill: true,
// pointRadius: 0,
// }]
// },
// options: {
// title: {
// display: true,
// text: `${tableName} Area Chart`
// },
// scales: {
// xAxes: [{
// scaleLabel: {
// display: true,
// labelString: headers[0] || 'X-axis'
// },
// ticks: {
// autoSkip: true,
// maxRotation: 0, // To ensure labels are readable
// minRotation: 0
// }
// }],
// yAxes: [{
// scaleLabel: {
// display: true,
// labelString: headers[1] || 'Y-axes'
// },
// ticks: {
// autoSkip: true
// }
// }]
// }
// }
// })
// .backgroundColor('white')
// .width(800)
// .height(500)
// .toBuffer();
const chart = new ChartJSImage();
chart.setConfig({
type: 'line',
data: {
labels: timestamps,
datasets: [{
label: tableName,
label: chartName,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
data: volumes,
data: downsampledData.map(item => { return { x: item[0], y: item[1] }; }),
fill: true,
pointRadius: 0,
}]
}],
},
options: {
title: {
display: true,
text: `${tableName} Area Chart`
text: `${chartName} Area Chart`,
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'month',
displayFormats: {
month: 'MMM yyyy',
},
tooltipFormat: 'MMM yyyy',
},
scaleLabel: {
display: true,
labelString: headers[0] || 'X-axis'
labelString: xTitle,
},
ticks: {
autoSkip: true,
maxRotation: 0, // To ensure labels are readable
minRotation: 0
}
maxRotation: 0,
minRotation: 0,
},

}],
yAxes: [{
scaleLabel: {
display: true,
labelString: headers[1] || 'Y-axes'
labelString: yTitle,
},
ticks: {
autoSkip: true
}
}]
}
}
beginAtZero: true,
},
}],
},
},
});
chart.setWidth(1200).setHeight(600);
chart.setWidth(width).setHeight(height);

return chart;
}

async getCsvDataFromDB(tableName: string): Promise<[number[][], string, string]> {
const csvFile = await this.csvFileRepository.getDocumentByTableName(tableName);

const records = csvFile.records || [];
const headers = csvFile.headers ? csvFile.headers.split(',', 2) : [];

const points = [];

for (const record of records) {
const [timestamp, volumeusd] = record.split(',', 2);
const value = parseFloat(volumeusd);
if (!isNaN(value)) {
points.push([Date.parse(timestamp), value]);
}
}

const buff = await chart.toBinary();
return buff;
return [points, headers[0], headers[1]];
}
}
Loading

0 comments on commit 37e1b0f

Please sign in to comment.