commit
a68fed7012
16 changed files with 2865 additions and 0 deletions
@ -0,0 +1,4 @@ |
|||||
|
/node_modules/ |
||||
|
chart.png |
||||
|
package-lock.json |
||||
|
image.svg |
||||
@ -0,0 +1,8 @@ |
|||||
|
# Default ignored files |
||||
|
/shelf/ |
||||
|
/workspace.xml |
||||
|
# Editor-based HTTP Client requests |
||||
|
/httpRequests/ |
||||
|
# Datasource local storage ignored files |
||||
|
/dataSources/ |
||||
|
/dataSources.local.xml |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project version="4"> |
||||
|
<component name="JavaScriptLibraryMappings"> |
||||
|
<includedPredefinedLibrary name="Node.js Core" /> |
||||
|
</component> |
||||
|
</project> |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project version="4"> |
||||
|
<component name="ProjectRootManager"> |
||||
|
<output url="file://$PROJECT_DIR$/out" /> |
||||
|
</component> |
||||
|
</project> |
||||
@ -0,0 +1,8 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project version="4"> |
||||
|
<component name="ProjectModuleManager"> |
||||
|
<modules> |
||||
|
<module fileurl="file://$PROJECT_DIR$/ChartJsSSR.iml" filepath="$PROJECT_DIR$/ChartJsSSR.iml" /> |
||||
|
</modules> |
||||
|
</component> |
||||
|
</project> |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project version="4"> |
||||
|
<component name="VcsDirectoryMappings"> |
||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" /> |
||||
|
</component> |
||||
|
</project> |
||||
@ -0,0 +1,8 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<module type="WEB_MODULE" version="4"> |
||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true"> |
||||
|
<exclude-output /> |
||||
|
<content url="file://$MODULE_DIR$" /> |
||||
|
<orderEntry type="sourceFolder" forTests="false" /> |
||||
|
</component> |
||||
|
</module> |
||||
@ -0,0 +1,8 @@ |
|||||
|
FROM node:18.18-bullseye |
||||
|
WORKDIR /app |
||||
|
USER node |
||||
|
|
||||
|
COPY ./ /app |
||||
|
RUN npm install --reg https://nexus.pblr-nyk.pro/repository/npm/ |
||||
|
EXPOSE 3000 |
||||
|
CMD ["nodejs", "chartWebServer.js"] |
||||
@ -0,0 +1,143 @@ |
|||||
|
const { createCanvas, registerFont } = require('canvas'); |
||||
|
const Chart = require('chart.js/auto'); |
||||
|
|
||||
|
const {testData} = require("./trash/testData"); |
||||
|
const {formatSeconds} = require("./utils"); |
||||
|
const moment = require('moment'); |
||||
|
const fs = require("fs"); |
||||
|
|
||||
|
function prepareData(data) { |
||||
|
const result = []; |
||||
|
|
||||
|
const grabFirstValue = (container) => { |
||||
|
if (!container) return 0; |
||||
|
for (const mapName of Object.keys(container)) { |
||||
|
return container[mapName]; |
||||
|
} |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
const grabMapName = (container) => { |
||||
|
if (!container) return ""; |
||||
|
for (const mapName of Object.keys(container)) { |
||||
|
return mapName; |
||||
|
} |
||||
|
return ""; |
||||
|
} |
||||
|
//console.log(data)
|
||||
|
for (const srvId of Object.keys(data.lastplay)) { |
||||
|
const obj = {} |
||||
|
obj["srv_id"] = srvId; |
||||
|
obj["server_name"] = data.servers[srvId].name; |
||||
|
obj["lastplay"] = grabFirstValue(data.lastplay[srvId]) |
||||
|
obj["lastplay_moment"] = moment.unix(obj["lastplay"]).format("DD-MM-YYYY HH:mm") |
||||
|
obj["gametime"] = grabFirstValue(data.gametime[srvId]) |
||||
|
obj["map_name"] = grabMapName(data.lastplay[srvId]) |
||||
|
result.push(obj); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
function generateChartData(d) { |
||||
|
const maxGametime = d.sort((a, b) => a.gametime - b.gametime).at(d.length - 1)["gametime"] |
||||
|
console.log(maxGametime) |
||||
|
|
||||
|
let gametimeDelimiter = 1; |
||||
|
for (const delimiter of [60, 3600, 3600 * 24]) { |
||||
|
if (maxGametime > delimiter) |
||||
|
gametimeDelimiter = delimiter; |
||||
|
} |
||||
|
console.log(gametimeDelimiter) |
||||
|
|
||||
|
const chartData = { |
||||
|
labels: [], |
||||
|
datasets: [] |
||||
|
} |
||||
|
const data = [] |
||||
|
d.sort((a, b) => b.gametime - a.gametime).forEach( |
||||
|
(val) => { |
||||
|
chartData.labels.push(`${val['server_name']}\n${val['lastplay_moment']}\nНаиграно: ${formatSeconds(val['gametime'])}`) |
||||
|
data.push(val['gametime'] / gametimeDelimiter) |
||||
|
} |
||||
|
) |
||||
|
chartData.datasets.push({ |
||||
|
label: "Наиграно времени", |
||||
|
data: data, |
||||
|
}) |
||||
|
|
||||
|
const chartConfig = { |
||||
|
type: 'radar', |
||||
|
data: chartData, |
||||
|
options: { |
||||
|
responsive: false, |
||||
|
maintainAspectRatio: false, |
||||
|
|
||||
|
borderColor: '#bd3b3b', |
||||
|
//backgroundColor: '#FFFFFF',
|
||||
|
|
||||
|
plugins: { |
||||
|
legend: { |
||||
|
position: 'top', |
||||
|
display: false |
||||
|
}, |
||||
|
title: { |
||||
|
display: false, |
||||
|
text: 'Chart.js Polar Area Chart With Centered Point Labels' |
||||
|
} |
||||
|
}, |
||||
|
scales: { |
||||
|
r: { |
||||
|
grid: { |
||||
|
color: '#ef9849' |
||||
|
}, |
||||
|
angleLines: { |
||||
|
color: '#f08149' |
||||
|
}, |
||||
|
pointLabels: { |
||||
|
display: true, |
||||
|
//centerPointLabels: true,
|
||||
|
padding: 44, |
||||
|
font: { |
||||
|
size: 16 |
||||
|
}, |
||||
|
backdropColor: "#395c78", |
||||
|
backdropPadding: "1", |
||||
|
color: "#f5e7de" |
||||
|
}, |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
return chartConfig; |
||||
|
} |
||||
|
|
||||
|
function createChartOnData(testData) { |
||||
|
|
||||
|
// Create a canvas and chart
|
||||
|
const width = 800; |
||||
|
const height = 800; |
||||
|
|
||||
|
const canvas = createCanvas(width, height); |
||||
|
const ctx = canvas.getContext('2d'); |
||||
|
|
||||
|
const d = prepareData(testData); |
||||
|
const chartConfig = generateChartData(d); |
||||
|
|
||||
|
new Chart(ctx, chartConfig); |
||||
|
|
||||
|
return canvas.createPNGStream(); |
||||
|
} |
||||
|
|
||||
|
module.exports = {createChartOnData} |
||||
|
|
||||
|
function test() { |
||||
|
const stream = createChartOnData(testData); |
||||
|
// Save the chart image as a file
|
||||
|
const fs = require('fs'); |
||||
|
const out = fs.createWriteStream('chart.png'); |
||||
|
stream.pipe(out); |
||||
|
out.on('finish', () => console.log('The chart image was saved.')); |
||||
|
} |
||||
|
|
||||
|
|
||||
@ -0,0 +1,38 @@ |
|||||
|
const express = require('express'); |
||||
|
const {createChartOnData} = require("./chartGenerator"); |
||||
|
|
||||
|
const app = express(); |
||||
|
const PORT = process.env.PORT || 3000; |
||||
|
|
||||
|
// Middleware для парсинга JSON тела запроса
|
||||
|
app.use(express.json({ limit: '10mb' })); |
||||
|
//ai sloooop moment
|
||||
|
app.post('/api/chart/:id.png', async (req, res) => { |
||||
|
const { id } = req.params; |
||||
|
const payload = req.body; |
||||
|
|
||||
|
// Проверяем, что тело запроса не пустое
|
||||
|
if (!payload || (typeof payload === 'object' && Object.keys(payload).length === 0)) { |
||||
|
return res.status(400).send('Bad Request: request body is required'); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// Получаем PNG поток из пользовательской функции
|
||||
|
const pngStream = createChartOnData(payload, id); |
||||
|
|
||||
|
// Устанавливаем заголовок ответа
|
||||
|
res.setHeader('Content-Type', 'image/png'); |
||||
|
|
||||
|
// Отправляем поток в ответ
|
||||
|
pngStream.pipe(res); |
||||
|
} catch (err) { |
||||
|
console.error('Error generating PNG:', err); |
||||
|
res.status(500).send('Internal Server Error: could not generate image'); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Запуск сервера
|
||||
|
app.listen(PORT, () => { |
||||
|
console.log(`Server is running on http://localhost:${PORT}`); |
||||
|
console.log(`Endpoint: POST /api/chart/:id.png`); |
||||
|
}); |
||||
@ -0,0 +1,12 @@ |
|||||
|
services: |
||||
|
chartjs_ssr: |
||||
|
build: ./ |
||||
|
container_name: chartjs_ssr |
||||
|
restart: always |
||||
|
ports: |
||||
|
- 3000:3000 |
||||
|
deploy: |
||||
|
resources: |
||||
|
limits: |
||||
|
cpus: 1.0 |
||||
|
memory: 512M |
||||
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"name": "ChartJsSSR", |
||||
|
"version": "1.0.0", |
||||
|
"dependencies": { |
||||
|
"chart.js": "^4.4.8", |
||||
|
"chartjs-node-canvas": "^5.0.0", |
||||
|
"express": "^5.2.1", |
||||
|
"moment": "^2.30.1", |
||||
|
"sharp": "^0.34.5" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
let { Buffer } = require("buffer"); |
||||
|
const {ChartJSNodeCanvas} = require("chartjs-node-canvas"); |
||||
|
|
||||
|
const dependencies = { |
||||
|
chartJS: require("chartjs-node-canvas") |
||||
|
}; |
||||
|
|
||||
|
const constants = { |
||||
|
TYPES: ['PNG', 'SVG'], |
||||
|
smalll: { |
||||
|
height: 400, |
||||
|
width: 400 |
||||
|
}, |
||||
|
medium: { |
||||
|
height: 500, |
||||
|
width: 500 |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
(async () => { |
||||
|
console.log("Creating Chart Object"); |
||||
|
const { CanvasRenderService } = dependencies.chartJS; |
||||
|
const { height, width } = constants.medium; |
||||
|
const renderService = new ChartJSNodeCanvas({ type: 'svg', width: 800, height: 600 }); |
||||
|
/*let renderService = new CanvasRenderService(width, height, (ChartJS) => { |
||||
|
ChartJS.defaults.global.animation = false; |
||||
|
ChartJS.defaults.global.responsive = false; |
||||
|
}, 'SVG'); //Tried SVG and svg*/
|
||||
|
try { |
||||
|
let buffer = await renderService.renderToBufferSync({ |
||||
|
type: 'bar', |
||||
|
data: { |
||||
|
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], |
||||
|
datasets: [{ |
||||
|
label: '# of Votes', |
||||
|
data: [12, 19, 3, 5, 2, 3], |
||||
|
backgroundColor: [ |
||||
|
'rgba(255, 99, 132, 0.2)', |
||||
|
'rgba(54, 162, 235, 0.2)', |
||||
|
'rgba(255, 206, 86, 0.2)', |
||||
|
'rgba(75, 192, 192, 0.2)', |
||||
|
'rgba(153, 102, 255, 0.2)', |
||||
|
'rgba(255, 159, 64, 0.2)' |
||||
|
], |
||||
|
borderColor: [ |
||||
|
'rgba(255, 99, 132, 1)', |
||||
|
'rgba(54, 162, 235, 1)', |
||||
|
'rgba(255, 206, 86, 1)', |
||||
|
'rgba(75, 192, 192, 1)', |
||||
|
'rgba(153, 102, 255, 1)', |
||||
|
'rgba(255, 159, 64, 1)' |
||||
|
], |
||||
|
borderWidth: 1 |
||||
|
}] |
||||
|
}, |
||||
|
options: { |
||||
|
/*scales: { |
||||
|
yAxes: [{ |
||||
|
ticks: { |
||||
|
beginAtZero: true |
||||
|
} |
||||
|
}] |
||||
|
}*/ |
||||
|
} |
||||
|
}, 'image/svg+xml'); |
||||
|
//When I run this I dont get anthing below this to print out
|
||||
|
console.log("Finished"); |
||||
|
console.log(); |
||||
|
const fs = require('node:fs'); |
||||
|
fs.writeFileSync('image.svg', buffer, 'utf8'); |
||||
|
//let base64Str = Buffer.from(buffer).toString("base64");
|
||||
|
//console.log(base64Str);
|
||||
|
} catch (e) { |
||||
|
console.log(e); |
||||
|
} |
||||
|
})(); |
||||
@ -0,0 +1,37 @@ |
|||||
|
const { createCanvas, registerFont } = require('canvas'); |
||||
|
const Chart = require('chart.js/auto'); |
||||
|
|
||||
|
// Create a canvas and chart
|
||||
|
const width = 800; |
||||
|
const height = 400; |
||||
|
|
||||
|
const canvas = createCanvas(width, height); |
||||
|
const ctx = canvas.getContext('2d'); |
||||
|
|
||||
|
const chartConfig = { |
||||
|
type: 'bar', |
||||
|
data: { |
||||
|
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], |
||||
|
datasets: [ |
||||
|
{ |
||||
|
label: 'My First Dataset', |
||||
|
data: [12, 19, 3, 5, 2, 3], |
||||
|
backgroundColor: ['red', 'blue', 'yellow', 'green', 'purple', 'orange'], |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
options: { |
||||
|
responsive: false, |
||||
|
maintainAspectRatio: false, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
new Chart(ctx, chartConfig); |
||||
|
|
||||
|
// Save the chart image as a file
|
||||
|
const fs = require('fs'); |
||||
|
const out = fs.createWriteStream('chart.png'); |
||||
|
const stream = canvas.createPNGStream(); |
||||
|
|
||||
|
stream.pipe(out); |
||||
|
out.on('finish', () => console.log('The chart image was saved.')); |
||||
File diff suppressed because it is too large
@ -0,0 +1,46 @@ |
|||||
|
/** |
||||
|
* Преобразует секунды в формат "дни ЧЧ:ММ:СС" (если дни > 0) |
||||
|
* или просто "ЧЧ:ММ:СС" (если дней нет). |
||||
|
* @param {number} totalSeconds - количество секунд (целое неотрицательное число) |
||||
|
* @returns {string} отформатированная строка |
||||
|
*/ |
||||
|
function formatSeconds(totalSeconds) { |
||||
|
if (totalSeconds < 0) return "0 00:00:00"; |
||||
|
|
||||
|
const days = Math.floor(totalSeconds / 86400); |
||||
|
let remainder = totalSeconds % 86400; |
||||
|
|
||||
|
const hours = Math.floor(remainder / 3600); |
||||
|
remainder %= 3600; |
||||
|
|
||||
|
const minutes = Math.floor(remainder / 60); |
||||
|
const seconds = remainder % 60; |
||||
|
|
||||
|
// Форматируем часы, минуты, секунды с ведущими нулями
|
||||
|
const timePart = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
||||
|
|
||||
|
if (days === 0) { |
||||
|
return timePart; |
||||
|
} |
||||
|
|
||||
|
// Склонение слова "день"
|
||||
|
let dayWord; |
||||
|
const lastDigit = days % 10; |
||||
|
const lastTwoDigits = days % 100; |
||||
|
|
||||
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 14) { |
||||
|
dayWord = "дней"; |
||||
|
} else { |
||||
|
switch (lastDigit) { |
||||
|
case 1: dayWord = "день"; break; |
||||
|
case 2: |
||||
|
case 3: |
||||
|
case 4: dayWord = "дня"; break; |
||||
|
default: dayWord = "дней"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return `${days} ${dayWord} ${timePart}`; |
||||
|
} |
||||
|
|
||||
|
module.exports = {formatSeconds} |
||||
Loading…
Reference in new issue