mirror of
https://github.com/syuilo/ai.git
synced 2024-11-21 20:58:00 +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
|
built
|
||||||
node_modules
|
node_modules
|
||||||
memory.json
|
memory.json
|
||||||
|
font.ttf
|
||||||
|
|
|
@ -21,6 +21,9 @@ Misskey用の日本語Botです。
|
||||||
```
|
```
|
||||||
`npm install` して `npm run build` して `npm start` すれば起動できます
|
`npm install` して `npm run build` して `npm start` すれば起動できます
|
||||||
|
|
||||||
|
## フォント
|
||||||
|
一部の機能にはフォントが必要です。藍にはフォントは同梱されていないので、ご自身でフォントをインストールディレクトリに`font.ttf`という名前で設置してください。
|
||||||
|
|
||||||
## 記憶
|
## 記憶
|
||||||
藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。
|
藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import ServerModule from './modules/server';
|
||||||
import FollowModule from './modules/follow';
|
import FollowModule from './modules/follow';
|
||||||
import ValentineModule from './modules/valentine';
|
import ValentineModule from './modules/valentine';
|
||||||
import MazeModule from './modules/maze';
|
import MazeModule from './modules/maze';
|
||||||
|
import ChartModule from './modules/chart';
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import * as request from 'request-promise-native';
|
import * as request from 'request-promise-native';
|
||||||
|
@ -71,6 +72,7 @@ promiseRetry(retry => {
|
||||||
new ValentineModule(),
|
new ValentineModule(),
|
||||||
new KeywordModule(),
|
new KeywordModule(),
|
||||||
new MazeModule(),
|
new MazeModule(),
|
||||||
|
new ChartModule(),
|
||||||
]);
|
]);
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
log(chalk.red('Failed to fetch the account'));
|
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',
|
post: '今日の迷路です! #AiMaze',
|
||||||
foryou: '描きました!'
|
foryou: '描きました!'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
chart: {
|
||||||
|
post: 'インスタンスの投稿数です!',
|
||||||
|
foryou: '描きました!'
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getSerif(variant: string | string[]): string {
|
export function getSerif(variant: string | string[]): string {
|
||||||
|
|
|
@ -59,6 +59,9 @@ Misskeyにアカウントを作成して初めて投稿を行うと、藍がそ
|
||||||
### バレンタイン
|
### バレンタイン
|
||||||
藍がチョコレートをくれます。
|
藍がチョコレートをくれます。
|
||||||
|
|
||||||
|
### チャート
|
||||||
|
インスタンスの投稿チャートなどを投稿してくれます。
|
||||||
|
|
||||||
### サーバー監視
|
### サーバー監視
|
||||||
サーバーの状態を監視し、負荷が高くなっているときは教えてくれます。
|
サーバーの状態を監視し、負荷が高くなっているときは教えてくれます。
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue