Rewrite insights in typescript and snabbdom
parent
84384b2222
commit
39c51a5ae6
|
@ -10,12 +10,14 @@ module.exports = targets => {
|
|||
const target = targets[args['config-plugin'] || 'main'];
|
||||
return {
|
||||
input: target.input,
|
||||
external: target.external,
|
||||
output: [
|
||||
prod
|
||||
? {
|
||||
file: `../../public/compiled/${target.output}.min.js`,
|
||||
format: 'iife',
|
||||
name: target.name,
|
||||
globals: target.globals,
|
||||
plugins: [
|
||||
terser({
|
||||
safari10: true,
|
||||
|
@ -30,6 +32,7 @@ module.exports = targets => {
|
|||
file: `../../public/compiled/${target.output}.js`,
|
||||
format: 'iife',
|
||||
name: target.name,
|
||||
globals: target.globals,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export default function numeral(n: number): Numeral;
|
||||
|
||||
export interface Numeral {
|
||||
format(f: string): string;
|
||||
}
|
|
@ -14,12 +14,13 @@
|
|||
"author": "ornicar",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@build/rollupProject": "2.0.0"
|
||||
"@build/rollupProject": "2.0.0",
|
||||
"@types/lichess": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"common": "2.0.0",
|
||||
"mithril": "github:ornicar/mithril.js#lila-1",
|
||||
"numeral": "^1.5"
|
||||
"numeral": "^1.5",
|
||||
"snabbdom": "^3.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "rollup --config",
|
||||
|
|
|
@ -3,8 +3,11 @@ import rollupProject from '@build/rollupProject';
|
|||
export default rollupProject({
|
||||
main: {
|
||||
name: 'LichessInsight',
|
||||
input: 'src/main.js',
|
||||
input: 'src/main.ts',
|
||||
output: 'insight',
|
||||
js: true,
|
||||
external: ['highcharts'],
|
||||
globals: {
|
||||
highcharts: 'Highcharts',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,80 +1,55 @@
|
|||
var m = require('mithril');
|
||||
import Ctrl from './ctrl';
|
||||
import { MaybeVNode, onInsert } from 'common/snabbdom';
|
||||
import { h } from 'snabbdom';
|
||||
import { Categ, Dimension, Metric } from './interfaces';
|
||||
|
||||
module.exports = function (ctrl) {
|
||||
return m('div.axis-form', [
|
||||
m(
|
||||
const selectData = (onClick: (v: { value: string }) => void) => ({
|
||||
attrs: { multiple: true },
|
||||
hook: onInsert(e =>
|
||||
$(e).multipleSelect({
|
||||
width: '200px',
|
||||
maxHeight: '400px',
|
||||
single: true,
|
||||
onClick,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const optgroup =
|
||||
<T>(callback: (item: T) => MaybeVNode) =>
|
||||
(categ: Categ<T>) =>
|
||||
h('optgroup', { attrs: { label: categ.name } }, categ.items.map(callback));
|
||||
|
||||
const option = (ctrl: Ctrl, item: Metric | Dimension, axis: 'metric' | 'dimension') =>
|
||||
h(
|
||||
'option',
|
||||
{
|
||||
attrs: {
|
||||
title: item.description.replace(/<a[^>]*>[^>]+<\/a[^>]*>/, ''),
|
||||
value: item.key,
|
||||
selected: ctrl.vm[axis].key === item.key,
|
||||
// was commented out:
|
||||
// if axis === 'metric'
|
||||
// disabled: !ctrl.validCombination(ctrl.vm.dimension, item),
|
||||
// if axis === 'dimension'
|
||||
// disabled: !ctrl.validCombination(item, ctrl.vm.metric),
|
||||
},
|
||||
},
|
||||
item.name
|
||||
);
|
||||
|
||||
export default function (ctrl: Ctrl) {
|
||||
return h('div.axis-form', [
|
||||
h(
|
||||
'select.ms.metric',
|
||||
{
|
||||
multiple: true,
|
||||
config: function (e, isUpdate) {
|
||||
$(e).multipleSelect({
|
||||
width: '200px',
|
||||
maxHeight: '400px',
|
||||
single: true,
|
||||
onClick: function (v) {
|
||||
ctrl.setMetric(v.value);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
ctrl.ui.metricCategs.map(function (categ) {
|
||||
return m(
|
||||
'optgroup',
|
||||
{
|
||||
label: categ.name,
|
||||
},
|
||||
categ.items.map(function (y) {
|
||||
return m(
|
||||
'option',
|
||||
{
|
||||
title: y.description.replace(/<a[^>]*>[^>]+<\/a[^>]*>/, ''),
|
||||
value: y.key,
|
||||
// disabled: !ctrl.validCombination(ctrl.vm.dimension, y),
|
||||
selected: ctrl.vm.metric.key === y.key,
|
||||
},
|
||||
y.name
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
selectData(v => ctrl.setMetric(v.value)),
|
||||
ctrl.ui.metricCategs.map(optgroup(y => option(ctrl, y, 'metric')))
|
||||
),
|
||||
m('span.by', 'by'),
|
||||
m(
|
||||
h('span.by', 'by'),
|
||||
h(
|
||||
'select.ms.dimension',
|
||||
{
|
||||
multiple: true,
|
||||
config: function (e, isUpdate) {
|
||||
$(e).multipleSelect({
|
||||
width: '200px',
|
||||
maxHeight: '400px',
|
||||
single: true,
|
||||
onClick: function (v) {
|
||||
ctrl.setDimension(v.value);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
ctrl.ui.dimensionCategs.map(function (categ) {
|
||||
return m(
|
||||
'optgroup',
|
||||
{
|
||||
label: categ.name,
|
||||
},
|
||||
categ.items.map(function (x) {
|
||||
if (x.key === 'period') return;
|
||||
return m(
|
||||
'option',
|
||||
{
|
||||
title: x.description.replace(/<a[^>]*>[^>]+<\/a[^>]*>/, ''),
|
||||
value: x.key,
|
||||
// disabled: !ctrl.validCombination(x, ctrl.vm.metric),
|
||||
selected: ctrl.vm.dimension.key === x.key,
|
||||
},
|
||||
x.name
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
selectData(v => ctrl.setDimension(v.value)),
|
||||
ctrl.ui.dimensionCategs.map(optgroup(x => (x.key !== 'period' ? option(ctrl, x, 'dimension') : undefined)))
|
||||
),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,29 +1,32 @@
|
|||
var m = require('mithril');
|
||||
import Ctrl from './ctrl';
|
||||
import { h } from 'snabbdom';
|
||||
import { Game } from './interfaces';
|
||||
import { onInsert } from 'common/snabbdom';
|
||||
|
||||
function miniGame(game) {
|
||||
return m(
|
||||
function miniGame(game: Game) {
|
||||
return h(
|
||||
'a',
|
||||
{
|
||||
key: game.id,
|
||||
href: `/${game.id}/${game.color}`,
|
||||
attrs: {
|
||||
key: game.id,
|
||||
href: `/${game.id}/${game.color}`,
|
||||
},
|
||||
},
|
||||
[
|
||||
m('span.mini-board.is2d', {
|
||||
'data-state': `${game.fen},${game.color},${game.lastMove}`,
|
||||
config(el, isUpdate) {
|
||||
if (!isUpdate) lichess.miniBoard.init(el);
|
||||
},
|
||||
h('span.mini-board.is2d', {
|
||||
attrs: { 'data-state': `${game.fen},${game.color},${game.lastMove}` },
|
||||
hook: onInsert(el => lichess.miniBoard.init(el)),
|
||||
}),
|
||||
m('span.vstext', [
|
||||
m('span.vstext__pl', [
|
||||
h('span.vstext', [
|
||||
h('span.vstext__pl', [
|
||||
game.user1.name,
|
||||
m('br'),
|
||||
h('br'),
|
||||
game.user1.title ? game.user1.title + ' ' : '',
|
||||
game.user1.rating,
|
||||
]),
|
||||
m('span.vstext__op', [
|
||||
h('span.vstext__op', [
|
||||
game.user2.name,
|
||||
m('br'),
|
||||
h('br'),
|
||||
game.user2.rating,
|
||||
game.user2.title ? ' ' + game.user2.title : '',
|
||||
]),
|
||||
|
@ -32,11 +35,11 @@ function miniGame(game) {
|
|||
);
|
||||
}
|
||||
|
||||
module.exports = function (ctrl) {
|
||||
export default function (ctrl: Ctrl) {
|
||||
if (!ctrl.vm.answer) return;
|
||||
|
||||
return m('div.game-sample.box', [
|
||||
m('div.top', 'Some of the games used to generate this insight'),
|
||||
m('div.boards', ctrl.vm.answer.games.map(miniGame)),
|
||||
return h('div.game-sample.box', [
|
||||
h('div.top', 'Some of the games used to generate this insight'),
|
||||
h('div.boards', ctrl.vm.answer.games.map(miniGame)),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,39 +1,56 @@
|
|||
var m = require('mithril');
|
||||
import { h, VNode } from 'snabbdom';
|
||||
import Ctrl from './ctrl';
|
||||
import { Chart } from './interfaces';
|
||||
import * as Highcharts from 'highcharts';
|
||||
|
||||
function metricDataTypeFormat(dt) {
|
||||
function metricDataTypeFormat(dt: string) {
|
||||
if (dt === 'seconds') return '{point.y:.1f}';
|
||||
if (dt === 'average') return '{point.y:,.1f}';
|
||||
if (dt === 'percent') return '{point.y:.1f}%';
|
||||
return '{point.y:,.0f}';
|
||||
}
|
||||
|
||||
function dimensionDataTypeFormat(dt) {
|
||||
function dimensionDataTypeFormat(dt: string) {
|
||||
if (dt === 'date') return '{value:%Y-%m-%d}';
|
||||
return '{value}';
|
||||
}
|
||||
|
||||
function yAxisTypeFormat(dt) {
|
||||
function yAxisTypeFormat(dt: string) {
|
||||
if (dt === 'seconds') return '{value:.1f}';
|
||||
if (dt === 'average') return '{value:,.1f}';
|
||||
if (dt === 'percent') return '{value:.0f}%';
|
||||
return '{value:,.0f}';
|
||||
}
|
||||
|
||||
var colors = {
|
||||
const colors = {
|
||||
green: '#759900',
|
||||
red: '#dc322f',
|
||||
orange: '#d59120',
|
||||
blue: '#007599',
|
||||
};
|
||||
var resultColors = {
|
||||
const resultColors = {
|
||||
Victory: colors.green,
|
||||
Draw: colors.blue,
|
||||
Defeat: colors.red,
|
||||
};
|
||||
|
||||
var theme = (function () {
|
||||
var light = $('body').hasClass('light');
|
||||
var t = {
|
||||
interface Theme {
|
||||
light: boolean;
|
||||
text: {
|
||||
weak: string;
|
||||
strong: string;
|
||||
};
|
||||
line: {
|
||||
weak: string;
|
||||
strong: string;
|
||||
fat: string;
|
||||
};
|
||||
colors?: string[];
|
||||
}
|
||||
|
||||
const theme = (function () {
|
||||
const light = $('body').hasClass('light');
|
||||
const t: Theme = {
|
||||
light: light,
|
||||
text: {
|
||||
weak: light ? '#808080' : '#9a9a9a',
|
||||
|
@ -62,8 +79,8 @@ var theme = (function () {
|
|||
return t;
|
||||
})();
|
||||
|
||||
function makeChart(el, data) {
|
||||
var sizeSerie = {
|
||||
function makeChart(el: HTMLElement, data: Chart) {
|
||||
const sizeSerie = {
|
||||
name: data.sizeSerie.name,
|
||||
data: data.sizeSerie.data,
|
||||
yAxis: 1,
|
||||
|
@ -74,8 +91,8 @@ function makeChart(el, data) {
|
|||
},
|
||||
color: 'rgba(120,120,120,0.2)',
|
||||
};
|
||||
var valueSeries = data.series.map(function (s) {
|
||||
var c = {
|
||||
const valueSeries = data.series.map(function (s) {
|
||||
const c: Highcharts.ColumnChartSeriesOptions = {
|
||||
name: s.name,
|
||||
data: s.data,
|
||||
yAxis: 0,
|
||||
|
@ -98,20 +115,20 @@ function makeChart(el, data) {
|
|||
);
|
||||
})(),
|
||||
shared: true,
|
||||
},
|
||||
} as Highcharts.SeriesTooltipOptions,
|
||||
};
|
||||
if (data.valueYaxis.name === 'Game result') c.color = resultColors[s.name];
|
||||
if (data.valueYaxis.name === 'Game result') c.color = resultColors[s.name as 'Victory' | 'Draw' | 'Defeat'];
|
||||
return c;
|
||||
});
|
||||
var chartConf = {
|
||||
const chartConf: Highcharts.Options = {
|
||||
chart: {
|
||||
type: 'column',
|
||||
alignTicks: data.valueYaxis.dataType !== 'percent',
|
||||
spacing: [20, 7, 20, 5],
|
||||
backgroundColor: null,
|
||||
backgroundColor: undefined,
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
plotBackgroundColor: null,
|
||||
plotBackgroundColor: undefined,
|
||||
plotShadow: false,
|
||||
plotBorderWidth: 0,
|
||||
style: {
|
||||
|
@ -119,25 +136,25 @@ function makeChart(el, data) {
|
|||
},
|
||||
},
|
||||
title: {
|
||||
text: null,
|
||||
text: undefined,
|
||||
},
|
||||
xAxis: {
|
||||
type: data.xAxis.dataType === 'date' ? 'datetime' : 'linear',
|
||||
categories: data.xAxis.categories.map(function (v) {
|
||||
return data.xAxis.dataType === 'date' ? v * 1000 : v;
|
||||
return `${data.xAxis.dataType === 'date' ? v * 1000 : v}`;
|
||||
}),
|
||||
crosshair: true,
|
||||
labels: {
|
||||
format: dimensionDataTypeFormat(data.xAxis.dataType),
|
||||
style: {
|
||||
color: theme.text.weak,
|
||||
fontSize: 9,
|
||||
fontSize: '9',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
style: {
|
||||
color: theme.text.weak,
|
||||
fontSize: 9,
|
||||
fontSize: '9',
|
||||
},
|
||||
},
|
||||
gridLineColor: theme.line.weak,
|
||||
|
@ -145,9 +162,9 @@ function makeChart(el, data) {
|
|||
tickColor: theme.line.strong,
|
||||
},
|
||||
yAxis: [data.valueYaxis, data.sizeYaxis].map(function (a, i) {
|
||||
var isPercent = data.valueYaxis.dataType === 'percent';
|
||||
var isSize = i % 2 === 1;
|
||||
var c = {
|
||||
const isPercent = data.valueYaxis.dataType === 'percent';
|
||||
const isSize = i % 2 === 1;
|
||||
const c: Highcharts.AxisOptions = {
|
||||
opposite: isSize,
|
||||
min: !isSize && isPercent ? 0 : undefined,
|
||||
max: !isSize && isPercent ? 100 : undefined,
|
||||
|
@ -155,14 +172,14 @@ function makeChart(el, data) {
|
|||
format: yAxisTypeFormat(a.dataType),
|
||||
style: {
|
||||
color: theme.text.weak,
|
||||
fontSize: 9,
|
||||
fontSize: '9',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
text: i === 1 ? a.name : false,
|
||||
text: i === 1 ? a.name : '',
|
||||
style: {
|
||||
color: theme.text.weak,
|
||||
fontSize: 9,
|
||||
fontSize: '9',
|
||||
},
|
||||
},
|
||||
gridLineColor: theme.line.weak,
|
||||
|
@ -170,7 +187,7 @@ function makeChart(el, data) {
|
|||
if (isSize && isPercent) {
|
||||
c.minorGridLineWidth = 0;
|
||||
c.gridLineWidth = 0;
|
||||
c.alternateGridColor = null;
|
||||
c.alternateGridColor = undefined;
|
||||
}
|
||||
return c;
|
||||
}),
|
||||
|
@ -235,20 +252,31 @@ function makeChart(el, data) {
|
|||
Highcharts.chart(el, chartConf);
|
||||
}
|
||||
|
||||
function empty(txt) {
|
||||
return m('div.chart.empty', [m('i[data-icon=]'), txt]);
|
||||
function empty(txt: string) {
|
||||
return h('div.chart.empty', [
|
||||
h('i', {
|
||||
attrs: { 'data-icon': '' },
|
||||
}),
|
||||
txt,
|
||||
]);
|
||||
}
|
||||
|
||||
module.exports = function (ctrl) {
|
||||
function chartHook(vnode: VNode, ctrl: Ctrl) {
|
||||
const el = vnode.elm as HTMLElement;
|
||||
if (ctrl.vm.loading || !ctrl.vm.answer) {
|
||||
$(el).html(lichess.spinnerHtml);
|
||||
} else {
|
||||
makeChart(el, ctrl.vm.answer);
|
||||
}
|
||||
}
|
||||
|
||||
export default function (ctrl: Ctrl) {
|
||||
if (!ctrl.validCombinationCurrent()) return empty('Invalid dimension/metric combination');
|
||||
if (!ctrl.vm.answer.series.length) return empty('No data. Try widening or clearing the filters.');
|
||||
return [
|
||||
m('div.chart', {
|
||||
config: function (el) {
|
||||
if (ctrl.vm.loading) return;
|
||||
makeChart(el, ctrl.vm.answer);
|
||||
},
|
||||
}),
|
||||
ctrl.vm.loading ? m.trust(lichess.spinnerHtml) : null,
|
||||
];
|
||||
};
|
||||
if (!ctrl.vm.answer?.series.length) return empty('No data. Try widening or clearing the filters.');
|
||||
return h('div.chart', {
|
||||
hook: {
|
||||
insert: vnode => chartHook(vnode, ctrl),
|
||||
update: (_oldVnode, newVnode) => chartHook(newVnode, ctrl),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,173 +1,160 @@
|
|||
var m = require('mithril');
|
||||
var throttle = require('common/throttle').default;
|
||||
import throttle from 'common/throttle';
|
||||
import * as xhr from 'common/xhr';
|
||||
import { Chart, Dimension, Env, Metric, Question, UI, EnvUser, Vm, Filters } from './interfaces';
|
||||
|
||||
module.exports = function (env, domElement) {
|
||||
this.ui = env.ui;
|
||||
this.user = env.user;
|
||||
this.own = env.myUserId === this.user.id;
|
||||
this.dimensions = [].concat.apply(
|
||||
[],
|
||||
this.ui.dimensionCategs.map(function (c) {
|
||||
return c.items;
|
||||
})
|
||||
);
|
||||
this.metrics = [].concat.apply(
|
||||
[],
|
||||
this.ui.metricCategs.map(function (c) {
|
||||
return c.items;
|
||||
})
|
||||
);
|
||||
export default class {
|
||||
env: Env;
|
||||
ui: UI;
|
||||
user: EnvUser;
|
||||
own: boolean;
|
||||
domElement: Element;
|
||||
redraw: () => void;
|
||||
|
||||
var findMetric = function (key) {
|
||||
return this.metrics.find(function (x) {
|
||||
return x.key === key;
|
||||
});
|
||||
}.bind(this);
|
||||
dimensions: Dimension[];
|
||||
metrics: Metric[];
|
||||
|
||||
var findDimension = function (key) {
|
||||
return this.dimensions.find(function (x) {
|
||||
return x.key === key;
|
||||
});
|
||||
}.bind(this);
|
||||
vm: Vm;
|
||||
|
||||
this.vm = {
|
||||
metric: findMetric(env.initialQuestion.metric),
|
||||
dimension: findDimension(env.initialQuestion.dimension),
|
||||
filters: env.initialQuestion.filters,
|
||||
loading: true,
|
||||
broken: false,
|
||||
answer: null,
|
||||
panel: Object.keys(env.initialQuestion.filters).length ? 'filter' : 'preset',
|
||||
};
|
||||
constructor(env: Env, domElement: Element, redraw: () => void) {
|
||||
this.env = env;
|
||||
this.ui = env.ui;
|
||||
this.user = env.user;
|
||||
this.own = env.myUserId === env.user.id;
|
||||
this.domElement = domElement;
|
||||
this.redraw = redraw;
|
||||
|
||||
this.setPanel = function (p) {
|
||||
this.dimensions = Array.prototype.concat.apply(
|
||||
[],
|
||||
env.ui.dimensionCategs.map(c => c.items)
|
||||
);
|
||||
this.metrics = Array.prototype.concat.apply(
|
||||
[],
|
||||
env.ui.metricCategs.map(c => c.items)
|
||||
);
|
||||
|
||||
this.vm = {
|
||||
metric: this.findMetric(this.env.initialQuestion.metric)!,
|
||||
dimension: this.findDimension(this.env.initialQuestion.dimension)!,
|
||||
filters: this.env.initialQuestion.filters,
|
||||
loading: true,
|
||||
broken: false,
|
||||
answer: null,
|
||||
panel: Object.keys(env.initialQuestion.filters).length ? 'filter' : 'preset',
|
||||
};
|
||||
}
|
||||
|
||||
private findMetric = (key: string) => this.metrics.find(x => x.key === key);
|
||||
|
||||
private findDimension = (key: string) => this.dimensions.find(x => x.key === key);
|
||||
|
||||
setPanel(p: 'filter' | 'preset') {
|
||||
this.vm.panel = p;
|
||||
m.redraw();
|
||||
}.bind(this);
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
var reset = function () {
|
||||
reset() {
|
||||
this.vm.metric = this.metrics[0];
|
||||
this.vm.dimension = this.dimensions[0];
|
||||
this.vm.filters = {};
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
var askQuestion = throttle(
|
||||
1000,
|
||||
function () {
|
||||
if (!this.validCombinationCurrent()) reset();
|
||||
this.pushState();
|
||||
this.vm.loading = true;
|
||||
this.vm.broken = false;
|
||||
m.redraw();
|
||||
setTimeout(
|
||||
function () {
|
||||
m.request({
|
||||
method: 'post',
|
||||
url: env.postUrl,
|
||||
data: {
|
||||
metric: this.vm.metric.key,
|
||||
dimension: this.vm.dimension.key,
|
||||
filters: this.vm.filters,
|
||||
},
|
||||
deserialize: function (d) {
|
||||
try {
|
||||
return JSON.parse(d);
|
||||
} catch (e) {
|
||||
throw new Error(d);
|
||||
}
|
||||
},
|
||||
}).then(
|
||||
function (answer) {
|
||||
this.vm.answer = answer;
|
||||
this.vm.loading = false;
|
||||
}.bind(this),
|
||||
function () {
|
||||
this.vm.loading = false;
|
||||
this.vm.broken = true;
|
||||
m.redraw();
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this),
|
||||
1
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
askQuestion = throttle(1000, () => {
|
||||
if (!this.validCombinationCurrent()) this.reset();
|
||||
this.pushState();
|
||||
this.vm.loading = true;
|
||||
this.vm.broken = false;
|
||||
this.redraw();
|
||||
setTimeout(() => {
|
||||
xhr
|
||||
.json(this.env.postUrl, {
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
metric: this.vm.metric.key,
|
||||
dimension: this.vm.dimension.key,
|
||||
filters: this.vm.filters,
|
||||
}),
|
||||
})
|
||||
.then(
|
||||
(answer: Chart) => {
|
||||
this.vm.answer = answer;
|
||||
this.vm.loading = false;
|
||||
this.redraw();
|
||||
},
|
||||
() => {
|
||||
this.vm.loading = false;
|
||||
this.vm.broken = true;
|
||||
this.redraw();
|
||||
}
|
||||
);
|
||||
}, 1);
|
||||
});
|
||||
|
||||
this.makeUrl = function (dKey, mKey, filters) {
|
||||
var url = [env.pageUrl, mKey, dKey].join('/');
|
||||
var filters = Object.keys(filters)
|
||||
makeUrl(dKey: string, mKey: string, filters: Filters) {
|
||||
let url = [this.env.pageUrl, mKey, dKey].join('/');
|
||||
const filtersStr = Object.keys(filters)
|
||||
.map(function (filterKey) {
|
||||
return filterKey + ':' + filters[filterKey].join(',');
|
||||
})
|
||||
.join('/');
|
||||
if (filters.length) url += '/' + filters;
|
||||
if (filtersStr.length) url += '/' + filtersStr;
|
||||
return url;
|
||||
};
|
||||
}
|
||||
|
||||
this.makeCurrentUrl = function () {
|
||||
makeCurrentUrl() {
|
||||
return this.makeUrl(this.vm.dimension.key, this.vm.metric.key, this.vm.filters);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
this.pushState = function () {
|
||||
history.replaceState({}, null, this.makeCurrentUrl());
|
||||
}.bind(this);
|
||||
pushState() {
|
||||
history.replaceState({}, '', this.makeCurrentUrl());
|
||||
}
|
||||
|
||||
this.validCombination = function (dimension, metric) {
|
||||
validCombination(dimension?: Dimension, metric?: Metric) {
|
||||
return dimension && metric && (dimension.position === 'game' || metric.position === 'move');
|
||||
};
|
||||
this.validCombinationCurrent = function () {
|
||||
}
|
||||
validCombinationCurrent() {
|
||||
return this.validCombination(this.vm.dimension, this.vm.metric);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
this.setMetric = function (key) {
|
||||
this.vm.metric = findMetric(key);
|
||||
setMetric(key: string) {
|
||||
this.vm.metric = this.findMetric(key)!;
|
||||
if (!this.validCombinationCurrent())
|
||||
this.vm.dimension = this.dimensions.find(
|
||||
function (d) {
|
||||
return this.validCombination(d, this.vm.metric);
|
||||
}.bind(this)
|
||||
);
|
||||
this.vm.dimension = this.dimensions.find(d => this.validCombination(d, this.vm.metric))!;
|
||||
this.vm.panel = 'filter';
|
||||
askQuestion();
|
||||
}.bind(this);
|
||||
this.askQuestion();
|
||||
}
|
||||
|
||||
this.setDimension = function (key) {
|
||||
this.vm.dimension = findDimension(key);
|
||||
setDimension(key: string) {
|
||||
this.vm.dimension = this.findDimension(key)!;
|
||||
if (!this.validCombinationCurrent())
|
||||
this.vm.metric = this.metrics.find(
|
||||
function (m) {
|
||||
return this.validCombination(this.vm.dimension, m);
|
||||
}.bind(this)
|
||||
);
|
||||
this.vm.metric = this.metrics.find(m => this.validCombination(this.vm.dimension, m))!;
|
||||
this.vm.panel = 'filter';
|
||||
askQuestion();
|
||||
}.bind(this);
|
||||
this.askQuestion();
|
||||
}
|
||||
|
||||
this.setFilter = function (dimensionKey, valueKeys) {
|
||||
setFilter(dimensionKey: string, valueKeys: string[]) {
|
||||
if (!valueKeys.length) delete this.vm.filters[dimensionKey];
|
||||
else this.vm.filters[dimensionKey] = valueKeys;
|
||||
askQuestion();
|
||||
}.bind(this);
|
||||
this.askQuestion();
|
||||
}
|
||||
|
||||
this.setQuestion = function (q) {
|
||||
this.vm.dimension = findDimension(q.dimension);
|
||||
this.vm.metric = findMetric(q.metric);
|
||||
setQuestion(q: Question) {
|
||||
this.vm.dimension = this.findDimension(q.dimension)!;
|
||||
this.vm.metric = this.findMetric(q.metric)!;
|
||||
this.vm.filters = q.filters;
|
||||
askQuestion();
|
||||
$(domElement).find('select.ms').multipleSelect('open');
|
||||
setTimeout(function () {
|
||||
$(domElement).find('select.ms').multipleSelect('close');
|
||||
this.askQuestion();
|
||||
$(this.domElement).find('select.ms').multipleSelect('open');
|
||||
setTimeout(() => {
|
||||
$(this.domElement).find('select.ms').multipleSelect('close');
|
||||
}, 1000);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
this.clearFilters = function () {
|
||||
clearFilters() {
|
||||
if (Object.keys(this.vm.filters).length) {
|
||||
this.vm.filters = {};
|
||||
askQuestion();
|
||||
this.askQuestion();
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
// this.trans = lichess.trans(env.i18n);
|
||||
|
||||
askQuestion();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,36 +1,45 @@
|
|||
var m = require('mithril');
|
||||
import { h, VNode } from 'snabbdom';
|
||||
import Ctrl from './ctrl';
|
||||
import { Dimension } from './interfaces';
|
||||
|
||||
function select(ctrl) {
|
||||
return function (dimension) {
|
||||
function select(ctrl: Ctrl) {
|
||||
return function (dimension: Dimension) {
|
||||
if (dimension.key === 'date') return;
|
||||
var single = dimension.key === 'period';
|
||||
return m(
|
||||
const single = dimension.key === 'period';
|
||||
function multipleSelect(vnode: VNode) {
|
||||
$(vnode.elm).multipleSelect({
|
||||
placeholder: dimension.name,
|
||||
width: '100%',
|
||||
selectAll: false,
|
||||
filter: dimension.key === 'opening',
|
||||
single: single,
|
||||
minimumCountSelected: 10,
|
||||
onClick: function (view) {
|
||||
const values = single ? [view.value] : $(vnode.elm).multipleSelect('getSelects');
|
||||
ctrl.setFilter(dimension.key, values);
|
||||
},
|
||||
});
|
||||
}
|
||||
return h(
|
||||
'select',
|
||||
{
|
||||
multiple: true,
|
||||
config: function (e, isUpdate) {
|
||||
if (isUpdate && ctrl.vm.filters[dimension.key]) return;
|
||||
$(e).multipleSelect({
|
||||
placeholder: dimension.name,
|
||||
width: '100%',
|
||||
selectAll: false,
|
||||
filter: dimension.key === 'opening',
|
||||
single: single,
|
||||
minimumCountSelected: 10,
|
||||
onClick: function (view) {
|
||||
var values = single ? [view.value] : $(e).multipleSelect('getSelects');
|
||||
ctrl.setFilter(dimension.key, values);
|
||||
},
|
||||
});
|
||||
attrs: { multiple: true },
|
||||
hook: {
|
||||
insert: multipleSelect,
|
||||
update: (_oldVnode, vnode) => {
|
||||
if (!ctrl.vm.filters[dimension.key]) multipleSelect(vnode);
|
||||
},
|
||||
},
|
||||
},
|
||||
dimension.values.map(function (value) {
|
||||
var selected = ctrl.vm.filters[dimension.key];
|
||||
return m(
|
||||
const selected = ctrl.vm.filters[dimension.key];
|
||||
return h(
|
||||
'option',
|
||||
{
|
||||
value: value.key,
|
||||
selected: selected && selected.includes(value.key),
|
||||
attrs: {
|
||||
value: value.key,
|
||||
selected: selected && selected.includes(value.key),
|
||||
},
|
||||
},
|
||||
value.name
|
||||
);
|
||||
|
@ -39,13 +48,14 @@ function select(ctrl) {
|
|||
};
|
||||
}
|
||||
|
||||
module.exports = function (ctrl) {
|
||||
return m('div.filters', [
|
||||
m(
|
||||
export default function (ctrl: Ctrl) {
|
||||
return h(
|
||||
'div.filters',
|
||||
h(
|
||||
'div.items',
|
||||
ctrl.ui.dimensionCategs.map(function (categ) {
|
||||
return m('div.categ.box', [m('div.top', categ.name), categ.items.map(select(ctrl))]);
|
||||
return h('div.categ.box', [h('div.top', categ.name), ...categ.items.map(select(ctrl))]);
|
||||
})
|
||||
),
|
||||
]);
|
||||
};
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
// Extend the global lichess object with a function defined in
|
||||
// public/javascripts/insight-refresh.js
|
||||
interface Lichess {
|
||||
refreshInsightForm(): void;
|
||||
}
|
||||
|
||||
/* Type definitions for multiple-select.js */
|
||||
|
||||
// multiple-select.js uses jquery, but typescript thinks $ is Cash, not jquery.
|
||||
// So as a hack, we extend the Cash interface knowing that at runtime, it will
|
||||
// actually be a jquery object.
|
||||
interface Cash {
|
||||
multipleSelect(method: 'getSelects'): string[];
|
||||
multipleSelect(
|
||||
method:
|
||||
| 'setSelects'
|
||||
| 'enable'
|
||||
| 'disable'
|
||||
| 'open'
|
||||
| 'close'
|
||||
| 'checkAll'
|
||||
| 'uncheckAll'
|
||||
| 'focus'
|
||||
| 'blur'
|
||||
| 'refresh'
|
||||
| 'close'
|
||||
): void;
|
||||
multipleSelect(option: MultiSelectOpts): void;
|
||||
}
|
||||
|
||||
interface MultiSelectOpts {
|
||||
name?: string;
|
||||
isOpen?: boolean;
|
||||
placeholder?: string;
|
||||
selectAll?: boolean;
|
||||
selectAllDelimiter?: string[];
|
||||
minimumCountSelected?: number;
|
||||
ellipsis?: boolean;
|
||||
multiple?: boolean;
|
||||
multipleWidth?: number;
|
||||
single?: boolean;
|
||||
filter?: boolean;
|
||||
width?: string;
|
||||
dropWidth?: number;
|
||||
maxHeight?: string;
|
||||
container?: Element;
|
||||
position?: string;
|
||||
keepOpen?: boolean;
|
||||
animate?: string;
|
||||
displayValues?: boolean;
|
||||
delimiter?: string;
|
||||
addTitle?: boolean;
|
||||
filterAcceptOnEnter?: boolean;
|
||||
hideOptgroupCheckboxes?: boolean;
|
||||
selectAllText?: string;
|
||||
allSelected?: string;
|
||||
countSelected?: string;
|
||||
noMatchesFound?: string;
|
||||
styler?(): boolean;
|
||||
textTemplate?($elm: Element): string;
|
||||
labelTemplate?($elm: Element): string;
|
||||
onOpen?(): any;
|
||||
onClose?(): any;
|
||||
onCheckAll?(): any;
|
||||
onUncheckAll?(): any;
|
||||
onFocus?(): any;
|
||||
onBlur?(): any;
|
||||
onOptgroupClick?(): any;
|
||||
onClick?(view: { label: string; value: string; checked: boolean; instance: any }): any;
|
||||
onFilter?(): any;
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
var m = require('mithril');
|
||||
import { h } from 'snabbdom';
|
||||
import Ctrl from './ctrl';
|
||||
|
||||
module.exports = function (ctrl) {
|
||||
return m('div.help.box', [
|
||||
m('div.top', 'Definitions'),
|
||||
m(
|
||||
export default function (ctrl: Ctrl) {
|
||||
return h('div.help.box', [
|
||||
h('div.top', 'Definitions'),
|
||||
h(
|
||||
'div.content',
|
||||
['metric', 'dimension'].map(function (type) {
|
||||
var data = ctrl.vm[type];
|
||||
return m('section.' + type, [m('h3', data.name), m('p', m.trust(data.description))]);
|
||||
['metric', 'dimension'].map(type => {
|
||||
const data = ctrl.vm[type as 'metric' | 'dimension'];
|
||||
return h('section.' + type, [h('h3', data.name), h('p', data.description)]);
|
||||
})
|
||||
),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,45 +1,59 @@
|
|||
var m = require('mithril');
|
||||
import { onInsert } from 'common/snabbdom';
|
||||
import { h } from 'snabbdom';
|
||||
import Ctrl from './ctrl';
|
||||
|
||||
var shareStates = ['nobody', 'friends only', 'everybody'];
|
||||
const shareStates = ['nobody', 'friends only', 'everybody'];
|
||||
|
||||
module.exports = function (ctrl) {
|
||||
var shareText = 'Shared with ' + shareStates[ctrl.user.shareId] + '.';
|
||||
return m('div.info.box', [
|
||||
m('div.top', [
|
||||
m(
|
||||
'a.username.user-link.insight-ulpt',
|
||||
{
|
||||
href: '/@/' + ctrl.user.name,
|
||||
},
|
||||
ctrl.user.name
|
||||
),
|
||||
]),
|
||||
m('div.content', [
|
||||
m('p', ['Insights over ', m('strong', ctrl.user.nbGames), ' rated games.']),
|
||||
m(
|
||||
export default function (ctrl: Ctrl) {
|
||||
const shareText = 'Shared with ' + shareStates[ctrl.user.shareId] + '.';
|
||||
return h('div.info.box', [
|
||||
h('div.top', [h('a.username.user-link.insight-ulpt', { attrs: { href: '/@/' + ctrl.user.name } }, ctrl.user.name)]),
|
||||
h('div.content', [
|
||||
h('p', ['Insights over ', h('strong', ctrl.user.nbGames), ' rated games.']),
|
||||
h(
|
||||
'p.share',
|
||||
ctrl.own
|
||||
? m(
|
||||
? h(
|
||||
'a',
|
||||
{
|
||||
href: '/account/preferences/privacy',
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
attrs: {
|
||||
href: '/account/preferences/privacy',
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
},
|
||||
},
|
||||
shareText
|
||||
)
|
||||
: shareText
|
||||
),
|
||||
]),
|
||||
m('div.refresh', {
|
||||
config: function (e, isUpdate) {
|
||||
if (isUpdate) return;
|
||||
var $ref = $('.insight-stale');
|
||||
if ($ref.length) {
|
||||
$(e).html($ref.show());
|
||||
lichess.refreshInsightForm();
|
||||
}
|
||||
},
|
||||
}),
|
||||
h(
|
||||
'div.refresh',
|
||||
ctrl.env.user.stale
|
||||
? h('div.insight-stale', [
|
||||
h('p', 'There are new games to learn from!'),
|
||||
h(
|
||||
'form.insight-refresh',
|
||||
{
|
||||
attrs: {
|
||||
action: `/insights/refresh/${ctrl.env.user.id}`,
|
||||
method: 'post',
|
||||
},
|
||||
hook: onInsert(_el => lichess.refreshInsightForm()),
|
||||
},
|
||||
[
|
||||
h('button.button.text', { attrs: { 'data-icon': '' } }, 'Upate insights'),
|
||||
h(
|
||||
'div.crunching.none',
|
||||
{
|
||||
hook: onInsert(el => el.insertAdjacentHTML('afterbegin', lichess.spinnerHtml)),
|
||||
},
|
||||
[h('br'), h('p', h('strong', 'Now crunching data just for you!'))]
|
||||
),
|
||||
]
|
||||
),
|
||||
])
|
||||
: null
|
||||
),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
export interface Vm {
|
||||
metric: Metric;
|
||||
dimension: Dimension;
|
||||
filters: Filters;
|
||||
loading: boolean;
|
||||
broken: boolean;
|
||||
answer: Chart | null;
|
||||
panel: 'filter' | 'preset';
|
||||
}
|
||||
|
||||
export interface Env {
|
||||
i18n: { [key: string]: string };
|
||||
initialQuestion: Question;
|
||||
myUserId: string;
|
||||
pageUrl: string;
|
||||
postUrl: string;
|
||||
ui: UI;
|
||||
user: EnvUser;
|
||||
}
|
||||
|
||||
export interface EnvUser {
|
||||
id: string;
|
||||
name: string;
|
||||
nbGames: number;
|
||||
shareId: number;
|
||||
stale: boolean;
|
||||
}
|
||||
|
||||
export interface Question {
|
||||
metric: string;
|
||||
dimension: string;
|
||||
filters: Filters;
|
||||
}
|
||||
|
||||
export type Filters = {
|
||||
[L in string]: string[];
|
||||
};
|
||||
|
||||
export interface UI {
|
||||
dimensionCategs: Categ<Dimension>[];
|
||||
metricCategs: Categ<Metric>[];
|
||||
presets: Preset[];
|
||||
}
|
||||
|
||||
export interface Categ<T> {
|
||||
name: string;
|
||||
items: T[];
|
||||
}
|
||||
|
||||
interface Preset {
|
||||
name: string;
|
||||
dimension: string;
|
||||
metric: string;
|
||||
filters: Filters;
|
||||
}
|
||||
|
||||
export interface Metric {
|
||||
key: string;
|
||||
name: string;
|
||||
position: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Dimension extends Metric {
|
||||
values: {
|
||||
key: string;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface Chart {
|
||||
question: Question;
|
||||
xAxis: Xaxis;
|
||||
valueYaxis: Yaxis;
|
||||
sizeYaxis: Yaxis;
|
||||
series: Serie[];
|
||||
sizeSerie: Serie;
|
||||
games: Game[];
|
||||
}
|
||||
|
||||
interface Xaxis {
|
||||
name: string;
|
||||
categories: number[];
|
||||
dataType: string;
|
||||
}
|
||||
|
||||
interface Yaxis {
|
||||
name: string;
|
||||
dataType: string;
|
||||
}
|
||||
|
||||
interface Serie {
|
||||
name: string;
|
||||
dataType: string;
|
||||
stack?: string;
|
||||
data: number[];
|
||||
}
|
||||
|
||||
export interface Game {
|
||||
id: string;
|
||||
fen: string;
|
||||
color: string;
|
||||
lastMove: string;
|
||||
user1: Player;
|
||||
user2: Player;
|
||||
}
|
||||
|
||||
interface Player {
|
||||
name: string;
|
||||
title?: string;
|
||||
rating: number;
|
||||
}
|
|
@ -1,17 +1,24 @@
|
|||
var m = require('mithril');
|
||||
import { attributesModule, classModule, init } from 'snabbdom';
|
||||
|
||||
var ctrl = require('./ctrl');
|
||||
var view = require('./view');
|
||||
import Ctrl from './ctrl';
|
||||
import { Env } from './interfaces';
|
||||
import view from './view';
|
||||
|
||||
module.exports = function (element, opts) {
|
||||
var controller = new ctrl(opts, element);
|
||||
const patch = init([classModule, attributesModule]);
|
||||
|
||||
m.module(element, {
|
||||
controller: function () {
|
||||
return controller;
|
||||
},
|
||||
view: view,
|
||||
});
|
||||
export default function (element: Element, opts: Env) {
|
||||
const ctrl = new Ctrl(opts, element, redraw);
|
||||
|
||||
return controller;
|
||||
};
|
||||
const blueprint = view(ctrl);
|
||||
let vnode = patch(element, blueprint);
|
||||
|
||||
// Wait until vnode has been intialized to call askQuestion because
|
||||
// askQuestion can call redraw
|
||||
ctrl.askQuestion();
|
||||
|
||||
function redraw() {
|
||||
vnode = patch(vnode, view(ctrl));
|
||||
}
|
||||
|
||||
return ctrl;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
var m = require('mithril');
|
||||
import { bind } from 'common/snabbdom';
|
||||
import { h } from 'snabbdom';
|
||||
import Ctrl from './ctrl';
|
||||
|
||||
module.exports = function (ctrl) {
|
||||
return m(
|
||||
export default function (ctrl: Ctrl) {
|
||||
return h(
|
||||
'div.box.presets',
|
||||
ctrl.ui.presets.map(function (p) {
|
||||
var active = ctrl.makeUrl(p.dimension, p.metric, p.filters) === ctrl.makeCurrentUrl();
|
||||
return m(
|
||||
'a',
|
||||
const active = ctrl.makeUrl(p.dimension, p.metric, p.filters) === ctrl.makeCurrentUrl();
|
||||
return h(
|
||||
'a.preset.text',
|
||||
{
|
||||
class: 'preset text' + (active ? ' active' : ''),
|
||||
'data-icon': '',
|
||||
onclick: function () {
|
||||
ctrl.setQuestion(p);
|
||||
},
|
||||
class: { active },
|
||||
attrs: { 'data-icon': '' },
|
||||
hook: bind('click', () => ctrl.setQuestion(p)),
|
||||
},
|
||||
p.name
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
var m = require('mithril');
|
||||
var numeral = require('numeral');
|
||||
import { h } from 'snabbdom';
|
||||
import numeral from 'numeral';
|
||||
import Ctrl from './ctrl';
|
||||
|
||||
function formatNumber(dt, n) {
|
||||
function formatNumber(dt: string, n: number) {
|
||||
if (dt === 'percent') n = n / 100;
|
||||
var f;
|
||||
let f;
|
||||
if (dt === 'seconds') f = '0.00';
|
||||
else if (dt === 'average') f = '0.00';
|
||||
else if (dt === 'percent') f = '0.0%';
|
||||
|
@ -11,38 +12,32 @@ function formatNumber(dt, n) {
|
|||
return numeral(n).format(f);
|
||||
}
|
||||
|
||||
function formatSerieName(dt, n) {
|
||||
function formatSerieName(dt: string, n: number) {
|
||||
if (dt === 'date') return new Date(n * 1000).toLocaleDateString();
|
||||
return n;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
vert: function (ctrl) {
|
||||
var answer = ctrl.vm.answer;
|
||||
if (!answer) return null;
|
||||
return m('table.slist', [
|
||||
m(
|
||||
'thead',
|
||||
m('tr', [
|
||||
m('th', answer.xAxis.name),
|
||||
answer.series.map(function (serie) {
|
||||
return m('th', serie.name);
|
||||
}),
|
||||
m('th', answer.sizeYaxis.name),
|
||||
])
|
||||
),
|
||||
m(
|
||||
'tbody',
|
||||
answer.xAxis.categories.map(function (c, i) {
|
||||
return m('tr', [
|
||||
m('th', formatSerieName(answer.xAxis.dataType, c)),
|
||||
answer.series.map(function (serie) {
|
||||
return m('td.data', formatNumber(serie.dataType, serie.data[i]));
|
||||
}),
|
||||
m('td.size', formatNumber(answer.sizeSerie.dataType, answer.sizeSerie.data[i])),
|
||||
]);
|
||||
})
|
||||
),
|
||||
]);
|
||||
},
|
||||
};
|
||||
export function vert(ctrl: Ctrl) {
|
||||
const answer = ctrl.vm.answer;
|
||||
if (!answer) return null;
|
||||
return h('table.slist', [
|
||||
h(
|
||||
'thead',
|
||||
h('tr', [
|
||||
h('th', answer.xAxis.name),
|
||||
...answer.series.map(serie => h('th', serie.name)),
|
||||
h('th', answer.sizeYaxis.name),
|
||||
])
|
||||
),
|
||||
h(
|
||||
'tbody',
|
||||
answer.xAxis.categories.map((c, i) => {
|
||||
return h('tr', [
|
||||
h('th', formatSerieName(answer.xAxis.dataType, c)),
|
||||
...answer.series.map(serie => h('td.data', formatNumber(serie.dataType, serie.data[i]))),
|
||||
h('td.size', formatNumber(answer.sizeSerie.dataType, answer.sizeSerie.data[i])),
|
||||
]);
|
||||
})
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,76 +1,57 @@
|
|||
var m = require('mithril');
|
||||
var axis = require('./axis');
|
||||
var filters = require('./filters');
|
||||
var presets = require('./presets');
|
||||
var chart = require('./chart');
|
||||
var table = require('./table');
|
||||
var help = require('./help');
|
||||
var info = require('./info');
|
||||
var boards = require('./boards');
|
||||
import { h, thunk } from 'snabbdom';
|
||||
import axis from './axis';
|
||||
import filters from './filters';
|
||||
import presets from './presets';
|
||||
import chart from './chart';
|
||||
import { vert } from './table';
|
||||
import help from './help';
|
||||
import info from './info';
|
||||
import boards from './boards';
|
||||
import Ctrl from './ctrl';
|
||||
import { bind } from 'common/snabbdom';
|
||||
|
||||
function cache(view, dataToKey) {
|
||||
var prev = null;
|
||||
return function (data) {
|
||||
var key = dataToKey(data);
|
||||
if (prev === key)
|
||||
return {
|
||||
subtree: 'retain',
|
||||
};
|
||||
prev = key;
|
||||
return view(data);
|
||||
};
|
||||
}
|
||||
const renderMain = (ctrl: Ctrl, _cacheKey: string | boolean) => {
|
||||
if (ctrl.vm.broken)
|
||||
return h('div.broken', [
|
||||
h('i', { attrs: { 'data-icon': '' } }),
|
||||
'Insights are unavailable.',
|
||||
h('br'),
|
||||
'Please try again later.',
|
||||
]);
|
||||
if (!ctrl.vm.answer) return h('div'); // returning undefined breaks snabbdom's thunks
|
||||
return h('div', [chart(ctrl), vert(ctrl), boards(ctrl)]);
|
||||
};
|
||||
|
||||
var renderMain = cache(
|
||||
function (ctrl) {
|
||||
if (ctrl.vm.broken)
|
||||
return m('div.broken', [m('i[data-icon=]'), 'Insights are unavailable.', m('br'), 'Please try again later.']);
|
||||
if (!ctrl.vm.answer) return;
|
||||
return m('div', [chart(ctrl), table.vert(ctrl), boards(ctrl)]);
|
||||
},
|
||||
function (ctrl) {
|
||||
var q = ctrl.vm.answer ? ctrl.vm.answer.question : null;
|
||||
return q ? ctrl.makeUrl(q.dimension, q.metric, q.filters) : ctrl.vm.broken;
|
||||
}
|
||||
);
|
||||
// Key that determines whether or not renderMain needs to get rerendered
|
||||
const cacheKey = (ctrl: Ctrl) => {
|
||||
const q = ctrl.vm.answer?.question;
|
||||
return q ? ctrl.makeUrl(q.dimension, q.metric, q.filters) : ctrl.vm.broken;
|
||||
};
|
||||
|
||||
module.exports = function (ctrl) {
|
||||
return m(
|
||||
'div',
|
||||
{
|
||||
class: ctrl.vm.loading ? 'loading' : 'ready',
|
||||
},
|
||||
[
|
||||
m('div.left-side', [
|
||||
const tabData = (ctrl: Ctrl, panel: 'filter' | 'preset') => ({
|
||||
class: { active: ctrl.vm.panel === panel },
|
||||
attrs: { 'data-panel': panel },
|
||||
hook: bind('click', () => ctrl.setPanel(panel)),
|
||||
});
|
||||
|
||||
export default function (ctrl: Ctrl) {
|
||||
return h(
|
||||
'main#insight',
|
||||
h('div.' + (ctrl.vm.loading ? 'loading' : 'ready'), [
|
||||
h('div.left-side', [
|
||||
info(ctrl),
|
||||
m('div.panel-tabs', [
|
||||
m(
|
||||
'a[data-panel="preset"]',
|
||||
{
|
||||
class: 'tab preset' + (ctrl.vm.panel === 'preset' ? ' active' : ''),
|
||||
onclick: function () {
|
||||
ctrl.setPanel('preset');
|
||||
},
|
||||
},
|
||||
'Presets'
|
||||
),
|
||||
m(
|
||||
'a[data-panel="filter"]',
|
||||
{
|
||||
class: 'tab filter' + (ctrl.vm.panel === 'filter' ? ' active' : ''),
|
||||
onclick: function () {
|
||||
ctrl.setPanel('filter');
|
||||
},
|
||||
},
|
||||
'Filters'
|
||||
),
|
||||
h('div.panel-tabs', [
|
||||
h('a.tab.preset', tabData(ctrl, 'preset'), 'Presets'),
|
||||
h('a.tab.filter', tabData(ctrl, 'filter'), 'Filters'),
|
||||
Object.keys(ctrl.vm.filters).length
|
||||
? m(
|
||||
? h(
|
||||
'a.clear',
|
||||
{
|
||||
title: 'Clear all filters',
|
||||
'data-icon': '',
|
||||
onclick: ctrl.clearFilters,
|
||||
attrs: {
|
||||
title: 'Clear all filters',
|
||||
'data-icon': '',
|
||||
},
|
||||
hook: bind('click', ctrl.clearFilters.bind(ctrl)),
|
||||
},
|
||||
'CLEAR'
|
||||
)
|
||||
|
@ -80,18 +61,8 @@ module.exports = function (ctrl) {
|
|||
ctrl.vm.panel === 'preset' ? presets(ctrl) : null,
|
||||
help(ctrl),
|
||||
]),
|
||||
m('header', [
|
||||
axis(ctrl),
|
||||
m(
|
||||
'h2',
|
||||
{
|
||||
class: 'text',
|
||||
'data-icon': '',
|
||||
},
|
||||
'Chess Insights'
|
||||
),
|
||||
]),
|
||||
m('div.insight__main.box', renderMain(ctrl)),
|
||||
]
|
||||
h('header', [axis(ctrl), h('h2.text', { attrs: { 'data-icon': '' } }, 'Chess Insights')]),
|
||||
thunk('div.insight__main.box', renderMain, [ctrl, cacheKey(ctrl)]),
|
||||
])
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"numeral": ["./@types/numeral"]
|
||||
}
|
||||
},
|
||||
"extends": "../tsconfig.base.json"
|
||||
}
|
Loading…
Reference in New Issue