mirror of
https://github.com/syuilo/ai.git
synced 2024-11-23 21:47:58 +00:00
chart
This commit is contained in:
parent
3d7e72f1ba
commit
2c74b27097
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ config.json
|
|||
built
|
||||
node_modules
|
||||
memory.json
|
||||
font.ttf
|
||||
|
|
|
@ -21,6 +21,9 @@ Misskey用の日本語Botです。
|
|||
```
|
||||
`npm install` して `npm run build` して `npm start` すれば起動できます
|
||||
|
||||
## フォント
|
||||
一部の機能にはフォントが必要です。藍にはフォントは同梱されていないので、ご自身でフォントをインストールディレクトリに`font.ttf`という名前で設置してください。
|
||||
|
||||
## 記憶
|
||||
藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import ServerModule from './modules/server';
|
|||
import FollowModule from './modules/follow';
|
||||
import ValentineModule from './modules/valentine';
|
||||
import MazeModule from './modules/maze';
|
||||
import ChartModule from './modules/chart';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import * as request from 'request-promise-native';
|
||||
|
@ -71,6 +72,7 @@ promiseRetry(retry => {
|
|||
new ValentineModule(),
|
||||
new KeywordModule(),
|
||||
new MazeModule(),
|
||||
new ChartModule(),
|
||||
]);
|
||||
}).catch(e => {
|
||||
log(chalk.red('Failed to fetch the account'));
|
||||
|
|
107
src/modules/chart/index.ts
Normal file
107
src/modules/chart/index.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import autobind from 'autobind-decorator';
|
||||
import Module from '../../module';
|
||||
import serifs from '../../serifs';
|
||||
import Message from '../../message';
|
||||
import { renderChart } from './render-chart';
|
||||
|
||||
export default class extends Module {
|
||||
public readonly name = 'chart';
|
||||
|
||||
@autobind
|
||||
public install() {
|
||||
this.post();
|
||||
setInterval(this.post, 1000 * 60 * 3);
|
||||
|
||||
return {
|
||||
mentionHook: this.mentionHook
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
private async post() {
|
||||
const now = new Date();
|
||||
if (now.getHours() !== 23) return;
|
||||
const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
|
||||
const data = this.getData();
|
||||
if (data.lastPosted == date) return;
|
||||
data.lastPosted = date;
|
||||
this.setData(data);
|
||||
|
||||
this.log('Time to chart');
|
||||
const file = await this.genChart('notes');
|
||||
|
||||
this.log('Posting...');
|
||||
this.ai.post({
|
||||
text: serifs.chart.post,
|
||||
fileIds: [file.id]
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private async genChart(type, params?): Promise<any> {
|
||||
this.log('Chart data fetching...');
|
||||
|
||||
let chart;
|
||||
|
||||
if (type === 'userNotes') {
|
||||
const data = await this.ai.api('charts/user/notes', {
|
||||
span: 'day',
|
||||
limit: 30,
|
||||
userId: params.userId
|
||||
});
|
||||
|
||||
chart = {
|
||||
datasets: [{
|
||||
data: data.diffs.normal
|
||||
}, {
|
||||
data: data.diffs.reply
|
||||
}, {
|
||||
data: data.diffs.renote
|
||||
}]
|
||||
};
|
||||
} else if (type === 'notes') {
|
||||
const data = await this.ai.api('charts/notes', {
|
||||
span: 'day',
|
||||
limit: 30,
|
||||
});
|
||||
|
||||
chart = {
|
||||
datasets: [{
|
||||
data: data.local.diffs.normal
|
||||
}, {
|
||||
data: data.local.diffs.reply
|
||||
}, {
|
||||
data: data.local.diffs.renote
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
this.log('Chart rendering...');
|
||||
const img = renderChart(chart);
|
||||
|
||||
this.log('Image uploading...');
|
||||
const file = await this.ai.upload(img, {
|
||||
filename: 'chart.png',
|
||||
contentType: 'image/png'
|
||||
});
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
@autobind
|
||||
private async mentionHook(msg: Message) {
|
||||
if (msg.includes(['チャート'])) {
|
||||
this.log('Chart requested');
|
||||
const file = await this.genChart('userNotes', {
|
||||
userId: msg.userId
|
||||
});
|
||||
this.log('Replying...');
|
||||
msg.replyWithFile(serifs.chart.foryou, file);
|
||||
return {
|
||||
reaction: 'like'
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
201
src/modules/chart/render-chart.ts
Normal file
201
src/modules/chart/render-chart.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
import { createCanvas, registerFont } from 'canvas';
|
||||
|
||||
const width = 1024 + 256;
|
||||
const height = 512 + 256;
|
||||
const margin = 128;
|
||||
|
||||
const chartAreaX = margin;
|
||||
const chartAreaY = margin;
|
||||
const chartAreaWidth = width - (margin * 2);
|
||||
const chartAreaHeight = height - (margin * 2);
|
||||
|
||||
const lineWidth = 16;
|
||||
const yAxisThickness = 2;
|
||||
|
||||
const colors = {
|
||||
bg: '#434343',
|
||||
text: '#e0e4cc',
|
||||
yAxis: '#5a5a5a',
|
||||
dataset: [
|
||||
'#ff4e50',
|
||||
'#c2f725',
|
||||
'#69d2e7',
|
||||
'#f38630',
|
||||
'#f9d423',
|
||||
]
|
||||
};
|
||||
|
||||
const yAxisTicks = 4;
|
||||
|
||||
type Chart = {
|
||||
title?: string;
|
||||
datasets: {
|
||||
title?: string;
|
||||
data: number[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export function renderChart(chart: Chart) {
|
||||
registerFont('./font.ttf', { family: 'CustomFont' });
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.antialias = 'default';
|
||||
|
||||
ctx.fillStyle = colors.bg;
|
||||
ctx.beginPath();
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
const xAxisCount = chart.datasets[0].data.length;
|
||||
const serieses = chart.datasets.length;
|
||||
|
||||
let lowerBound = Infinity;
|
||||
let upperBound = -Infinity;
|
||||
|
||||
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
|
||||
let v = 0;
|
||||
for (let series = 0; series < serieses; series++) {
|
||||
v += chart.datasets[series].data[xAxis];
|
||||
}
|
||||
if (v > upperBound) upperBound = v;
|
||||
if (v < lowerBound) lowerBound = v;
|
||||
}
|
||||
|
||||
// Calculate Y axis scale
|
||||
const yAxisSteps = niceScale(lowerBound, upperBound, yAxisTicks);
|
||||
const yAxisStepsMin = yAxisSteps[0];
|
||||
const yAxisStepsMax = yAxisSteps[yAxisSteps.length - 1];
|
||||
const yAxisRange = yAxisStepsMax - yAxisStepsMin;
|
||||
|
||||
// Draw Y axis
|
||||
ctx.lineWidth = yAxisThickness;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = colors.yAxis;
|
||||
for (let i = 0; i < yAxisSteps.length; i++) {
|
||||
const step = yAxisSteps[yAxisSteps.length - i - 1];
|
||||
const y = i * (chartAreaHeight / (yAxisSteps.length - 1));
|
||||
ctx.beginPath();
|
||||
ctx.lineTo(chartAreaX, chartAreaY + y);
|
||||
ctx.lineTo(chartAreaX + chartAreaWidth, chartAreaY + y);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.font = '20px CustomFont';
|
||||
ctx.fillStyle = colors.text;
|
||||
ctx.fillText(step.toString(), chartAreaX, chartAreaY + y - 8);
|
||||
}
|
||||
|
||||
const newDatasets = [];
|
||||
|
||||
for (let series = 0; series < serieses; series++) {
|
||||
newDatasets.push({
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
|
||||
for (let series = 0; series < serieses; series++) {
|
||||
newDatasets[series].data.push(chart.datasets[series].data[xAxis] / yAxisRange);
|
||||
}
|
||||
}
|
||||
|
||||
const perXAxisWidth = chartAreaWidth / xAxisCount;
|
||||
|
||||
let newUpperBound = -Infinity;
|
||||
|
||||
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
|
||||
let v = 0;
|
||||
for (let series = 0; series < serieses; series++) {
|
||||
v += newDatasets[series].data[xAxis];
|
||||
}
|
||||
if (v > newUpperBound) newUpperBound = v;
|
||||
}
|
||||
|
||||
// Draw X axis
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
|
||||
const xAxisPerTypeHeights = [];
|
||||
|
||||
for (let series = 0; series < serieses; series++) {
|
||||
const v = newDatasets[series].data[xAxis];
|
||||
const vHeight = (v / newUpperBound) * (chartAreaHeight - ((yAxisStepsMax - upperBound) / yAxisStepsMax * chartAreaHeight));
|
||||
xAxisPerTypeHeights.push(vHeight);
|
||||
}
|
||||
|
||||
for (let series = serieses - 1; series >= 0; series--) {
|
||||
ctx.strokeStyle = colors.dataset[series % colors.dataset.length];
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < series; i++) {
|
||||
total += xAxisPerTypeHeights[i];
|
||||
}
|
||||
|
||||
const height = xAxisPerTypeHeights[series];
|
||||
|
||||
const x = chartAreaX + (perXAxisWidth * ((xAxisCount - 1) - xAxis)) + (perXAxisWidth / 2);
|
||||
|
||||
const yTop = (chartAreaY + chartAreaHeight) - (total + height);
|
||||
const yBottom = (chartAreaY + chartAreaHeight) - (total);
|
||||
|
||||
ctx.globalAlpha = 1 - (xAxis / xAxisCount);
|
||||
ctx.beginPath();
|
||||
ctx.lineTo(x, yTop);
|
||||
ctx.lineTo(x, yBottom);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
return canvas.toBuffer();
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
|
||||
// https://github.com/apexcharts/apexcharts.js/blob/master/src/modules/Scales.js
|
||||
// This routine creates the Y axis values for a graph.
|
||||
function niceScale(lowerBound: number, upperBound: number, ticks: number): number[] {
|
||||
// Calculate Min amd Max graphical labels and graph
|
||||
// increments. The number of ticks defaults to
|
||||
// 10 which is the SUGGESTED value. Any tick value
|
||||
// entered is used as a suggested value which is
|
||||
// adjusted to be a 'pretty' value.
|
||||
//
|
||||
// Output will be an array of the Y axis values that
|
||||
// encompass the Y values.
|
||||
const steps = [];
|
||||
|
||||
// Determine Range
|
||||
const range = upperBound - lowerBound;
|
||||
|
||||
let tiks = ticks + 1;
|
||||
// Adjust ticks if needed
|
||||
if (tiks < 2) {
|
||||
tiks = 2;
|
||||
} else if (tiks > 2) {
|
||||
tiks -= 2;
|
||||
}
|
||||
|
||||
// Get raw step value
|
||||
const tempStep = range / tiks;
|
||||
|
||||
// Calculate pretty step value
|
||||
const mag = Math.floor(Math.log10(tempStep));
|
||||
const magPow = Math.pow(10, mag);
|
||||
const magMsd = (parseInt as any)(tempStep / magPow);
|
||||
const stepSize = magMsd * magPow;
|
||||
|
||||
// build Y label array.
|
||||
// Lower and upper bounds calculations
|
||||
const lb = stepSize * Math.floor(lowerBound / stepSize);
|
||||
const ub = stepSize * Math.ceil(upperBound / stepSize);
|
||||
// Build array
|
||||
let val = lb;
|
||||
while (1) {
|
||||
steps.push(val);
|
||||
val += stepSize;
|
||||
if (val > ub) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
|
@ -316,6 +316,11 @@ export default {
|
|||
post: '今日の迷路です! #AiMaze',
|
||||
foryou: '描きました!'
|
||||
},
|
||||
|
||||
chart: {
|
||||
post: 'インスタンスの投稿数です!',
|
||||
foryou: '描きました!'
|
||||
},
|
||||
};
|
||||
|
||||
export function getSerif(variant: string | string[]): string {
|
||||
|
|
|
@ -59,6 +59,9 @@ Misskeyにアカウントを作成して初めて投稿を行うと、藍がそ
|
|||
### バレンタイン
|
||||
藍がチョコレートをくれます。
|
||||
|
||||
### チャート
|
||||
インスタンスの投稿チャートなどを投稿してくれます。
|
||||
|
||||
### サーバー監視
|
||||
サーバーの状態を監視し、負荷が高くなっているときは教えてくれます。
|
||||
|
||||
|
|
Loading…
Reference in a new issue