mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +00:00 
			
		
		
		
	feat: introduce intersection calculation of charts
This commit is contained in:
		
							parent
							
								
									eb894c330f
								
							
						
					
					
						commit
						7fcd9435f3
					
				
					 15 changed files with 188 additions and 18 deletions
				
			
		
							
								
								
									
										47
									
								
								packages/backend/migration/1644344266289-chart-v14.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								packages/backend/migration/1644344266289-chart-v14.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
const { MigrationInterface, QueryRunner } = require("typeorm");
 | 
			
		||||
 | 
			
		||||
module.exports = class chartV141644344266289 {
 | 
			
		||||
    name = 'chartV141644344266289'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___users"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___users"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___notedUsers"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___notedUsers"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___users"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___users"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___notedUsers"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___notedUsers"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___readWrite" smallint NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___read" character varying array NOT NULL DEFAULT '{}'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___read" smallint NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___write" character varying array NOT NULL DEFAULT '{}'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___write" smallint NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___readWrite" smallint NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___read" character varying array NOT NULL DEFAULT '{}'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___read" smallint NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___write" character varying array NOT NULL DEFAULT '{}'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___write" smallint NOT NULL DEFAULT '0'`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___write"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___write"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___read"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___read"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___readWrite"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___write"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___write"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___read"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___read"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___readWrite"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___notedUsers" smallint NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___notedUsers" character varying array NOT NULL DEFAULT '{}'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___users" integer NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___users" character varying array NOT NULL DEFAULT '{}'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___notedUsers" smallint NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___notedUsers" character varying array NOT NULL DEFAULT '{}'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___users" integer NOT NULL DEFAULT '0'`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___users" character varying array NOT NULL DEFAULT '{}'`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +80,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
 | 
			
		||||
	const timeline = await query.take(ps.limit!).getMany();
 | 
			
		||||
 | 
			
		||||
	if (user) activeUsersChart.update(user);
 | 
			
		||||
	if (user) activeUsersChart.read(user);
 | 
			
		||||
 | 
			
		||||
	return await Notes.packMany(timeline, user);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -96,7 +96,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
 | 
			
		||||
	process.nextTick(() => {
 | 
			
		||||
		if (user) {
 | 
			
		||||
			activeUsersChart.update(user);
 | 
			
		||||
			activeUsersChart.read(user);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -153,7 +153,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
 | 
			
		||||
	process.nextTick(() => {
 | 
			
		||||
		if (user) {
 | 
			
		||||
			activeUsersChart.update(user);
 | 
			
		||||
			activeUsersChart.read(user);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -122,7 +122,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
 | 
			
		||||
	process.nextTick(() => {
 | 
			
		||||
		if (user) {
 | 
			
		||||
			activeUsersChart.update(user);
 | 
			
		||||
			activeUsersChart.read(user);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -145,7 +145,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
 | 
			
		||||
	process.nextTick(() => {
 | 
			
		||||
		if (user) {
 | 
			
		||||
			activeUsersChart.update(user);
 | 
			
		||||
			activeUsersChart.read(user);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -142,7 +142,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
 | 
			
		||||
	const timeline = await query.take(ps.limit!).getMany();
 | 
			
		||||
 | 
			
		||||
	activeUsersChart.update(user);
 | 
			
		||||
	activeUsersChart.read(user);
 | 
			
		||||
 | 
			
		||||
	return await Notes.packMany(timeline, user);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,9 +23,9 @@ export default class ActiveUsersChart extends Chart<typeof schema> {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async update(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise<void> {
 | 
			
		||||
	public async read(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise<void> {
 | 
			
		||||
		await this.commit({
 | 
			
		||||
			'users': [user.id],
 | 
			
		||||
			'read': [user.id],
 | 
			
		||||
			'registeredWithinWeek': (Date.now() - user.createdAt.getTime() < week) ? [user.id] : [],
 | 
			
		||||
			'registeredWithinMonth': (Date.now() - user.createdAt.getTime() < month) ? [user.id] : [],
 | 
			
		||||
			'registeredWithinYear': (Date.now() - user.createdAt.getTime() < year) ? [user.id] : [],
 | 
			
		||||
| 
						 | 
				
			
			@ -36,9 +36,9 @@ export default class ActiveUsersChart extends Chart<typeof schema> {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async noted(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise<void> {
 | 
			
		||||
	public async write(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise<void> {
 | 
			
		||||
		await this.commit({
 | 
			
		||||
			'notedUsers': [user.id],
 | 
			
		||||
			'write': [user.id],
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,8 +3,9 @@ import Chart from '../../core';
 | 
			
		|||
export const name = 'activeUsers';
 | 
			
		||||
 | 
			
		||||
export const schema = {
 | 
			
		||||
	'users': { uniqueIncrement: true },
 | 
			
		||||
	'notedUsers': { uniqueIncrement: true, range: 'small' },
 | 
			
		||||
	'readWrite': { intersection: ['read', 'write'], range: 'small' },
 | 
			
		||||
	'read': { uniqueIncrement: true, range: 'small' },
 | 
			
		||||
	'write': { uniqueIncrement: true, range: 'small' },
 | 
			
		||||
	'registeredWithinWeek': { uniqueIncrement: true, range: 'small' },
 | 
			
		||||
	'registeredWithinMonth': { uniqueIncrement: true, range: 'small' },
 | 
			
		||||
	'registeredWithinYear': { uniqueIncrement: true, range: 'small' },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import Chart from '../../core';
 | 
			
		||||
 | 
			
		||||
export const name = 'testIntersection';
 | 
			
		||||
 | 
			
		||||
export const schema = {
 | 
			
		||||
	'a': { uniqueIncrement: true },
 | 
			
		||||
	'b': { uniqueIncrement: true },
 | 
			
		||||
	'aAndB': { intersection: ['a', 'b'] },
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const entity = Chart.schemaToEntity(name, schema);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
import autobind from 'autobind-decorator';
 | 
			
		||||
import Chart, { KVs } from '../core';
 | 
			
		||||
import { name, schema } from './entities/test-intersection';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * For testing
 | 
			
		||||
 */
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default class TestIntersectionChart extends Chart<typeof schema> {
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super(name, schema);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	protected async queryCurrentState(): Promise<Partial<KVs<typeof schema>>> {
 | 
			
		||||
		return {};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async addA(key: string): Promise<void> {
 | 
			
		||||
		await this.commit({
 | 
			
		||||
			a: [key],
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async addB(key: string): Promise<void> {
 | 
			
		||||
		await this.commit({
 | 
			
		||||
			b: [key],
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +46,8 @@ const removeDuplicates = (array: any[]) => Array.from(new Set(array));
 | 
			
		|||
type Schema = Record<string, {
 | 
			
		||||
	uniqueIncrement?: boolean;
 | 
			
		||||
 | 
			
		||||
	intersection?: string[] | ReadonlyArray<string>;
 | 
			
		||||
 | 
			
		||||
	range?: 'big' | 'small' | 'medium';
 | 
			
		||||
 | 
			
		||||
	// previousな値を引き継ぐかどうか
 | 
			
		||||
| 
						 | 
				
			
			@ -384,6 +386,33 @@ export default abstract class Chart<T extends Schema> {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// compute intersection
 | 
			
		||||
			// TODO: intersectionに指定されたカラムがintersectionだった場合の対応
 | 
			
		||||
			for (const [k, v] of Object.entries(this.schema)) {
 | 
			
		||||
				const intersection = v.intersection;
 | 
			
		||||
				if (intersection) {
 | 
			
		||||
					const name = columnPrefix + k.replaceAll('.', columnDot);
 | 
			
		||||
					const firstKey = intersection[0];
 | 
			
		||||
					const firstTempColumnName = uniqueTempColumnPrefix + firstKey.replaceAll('.', columnDot);
 | 
			
		||||
					const currentValuesForHour = new Set([...(finalDiffs[firstKey] ?? []), ...logHour[firstTempColumnName]]);
 | 
			
		||||
					const currentValuesForDay = new Set([...(finalDiffs[firstKey] ?? []), ...logDay[firstTempColumnName]]);
 | 
			
		||||
					for (let i = 1; i < intersection.length; i++) {
 | 
			
		||||
						const targetKey = intersection[i];
 | 
			
		||||
						const targetTempColumnName = uniqueTempColumnPrefix + targetKey.replaceAll('.', columnDot);
 | 
			
		||||
						const targetValuesForHour = new Set([...(finalDiffs[targetKey] ?? []), ...logHour[targetTempColumnName]]);
 | 
			
		||||
						const targetValuesForDay = new Set([...(finalDiffs[targetKey] ?? []), ...logDay[targetTempColumnName]]);
 | 
			
		||||
						currentValuesForHour.forEach(v => {
 | 
			
		||||
							if (!targetValuesForHour.has(v)) currentValuesForHour.delete(v);
 | 
			
		||||
						});
 | 
			
		||||
						currentValuesForDay.forEach(v => {
 | 
			
		||||
							if (!targetValuesForDay.has(v)) currentValuesForDay.delete(v);
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
					queryForHour[name] = currentValuesForHour.size;
 | 
			
		||||
					queryForDay[name] = currentValuesForDay.size;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// ログ更新
 | 
			
		||||
			await Promise.all([
 | 
			
		||||
				this.repositoryForHour.createQueryBuilder()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -297,7 +297,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if (!silent) {
 | 
			
		||||
		if (Users.isLocalUser(user)) activeUsersChart.noted(user);
 | 
			
		||||
		if (Users.isLocalUser(user)) activeUsersChart.write(user);
 | 
			
		||||
 | 
			
		||||
		// 未読通知を作成
 | 
			
		||||
		if (data.visibility === 'specified') {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,14 +6,17 @@ import { async, initTestDb } from './utils';
 | 
			
		|||
import TestChart from '../src/services/chart/charts/test';
 | 
			
		||||
import TestGroupedChart from '../src/services/chart/charts/test-grouped';
 | 
			
		||||
import TestUniqueChart from '../src/services/chart/charts/test-unique';
 | 
			
		||||
import TestIntersectionChart from '../src/services/chart/charts/test-intersection';
 | 
			
		||||
import * as _TestChart from '../src/services/chart/charts/entities/test';
 | 
			
		||||
import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped';
 | 
			
		||||
import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique';
 | 
			
		||||
import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection';
 | 
			
		||||
 | 
			
		||||
describe('Chart', () => {
 | 
			
		||||
	let testChart: TestChart;
 | 
			
		||||
	let testGroupedChart: TestGroupedChart;
 | 
			
		||||
	let testUniqueChart: TestUniqueChart;
 | 
			
		||||
	let testIntersectionChart: TestIntersectionChart;
 | 
			
		||||
	let clock: lolex.Clock;
 | 
			
		||||
 | 
			
		||||
	beforeEach(async(async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,11 +24,13 @@ describe('Chart', () => {
 | 
			
		|||
			_TestChart.entity.hour, _TestChart.entity.day,
 | 
			
		||||
			_TestGroupedChart.entity.hour, _TestGroupedChart.entity.day,
 | 
			
		||||
			_TestUniqueChart.entity.hour, _TestUniqueChart.entity.day,
 | 
			
		||||
			_TestIntersectionChart.entity.hour, _TestIntersectionChart.entity.day,
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		testChart = new TestChart();
 | 
			
		||||
		testGroupedChart = new TestGroupedChart();
 | 
			
		||||
		testUniqueChart = new TestUniqueChart();
 | 
			
		||||
		testIntersectionChart = new TestIntersectionChart();
 | 
			
		||||
 | 
			
		||||
		clock = lolex.install({
 | 
			
		||||
			now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0))
 | 
			
		||||
| 
						 | 
				
			
			@ -426,6 +431,45 @@ describe('Chart', () => {
 | 
			
		|||
				foo: [2, 0, 0],
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		describe('Intersection', () => {
 | 
			
		||||
			it('条件が満たされていない場合はカウントされない', async(async () => {
 | 
			
		||||
				await testIntersectionChart.addA('alice');
 | 
			
		||||
				await testIntersectionChart.addA('bob');
 | 
			
		||||
				await testIntersectionChart.addB('carol');
 | 
			
		||||
				await testIntersectionChart.save();
 | 
			
		||||
	
 | 
			
		||||
				const chartHours = await testUniqueChart.getChart('hour', 3, null);
 | 
			
		||||
				const chartDays = await testUniqueChart.getChart('day', 3, null);
 | 
			
		||||
	
 | 
			
		||||
				assert.deepStrictEqual(chartHours, {
 | 
			
		||||
					aAndB: [0, 0, 0],
 | 
			
		||||
				});
 | 
			
		||||
	
 | 
			
		||||
				assert.deepStrictEqual(chartDays, {
 | 
			
		||||
					aAndB: [0, 0, 0],
 | 
			
		||||
				});
 | 
			
		||||
			}));
 | 
			
		||||
 | 
			
		||||
			it('条件が満たされている場合にカウントされる', async(async () => {
 | 
			
		||||
				await testIntersectionChart.addA('alice');
 | 
			
		||||
				await testIntersectionChart.addA('bob');
 | 
			
		||||
				await testIntersectionChart.addB('carol');
 | 
			
		||||
				await testIntersectionChart.addB('alice');
 | 
			
		||||
				await testIntersectionChart.save();
 | 
			
		||||
	
 | 
			
		||||
				const chartHours = await testUniqueChart.getChart('hour', 3, null);
 | 
			
		||||
				const chartDays = await testUniqueChart.getChart('day', 3, null);
 | 
			
		||||
	
 | 
			
		||||
				assert.deepStrictEqual(chartHours, {
 | 
			
		||||
					aAndB: [1, 0, 0],
 | 
			
		||||
				});
 | 
			
		||||
	
 | 
			
		||||
				assert.deepStrictEqual(chartDays, {
 | 
			
		||||
					aAndB: [1, 0, 0],
 | 
			
		||||
				});
 | 
			
		||||
			}));
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('Resync', () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,6 +69,7 @@ const colors = {
 | 
			
		|||
	yellow: '#FEB019',
 | 
			
		||||
	red: '#FF4560',
 | 
			
		||||
	purple: '#e300db',
 | 
			
		||||
	orange: '#fe6919',
 | 
			
		||||
};
 | 
			
		||||
const colorSets = [colors.blue, colors.green, colors.yellow, colors.red, colors.purple];
 | 
			
		||||
const getColor = (i) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -518,15 +519,20 @@ export default defineComponent({
 | 
			
		|||
			const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
 | 
			
		||||
			return {
 | 
			
		||||
				series: [{
 | 
			
		||||
					name: 'Active',
 | 
			
		||||
					name: 'Read & Write',
 | 
			
		||||
					type: 'area',
 | 
			
		||||
					data: format(raw.users),
 | 
			
		||||
					color: '#888888',
 | 
			
		||||
					data: format(raw.readWrite),
 | 
			
		||||
					color: colors.orange,
 | 
			
		||||
				}, {
 | 
			
		||||
					name: 'Noted',
 | 
			
		||||
					name: 'Write',
 | 
			
		||||
					type: 'area',
 | 
			
		||||
					data: format(raw.notedUsers),
 | 
			
		||||
					data: format(raw.write),
 | 
			
		||||
					color: colors.blue,
 | 
			
		||||
				}, {
 | 
			
		||||
					name: 'Read',
 | 
			
		||||
					type: 'area',
 | 
			
		||||
					data: format(raw.read),
 | 
			
		||||
					color: '#888888',
 | 
			
		||||
				}, {
 | 
			
		||||
					name: '< Week',
 | 
			
		||||
					type: 'area',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue