OSDN Git Service

報告書の戦果のグラフが機能しないのを直す
[kancollesniffer/KancolleSniffer.git] / LogViewer / tags.tag
1 <dummy>
2 </dummy>
3
4 <main-tab>
5 <ul class="tab">
6     <li each={name, i in mainTabs} class={select: mainTab === i} onclick={parent.changeTab}>{name}</li>
7 </ul>
8
9 <script>
10 /* global moment, c3, opts */
11
12 this.mainTab = +sessionStorage.getItem('prevTab');
13 opts.observable.trigger("mainTabChanged", this.mainTab);
14
15 this.changeTab = function(e) {
16     this.mainTab = e.item.i;
17     sessionStorage.setItem('prevTab', e.item.i);
18     opts.observable.trigger("mainTabChanged", e.item.i);
19 }.bind(this);
20 </script>
21 </main-tab>
22
23 <log-term>
24 <div id="term" show={enabled}>
25 <ul class="tab tabsub" style="float: left; margin-right: 0.2em">
26     <li each={name, i in rangeTabs} class={select: opts.logRange.val === i} onclick={parent.rangeTabChange}>{name}</li>
27 </ul>
28 <div style="padding: 0.2em 0;">
29 <input type="text" id="term_from" style="width: 10em">~<input type="text" id="term_to" style="width: 10em">
30 </div>
31 </div>
32
33 <script>
34 var self = this;
35
36 this.rangeTabs = [
37 "今日",
38 "今週",
39 "今月",
40 "すべて",
41 "期間指定"
42 ];
43
44 this.enabled = false;
45
46 opts.observable.on("mainTabChanged", function(idx) {
47     self.update({enabled: idx >= 0 && idx < self.logTables});
48 });
49
50 var val = sessionStorage.getItem('logRange');
51 opts.logRange.val = val === null ? 2 : +val;
52
53 this.init = function() {
54     self.initPicker('#term_from', '#term_to', function() {
55         if (opts.logRange.val === 4)
56             opts.observable.trigger( "logRangeChanged");
57     });
58 };
59
60 this.on("mount", this.init);
61
62 this.rangeTabChange = function(e) {
63     sessionStorage.setItem("logRange", e.item.i);
64     opts.logRange.val = e.item.i;
65     opts.observable.trigger("logRangeChanged");
66 };
67
68 </script>
69 </log-term>
70
71 <log-tables>
72 <div each={header, i in tables} show={mainTab === i}>
73 <table class="display compact cell-border" id={"log" + i}>
74 <thead>
75 <tr></tr>
76 </thead>
77 <tfoot>
78 <tr></tr>
79 </toot>
80 </table>
81 </div>
82
83 <script>
84 this.tables = [
85 "<th>日付</th><th>海域</th><th>マップ</th><th>マス</th><th>ボス</th><th>ランク</th><th>ドロップ艦種</th><th>ドロップ艦娘", // ドロップ
86 "<th>日付</th><th style=\"min-width: 3.2em;\">海域</th><th>マップ</th><th>マス</th><th>ボス</th><th>ランク</th><th>艦隊行動</th><th>味方陣形</th><th>敵陣形</th><th style=\"min-width: 3.2em;\">敵艦隊</th><th>味方艦1</th><th>味方艦1HP</th><th>味方艦2</th><th>味方艦2HP</th><th>味方艦3</th><th>味方艦3HP</th><th>味方艦4</th><th>味方艦4HP</th><th>味方艦5</th><th>味方艦5HP</th><th>味方艦6</th><th>味方艦6HP</th><th>大破艦</th><th>中破艦</th><th style=\"min-width: 2.2em;\">敵艦1</th><th>敵艦1HP</th><th style=\"min-width: 2.2em;\">敵艦2</th><th>敵艦2HP</th><th style=\"min-width: 2.2em;\">敵艦3</th><th>敵艦3HP</th><th style=\"min-width: 2.2em;\">敵艦4</th><th>敵艦4HP</th><th style=\"min-width: 2.2em;\">敵艦5</th><th>敵艦5HP</th><th style=\"min-width: 2.2em;\">敵艦6</th><th>敵艦6HP</th><th>味方制空値</th><th>敵制空値</th><th>制空状態</th>", // 海戦
87 "<th>日付</th><th>結果</th><th>遠征</th><th>燃料</th><th>弾薬</th><th>鋼材</th><th>ボーキ</th><th>開発資材</th><th>高速修復材</th><th>高速建造材</th><th>改修資材</th>", // 遠征
88 "<th>日付</th><th>開発装備</th><th>種別</th><th>燃料</th><th>弾薬</th><th>鋼材</th><th>ボーキ</th><th>秘書艦</th><th>司令部Lv</th>", // 開発
89 "<th>日付</th><th>種類</th><th>名前</th><th>艦種</th><th>燃料</th><th>弾薬</th><th>鋼材</th><th>ボーキ</th><th>開発資材</th><th>空きドック</th><th>秘書艦</th><th>司令部Lv</th>", // 建造
90 "<th>日付</th><th>改修装備</th><th>レベル</th><th>成功</th><th>確実化</th><th>消費装備</th><th>消費数</th><th>燃料</th><th>弾薬</th><th>鋼材</th><th>ボーキ</th><th>開発資材</th><th>改修資材</th><th>秘書艦</th><th>二番艦</th>", // 改修
91 "<th>日付</th><th>燃料</th><th>弾薬</th><th>鋼材</th><th>ボーキ</th><th>高速建造材</th><th>高速修復材</th><th>開発資材</th><th>改修資材</th>" // 戦果
92 ];
93
94 this.jsons = [
95     "海戦・ドロップ報告書.json",
96     "海戦・ドロップ報告書.json",
97     "遠征報告書.json",
98     "開発報告書.json",
99     "建造報告書.json",
100     "改修報告書.json",
101     "資材ログ.json"
102 ];
103
104 this.on("mount", function() {
105     this.init();
106 });
107
108 this.mainTab = 0;
109 var self = this;
110
111 opts.observable.on("mainTabChanged", function(idx) {
112     self.update({mainTab: idx});
113     self.show();
114 });
115
116 opts.observable.on("logRangeChanged", function() {
117     self.show();
118 });
119
120 this.init = function() {
121     for (var t = 0; t < this.tables.length; t++) {
122         var table = $('#log' + t);
123         self.setHeaderAndFooter(table, self.tables[t]);
124         var dTable = table.DataTable(self.tableOptions(t));
125         self.searchSetup(dTable);
126     }
127 };
128
129 this.tableOptions = function(tableId) {
130     var opts = {
131         destroy: true,
132         deferRender: true,
133         stateSave: true,
134         order: [[0, "desc"]],
135         pageLength: 50,
136         lengthMenu: [[50, 100, 200, -1], [50, 100, 200, "All"]],
137         drawCallback: function() {
138             $('#loading').hide();
139         }
140     };
141     if (tableId === 0) {
142         opts.columns = self.dropColumns();
143     } else if (tableId === 1) {
144         opts.columns = self.sortieColumns();
145     }
146     return opts;
147 };
148
149 this.dropColumns = function() {
150     return [{data: 0}, {data: 1}, {data: 40}, {data: 2}, {data: 3}, {data: 4}, {data: 9}, {data: 10}];
151 };
152
153 this.sortieColumns = function() {
154     var entries = [];
155     for (var i = 0; i < 38; i++) {
156         if (i === 2)
157             entries.push({data: 40});
158         if (i === 9 || i === 10)
159             continue;
160         if (i === 23) {
161             entries.push({data: 38});
162             entries.push({data: 39});
163         }
164         entries.push({data: i});
165     }
166     return entries;
167 };
168
169 this.setHeaderAndFooter = function(table, header) {
170     table.find("thead tr").first().html(header);
171     var footer = table.find("tfoot tr");
172     table.find("th").each(function(index) {
173         footer.append(
174             '<th style="padding: 1px"><input style="min-width: 100%" size="1" type="search" placeholder="Search ' +
175             $(this).text() + '"/></th>');
176     });
177 };
178
179 this.searchSetup = function(dTable) {
180     self.setupCellSearch(dTable);
181     self.setupGlobalSearch(dTable);
182 };
183
184 this.setupCellSearch = function(dTable) {
185     dTable.columns().every(function() {
186         var that = this;
187         that.search(""); // reset
188         $('input', this.footer()).on("input search", function() {
189             that.search(this.value, true, false).draw();
190         });
191     });
192 };
193
194 this.setupGlobalSearch = function(dTable) {
195     var searchLabel = $(dTable.table().container()).find(".dataTables_filter label").first();
196     searchLabel.html('Search: <input type="search">');
197     searchLabel.children("input").first().on("input search", function() {
198         dTable.search(this.value, true, false).draw();
199     });
200     dTable.search(""); // reset
201 };
202
203 this.show = function() {
204     if (this.mainTab >= this.jsons.length)
205         return;
206     var now = moment();
207     var from;
208     var query = "?from=";
209     switch (opts.logRange.val){
210     case 0:
211         from = now.clone().startOf('day').hours(5);
212         if (now.hour() < 5)
213             from.subtract(1, 'days');
214         query += from.valueOf();
215         break;
216     case 1:
217         from = now.clone().day(1).startOf('day').hours(5);
218         if (now.day() === 0 || now.day() === 1 && now.hour() < 5)
219             from.subtract(1, 'weeks');
220         query += from.valueOf();
221         break;
222     case 2:
223         if (now.hours() >= 22 &&
224             now.date() === now.clone().endOf('month').date()) {
225             from = now.clone().startOf('day').hours(22);
226         } else {
227             from = now.clone().startOf('month').subtract(1, 'days').hours(22);
228         }
229         query += from.valueOf();
230         break;
231     case 3:
232         query = "";
233         break;
234     case 4:
235         from = $('#term_from').datetimepicker("getValue");
236         var to = $('#term_to').datetimepicker("getValue");
237         if (from === null)
238             return;
239         query += from.valueOf();
240         if (to !== null)
241             query += "&to=" + to.valueOf();
242         break;
243     }
244     $('#loading').show();
245     var url = this.jsons[this.mainTab] + query;
246     $('#log' + this.mainTab).DataTable().ajax.url(url).load();
247 };
248 </script>
249 </log-tables>
250
251 <chart-type>
252 <form id="chart_type" show={mainTabs[mainTab] === "資材グラフ"}>
253 <div style="margin: 0 0 0.5em 1em;">
254 <label><input type="radio" name="chart_type" value="0" checked={opts.chartSpec.type === 0} onchange={chartTypeChange}>連続</label>
255 <label><input type="radio" name="chart_type" value="1" checked={opts.chartSpec.type === 1} onchange={chartTypeChange}>差分</label>
256 </div>
257 </form>
258
259 <script>
260 this.mainTab = 0;
261 opts.chartSpec.type = +sessionStorage.getItem('chartType');
262 var self = this;
263
264 this.chartTypeChange = function(e) {
265     opts.chartSpec.type = +e.target.value;
266     sessionStorage.setItem('chartType', opts.chartSpec.type);
267     opts.observable.trigger("chartTypeChanged");
268     opts.observable.trigger("chartSpecChanged");
269 };
270
271 opts.observable.on("mainTabChanged", function(idx) {
272     self.update({mainTab: idx});
273 });
274 </script>
275 </chart-type>
276
277 <chart-range>
278 <div show={mainTabs[mainTab] === "資材グラフ"}>
279 <ul class="tab tabsub" style="float: left; margin-right: 0.2em" show={chartSpec.type === 0}>
280     <li each={name, i in seqChartRanges} class={select: chartSpec.seqRange === i} onclick={parent.rangeTabChange}>{name}</li>
281 </ul>
282
283 <ul class="tab tabsub" style="float: left; margin-right: 0.2em" show={chartSpec.type === 1}>
284     <li each={name, i in diffChartRanges} class={select: chartSpec.diffRange === i} onclick={parent.rangeTabChange}>{name}</li>
285 </ul>
286 <div style="padding: 0.2em 0;">
287 <input type="text" id="chart_from" style="width: 10em">~<input type="text" id="chart_to" style="width: 10em">
288 <label><input type="checkbox" id="tooltip" value="" style="margin-left: 2em;" onchange={tooltipChange} checked={opts.chartSpec.tooltip === 1}>ツールチップ</label>
289 </div>
290 </div>
291
292 <script>
293 this.seqChartRanges = [
294 "一日",
295 "一週間",
296 "一か月",
297 "三か月",
298 "すべて",
299 "期間指定"
300 ];
301
302 this.diffChartRanges = [
303 "一か月(日)",
304 "三か月(日)",
305 "半年(週)",
306 "すべて(月)",
307 "期間指定"
308 ];
309
310 opts.chartSpec.seqRange = +sessionStorage.getItem('seqChartRange');
311 opts.chartSpec.diffRange = +sessionStorage.getItem('diffChartRange');
312 opts.chartSpec.tooltip = +sessionStorage.getItem('chartTooltip');
313 this.chartSpec = opts.chartSpec;
314
315 this.rangeTabChange = function(e) {
316     if (opts.chartSpec.type === 0) {
317         opts.chartSpec.seqRange = e.item.i;
318         sessionStorage.setItem('seqChartRange', e.item.i);
319     } else {
320         opts.chartSpec.diffRange = e.item.i;
321         sessionStorage.setItem('diffChartRange', e.item.i);
322     }
323     opts.observable.trigger("chartSpecChanged");
324 };
325
326 this.tooltipChange = function(e) {
327     opts.chartSpec.tooltip = +e.target.checked;
328     sessionStorage.setItem('chartTooltip', +e.target.checked);
329     opts.observable.trigger("chartSpecChanged");
330 };
331
332 this.useDatePicker = function() {
333     return opts.chartSpec.type === 0 && opts.chartSpec.seqRange === 5 ||
334         opts.chartSpec.type === 1 && opts.chartSpec.diffRange === 4;
335 };
336
337 var self = this;
338
339 this.init = function() {
340     self.initPicker('#chart_from', '#chart_to', function() {
341         if (self.useDatePicker())
342             opts.observable.trigger("chartSpecChanged");
343     });
344 };
345
346 this.mainTab = 0;
347
348 this.on("mount", self.init);
349
350 opts.observable.on("mainTabChanged", function(idx) {
351     self.update({mainTab: idx});
352 });
353
354 opts.observable.on("chartTypeChanged", function() {
355     self.update();
356 });
357 </script>
358 </chart-range>
359
360 <sequential-chart>
361 <script>
362 var self = this;
363
364 opts.observable.on("chartSpecChanged", function() {
365     if (opts.chartSpec.type === 0)
366         self.drawChart();
367 });
368
369 opts.observable.on("chartSizeChanged", function() {
370     if (opts.chartSpec.type === 0)
371         self.resize();
372 });
373
374 this.header = ["日付", "燃料", "弾薬", "鋼材", "ボーキ", "高速建造材", "高速修復材", "開発資材", "改修資材"];
375
376 opts.observable.on("offAllLegends", function() {
377     if (opts.chartSpec.type !== 0)
378         return;
379     self.chart.hide();
380     self.header.slice(1).forEach(function(c) {
381         self.unselected[c] = true;
382     });
383 });
384
385 this.resize = function() {
386     if (!self.chart)
387         return;
388     $('#loading').show();
389     setTimeout(function() {
390         self.chart.resize(self.chartSize());
391     });
392 };
393
394 this.drawChart = function(data) {
395     var range = this.calcRange(opts.chartSpec.seqRange);
396     if (range.last === 0)
397         return;
398     if (!data) {
399         $('#loading').show();
400         $.ajax({
401             url: "./資材ログ.json?number=true" +
402                 "&from=" + range.first + "&to=" + range.last,
403             success: function(d) { self.drawChart(d); },
404             dataType: "json", cache: false
405         });
406         return;
407     }
408     var picked;
409     picked = this.pickChartData(data.data, range);
410     picked.data.unshift(self.header);
411     this.drawSeqChart(picked);
412 };
413
414 this.calcRange = function(range) {
415     var first = 0;
416     var last = (new Date()).valueOf();
417     switch (range) {
418         case 0:
419             first = moment(last).subtract(24, 'hours').valueOf();
420             break;
421         case 1:
422             first = moment(last).subtract(7, 'days').valueOf();
423             break;
424         case 2:
425             first = moment(last).subtract(1, 'months').valueOf();
426             break;
427         case 3:
428             first = moment(last).subtract(3, 'months').valueOf();
429             break;
430         case 4:
431             break;
432         case 5:
433             var fromDate = $('#chart_from').datetimepicker("getValue");
434             var toDate = $('#chart_to').datetimepicker("getValue");
435             if (fromDate === null || toDate === null)
436                 return {first: 0, last:0};
437             first = fromDate.valueOf();
438             last = toDate.valueOf();
439             break;
440     }
441     return {first: first, last: last};
442 };
443
444 this.unselected = {};
445
446 this.drawSeqChart = function(picked) {
447     var size = this.chartSize();
448     this.chart = c3.generate({
449         bindto: '#chart',
450         size: {
451             height: size.height,
452             width: size.width
453         },
454         data: {
455             x: '日付',
456             xFormat: '%Y-%m-%d %H:%M:%S',
457             rows: picked.data,
458             axes: {
459                 燃料: 'y',
460                 弾薬: 'y',
461                 鋼材: 'y',
462                 ボーキ: 'y',
463                 高速建造材: 'y2',
464                 高速修復材: 'y2',
465                 開発資材: 'y2',
466                 改修資材: 'y2'
467             }
468         },
469         point: {
470             show: false
471         },
472         tooltip: {
473             show: opts.chartSpec.tooltip
474         },
475         grid: {
476             x: {
477                 lines: picked.grid
478             }
479         },
480         axis: {
481             x: {
482                 type: 'timeseries',
483                 tick: {
484                     rotate: 30,
485                     format: "%m-%d %H:%M",
486                     values: picked.tick
487                 }
488             },
489             y2: {
490                 show: true
491             }
492         },
493         legend: {
494             item: {
495                 onclick: function(id) {
496                     self.unselected[id] = !self.unselected[id];
497                     self.chart.toggle(id);
498                 }
499             }
500         },
501         onrendered: function() {
502             $('#loading').hide();
503             opts.observable.trigger("chartRendered");
504         }
505     });
506     self.chart.hide(Object.keys(self.unselected).filter(function(e) {
507         return self.unselected[e];
508     }));
509 };
510
511 this.pickChartData = function(data, range) {
512     var newdata = [];
513     var ticks = [];
514     var grid = [];
515     var first = range.first;
516     var last = range.last;
517     var interval, tickInterval, lastTick;
518     if (last <= first + this.oneDay) {
519         interval = 1000;
520         tickInterval = 3600 * 1000;
521         lastTick = last - last % tickInterval;
522     } else if (last <= first + this.oneDay * 21) {
523         interval = 1000;
524         tickInterval = this.oneDay;
525         lastTick = this.to5am(last);
526     } else if (last <= first + this.oneDay * 63) {
527         interval = 3600 * 1000;
528         tickInterval = this.oneDay * 7;
529         lastTick = this.to5am(moment(last).day(1).valueOf());
530     } else if (last <= first + this.oneDay * 126) {
531         interval = 3600 * 6000;
532         tickInterval = this.oneDay * 14;
533         lastTick = this.to5am(moment(last).day(1).valueOf());
534     } else {
535         var magn = Math.ceil((last - data[0][0]) / (this.oneDay * 365) / 2);
536         interval = this.oneDay * magn;
537         tickInterval = this.oneDay * 28 * magn;
538         lastTick = this.to5am(moment(last).day(1).valueOf());
539     }
540     var lastData;
541     for (var i = data.length - 1; i >= 0; i--) {
542         var row = data[i];
543         var date = row[0];
544         if (date >= first) {
545             if (date <= last) {
546                 var v = date - date % interval;
547                 if (lastData !== v) {
548                     newdata.unshift(row);
549                     lastData = v;
550                 }
551             }
552         } else {
553             break;
554         }
555     }
556     for (var tick = lastTick; tick > lastData; tick -= tickInterval) {
557         var str = self.toDateString(moment(tick));
558         ticks.unshift(str);
559         grid.unshift({ value: str });
560     }
561     return { data: newdata, tick: ticks, grid: grid };
562 };
563 </script>
564 </sequential-chart>
565
566 <differential-chart>
567 <script>
568 var self = this;
569
570 opts.observable.on("chartSpecChanged", function() {
571     if (opts.chartSpec.type === 1)
572         self.drawChart();
573 });
574
575 opts.observable.on("chartSizeChanged", function() {
576     if (opts.chartSpec.type === 1)
577         self.resize();
578 });
579
580 this.header = ["日付", "燃料", "弾薬", "鋼材", "ボーキ"];
581
582 opts.observable.on("offAllLegends", function() {
583     if (opts.chartSpec.type !== 1)
584         return;
585     self.chart.hide();
586     self.header.slice(1).forEach(function(c) {
587         self.unselected[c] = true;
588     });
589 });
590
591 this.resize = function() {
592     if (!self.chart)
593         return;
594     $('#loading').show();
595     setTimeout(function() {
596         self.chart.resize(self.chartSize());
597     });
598 };
599
600 this.drawChart = function(data) {
601     var range = this.calcRange(opts.chartSpec.diffRange);
602     if (range.last === 0)
603         return;
604     if (!data) {
605         $('#loading').show();
606         $.ajax({
607             url: "./資材ログ.json?number=true" +
608                 "&from=" + range.first + "&to=" + range.last,
609             success: function(d) { self.drawChart(d); },
610             dataType: "json", cache: false
611         });
612         return;
613     }
614     var picked;
615     picked = this.pickChartData(data.data, range);
616     picked.data.unshift(self.header);
617     this.drawDiffChart(picked);
618 };
619
620 this.calcRange = function(range) {
621     var first = 0;
622     var last = (new Date()).valueOf();
623     switch (range) {
624         case 0:
625             first = moment(last).subtract(1, 'months').valueOf();
626             break;
627         case 1:
628             first = moment(last).subtract(3, 'months').valueOf();
629             break;
630         case 2:
631             first = moment(last).subtract(6, 'months').subtract(1, 'weeks').valueOf();
632             break;
633         case 3:
634             break;
635         case 4:
636             var fromDate = $('#chart_from').datetimepicker("getValue");
637             var toDate = $('#chart_to').datetimepicker("getValue");
638             if (fromDate === null || toDate === null)
639                 return {first: 0, last: 0};
640             first = Math.max(first, fromDate.valueOf());
641             last = Math.min(last, toDate.valueOf());
642             break;
643     }
644     return {first: first, last: last};
645 };
646
647 this.unselected = {};
648
649 this.drawDiffChart = function(picked) {
650     var size = this.chartSize();
651     this.chart = c3.generate({
652         bindto: '#chart',
653         size: {
654             height: size.height,
655             width: size.width
656         },
657         data: {
658             x: '日付',
659             rows: picked.data,
660             axes: {
661                 燃料: 'y',
662                 弾薬: 'y',
663                 鋼材: 'y',
664                 ボーキ: 'y'
665             },
666             type: 'bar',
667             groups: [["燃料", "弾薬", "鋼材", "ボーキ"]]
668         },
669         bar: {
670             width: {
671                 ratio: picked.width
672             }
673         },
674         tooltip: {
675             show: opts.chartSpec.tooltip
676         },
677         grid: {
678             x: {
679                 lines: picked.grid
680             },
681             y: {
682                 lines: [
683                     { value: 0 }
684                 ]
685             }
686         },
687         axis: {
688             x: {
689                 type: 'timeseries',
690                 tick: {
691                     rotate: 30,
692                     format: picked.monthly ? "%Y-%m" : "%m-%d %H:%M",
693                     values: picked.tick
694                 }
695             }
696         },
697         legend: {
698             item: {
699                 onclick: function(id) {
700                     self.unselected[id] = !self.unselected[id];
701                     self.chart.toggle(id);
702                 }
703             }
704         },
705         onrendered: function() {
706             $('#loading').hide();
707             opts.observable.trigger("chartRendered");
708         }
709     });
710     self.chart.hide(Object.keys(self.unselected).filter(function(e) {
711         return self.unselected[e];
712     }));
713 };
714
715 this.pickChartData = function(data, range) {
716     var newdata = [];
717     var ticks = [];
718     var grid = [];
719     var first = range.first;
720     var last = range.last;
721     var interval, tickInterval, lastTick;
722     var barWidth;
723     if (first === 0)
724         return this.pickMonthlyChartData(data);
725     if (last <= first + this.oneDay * 2 * 31) {
726         interval = this.oneDay;
727         tickInterval = this.oneDay * 2;
728         lastTick = this.to5am(last);
729         barWidth = 0.3;
730     } else if (last <= first + this.oneDay * 3 * 31) {
731         interval = this.oneDay;
732         tickInterval = this.oneDay * 7;
733         lastTick = this.to5am(last);
734         barWidth = 0.1;
735     } else {
736         interval = this.oneDay * 7;
737         tickInterval = this.oneDay * 28;
738         lastTick = this.to5am(moment(last).day(1).valueOf());
739         barWidth = 0.1;
740         if (last <= first + this.oneDay * 6 * 38) {
741             tickInterval = this.oneDay * 14;
742             barWidth = 0.3;
743         }
744     }
745     var lastDate = lastTick;
746     var prevRow;
747     for (var i = data.length - 1; i >= 0; i--) {
748         var row = data[i];
749         var date = row[0];
750         if (date > first) {
751             if (date <= last) {
752                 if (!prevRow) {
753                     prevRow = row;
754                     continue;
755                 }
756                 if (date <= lastDate) {
757                     var newrow = [lastDate];
758                     for (var r = 1; r < 5; r++) {
759                         newrow.push(prevRow[r] - row[r]);
760                     }
761                     newdata.unshift(newrow);
762                     lastDate = lastDate - interval;
763                     prevRow = row;
764                 }
765             }
766         } else {
767             break;
768         }
769     }
770     if (tickInterval >= this.oneDay * 7)
771         lastTick = moment(lastTick).day(1).hour(5).minute(0).valueOf();
772     for (var tick = lastTick; tick > lastDate; tick -= tickInterval) {
773         ticks.unshift(tick);
774         grid.unshift({ value: tick });
775     }
776     return { data: newdata, tick: ticks, grid: grid, width: barWidth };
777 };
778
779 this.pickMonthlyChartData = function(data) {
780     var newdata = [];
781     var ticks = [];
782     var grid = [];
783     var prevRow;
784     var prevMonth;
785     var row;
786     var date;
787     for (var i = data.length - 1; i >= 0; i--) {
788         row = data[i];
789         if (!prevRow) {
790             prevRow = row;
791             var eom = moment(row[0]).endOf('month');
792             prevRow[0] = eom.valueOf();
793             prevMonth = eom.month();
794             continue;
795         }
796         date = new Date(row[0]);
797         if (prevMonth !== date.getMonth()) {
798             var newrow = [prevRow[0]];
799             for (var r = 1; r < 5; r++)
800                 newrow.push(prevRow[r] - row[r]);
801             newdata.unshift(newrow);
802             ticks.unshift(prevRow[0]);
803             grid.unshift({ value: prevRow[0] });
804             prevRow = row;
805             prevMonth = date.getMonth();
806         }
807     }
808     if (prevRow && date !== prevRow[0]) {
809         newrow = [prevRow[0]];
810         for (r = 1; r < 5; r++)
811             newrow.push(prevRow[r] - row[r]);
812         newdata.unshift(newrow);
813         ticks.unshift(prevRow[0]);
814         grid.unshift({ value: prevRow[0] });
815     }
816     return { monthly: true, data: newdata, tick: ticks, grid: grid, width: 0.5 };
817 };
818 </script>
819 </differential-chart>
820
821 <material-chart>
822 <div show={mainTabs[mainTab] === "資材グラフ"}>
823 <span class="c3-legend-item" id="off-all-legends" style="text-decoration: underline; cursor: pointer; z-index: 10; position: absolute; display: none;" onclick={offAllLegends} >全解除</span>
824 <div id="chart" style="clear: both; margin: 1em;"></div>
825 </div>
826
827 <script>
828 this.mainTab = 0;
829 var self = this;
830
831 opts.observable.on("mainTabChanged", function(idx) {
832     self.update({mainTab: idx});
833     if (self.mainTabs[idx] === "資材グラフ")
834         opts.observable.trigger("chartSpecChanged");
835 });
836
837 opts.observable.on("chartRendered", function() {
838     var legend, offset;
839     if (opts.chartSpec.type === 0) {
840         legend = $(".c3-legend-item-改修資材>text").offset();
841         offset = 80;
842     } else {
843         legend = $(".c3-legend-item-ボーキ>text").offset();
844         offset = 60;
845     }
846     if (legend)
847         $("#off-all-legends").offset({top: legend.top, left: legend.left + offset}).show();
848 });
849
850 this.offAllLegends = function() {
851     opts.observable.trigger("offAllLegends");
852 };
853
854 this.timer = null;
855 $(window).resize(function() {
856     if (self.timer)
857         clearTimeout(self.timer);
858     self.timer = setTimeout(function() {
859         if (self.mainTabs[self.mainTab] === "資材グラフ")
860             opts.observable.trigger("chartSizeChanged");
861         else if (self.mainTabs[self.mainTab] === "戦果")
862             opts.observable.trigger("achivementChartSizeChanged");
863     }, 200);
864 });
865 </script>
866 </material-chart>
867
868 <achivement-table>
869 <div show={mainTabs[mainTab] === "戦果"}>
870 <span style="margin-left: 1em;">期間:&nbsp;</span><select style="width: 7em; margin-bottom: 1em;" name="月" onchange={monthChange}>
871 <option each={m, i in months} value={m}>{m}</option>
872 </select>
873 <table id="achivement_table" class="display compact cell-border">
874 <thead>
875 <tr><th>日付</th><th>戦果</th><th>月毎</th><th>EO</th><th>月毎(EO込)</th></tr>
876 </thead>
877 </table>
878 <div id="achivementChart" style="margin: 1em;"></div>
879 </div>
880
881 <script>
882 this.on("mount", function() {
883     $("#achivement_table").dataTable({
884         destroy: true,
885         deferRener: true,
886         stateSave: true,
887         order: [[0, "desc"]],
888         paging: false,
889         searching: false,
890         info: false,
891         drawCallback: function() {
892             $('#loading').hide();
893         }
894     });
895 });
896
897 var self = this;
898
899 opts.observable.on("mainTabChanged", function(idx) {
900     self.update({mainTab: idx});
901     if (self.mainTabs[self.mainTab] === "戦果")
902         self.updateData();
903 });
904
905 this.months = [];
906 this.selectedIndex = 0;
907
908 this.monthChange = function(event) {
909     this.selectedIndex = event.target.selectedIndex;
910     if (this.selectedIndex === 0) {
911         this.updateData();
912         return;
913     }
914     this.show();
915 };
916
917 this.calcResult = function(data) {
918     this.result = {};
919     var expPerAch = 10000 / 7.0;
920     var dayEo = 0;
921     var endOfMonth = moment(0);
922     var monthExp = 0;
923     var monthEo = 0;
924     var endOfYear = moment(0);
925     var yearExp = 0;
926     var carryOverAch = 0;
927     var carryOverEo = 0;
928     var prevExp = null;
929     var lastDate = moment(0);
930     var lastExp = -1;
931     var nextDate = moment(0);
932     for (var i = 0; i < data.length; i++) {
933         var row = data[i];
934         var date = this.parseDate(row[0]);
935         var exp = row[1] - 0;
936         var eo = row[2] - 0;
937         var isNewYear = date.isSameOrAfter(endOfYear);
938         var isNewMonth = date.isSameOrAfter(endOfMonth);
939         var isNewDate = date.isSameOrAfter(nextDate);
940         if (isNewDate || isNewMonth || isNewYear) {
941             if (lastDate.add(1, 'hours').isSameOrBefore(date)) {
942                 // 2時を過ぎて最初のexpを戦果の計算に使うと、2時をまたいだ出撃の戦果が前日に加算される。
943                 // そこで2時前のexpを使って戦果を計算するが、2時前のexpが正しく出力されていない場合は
944                 // 戦果を正しく計算できない。記録の間隔が1時間以上空いているときは、2時をまたいだ出撃が
945                 // 行われていない可能性が高いので計算には今のexpを使うことにする。
946                 // これは5時基準で出力された過去のデータで、妥当な戦果を計算するために必要な処理である。
947                 lastExp = exp;
948             }
949             if (nextDate.valueOf() !== 0) {
950                 var d = isNewDate ? nextDate.subtract(1, 'days') : endOfMonth;
951                 var m = d.format("YYYY-MM");
952                 if (!this.result[m])
953                     this.result[m] = [];
954                 var perMonth = (lastExp - monthExp) / expPerAch + carryOverAch;
955                 this.result[m].push([
956                     d.format("YYYY-MM-DD"),
957                     ((lastExp - prevExp) / expPerAch).toFixed(1),
958                     perMonth.toFixed(1), dayEo,
959                     (perMonth + monthEo + carryOverEo).toFixed(1)
960                 ]);
961             }
962             prevExp = lastExp === -1 ? exp : lastExp;
963             if (isNewYear) {
964                 endOfYear = date.clone().endOf('year').hour(22).startOf('hour');
965                 if (endOfYear.isSameOrBefore(date))
966                     endOfYear.add(1, 'year');
967                 yearExp = lastExp === -1 ? exp : lastExp;
968                 monthEo = 0;
969             }
970             if (isNewMonth) {
971                 endOfMonth = date.clone().endOf('month');
972                 if (date.date() === endOfMonth.date())
973                     endOfMonth.add(1, 'months').endOf('month');
974                 endOfMonth.hour(22).startOf('hour');
975                 monthExp = lastExp === -1 ? exp : lastExp;
976                 carryOverEo = monthEo * expPerAch / 50000;
977                 carryOverAch = (monthExp - yearExp) / 50000;
978                 monthEo = 0;
979                 m = endOfMonth.format("YYYY-MM");
980                 if (!this.result[m])
981                     this.result[m] = [];
982                 this.result[m].push([endOfMonth.format("YYYY-MM 引継"),
983                     carryOverAch.toFixed(1), carryOverAch.toFixed(1), carryOverEo.toFixed(1), (carryOverAch + carryOverEo).toFixed(1)]);
984             }
985             dayEo = 0;
986             nextDate = date.clone().hour(2).startOf('hour');
987             if (date.hour() >= 2)
988                 nextDate.add(1, 'days');
989             if (nextDate.date() === 1)
990                 nextDate.add(1, 'days');
991         }
992         if (date.isBefore(date.clone().endOf('month').hour(22).startOf('hour'))) {
993             // 月末22時から翌0時までのEOのボーナス戦果は消える。
994             dayEo += eo;
995             monthEo += eo;
996         }
997         lastDate = date;
998         lastExp = exp;
999     }
1000     if (lastDate.isBefore(endOfMonth)) {
1001         var eom = endOfMonth.format("YYYY-MM");
1002         var ave = (perMonth - carryOverAch) / lastDate.date();
1003         var estimate = perMonth + ave * (endOfMonth.date() - lastDate.date());
1004         this.result[eom].push([endOfMonth.format("YYYY-MM-DD 予測"), ave.toFixed(1) + " 平均", estimate.toFixed(1) + " 予測", monthEo + " 合計", (estimate + monthEo).toFixed(1) + " 予測"]);
1005     }
1006 };
1007
1008 this.calcChartData = function() {
1009     this.chartData = {};
1010     for (var month in this.result) {
1011         var data = this.chartData[month] = [];
1012         var result = this.result[month];
1013         var eo = 0;
1014         var d = 0;
1015         data.push(["日付", "戦果", "EO", "月毎"]);
1016         for (var i = 0; i < result.length; i++) {
1017             var row = result[i];
1018             if (row[0].match(/予測/))
1019                 continue;
1020             if (row[0].match(/引継/)) {
1021                 eo = row[3] - 0;
1022                 data.push([0, row[1], row[3], row[4]]);
1023                 continue;
1024             }
1025             d = moment(row[0], "YYYY-MM-DD").date();
1026             eo += row[3];
1027             data.push([d, row[2], eo, row[4]]);
1028         }
1029         var endOfMonth = moment(month, "YYYY-MM").endOf("month").date();
1030         while (d < endOfMonth) {
1031             d++;
1032             data.push([d, null, null, null]);
1033         }
1034     }
1035 };
1036
1037 this.chartSize = function() {
1038     var width = Math.max($(window).width() - 6 * this.pxPerEm, 800);
1039     return {
1040         height: width * 0.4,
1041         width: width
1042     };
1043 };
1044
1045 opts.observable.on("achivementChartSizeChanged", function() {
1046     if (!self.chart)
1047         return;
1048     $('#loading').show();
1049     setTimeout(function() {
1050         self.chart.resize(self.chartSize());
1051     });
1052 });
1053
1054 this.showChart = function(month) {
1055     this.chart = c3.generate({
1056         bindto: "#achivementChart",
1057         size: this.chartSize(),
1058         data: {
1059             x: "日付",
1060             rows: this.chartData[month],
1061             types: {
1062                 戦果: "area",
1063                 EO: "area",
1064                 月毎: "area"
1065             }
1066         },
1067         onrendered: function() { $('#loading').hide(); }
1068     });
1069 };
1070
1071 this.updateData = function(data) {
1072     if (!data) {
1073         $('#loading').show();
1074         $.ajax({
1075             url: "./戦果.json",
1076             success: function(data) {
1077                 self.updateData(data.data);
1078             },
1079             dataType: 'json',
1080             cache: false
1081         });
1082         return;
1083     }
1084     this.calcResult(data);
1085     this.calcChartData();
1086     this.months = Object.keys(this.result).sort(function(a, b) {
1087         if (a === b)
1088             return 0;
1089         if (a < b)
1090             return 1;
1091         return -1;
1092     });
1093     this.update();
1094     this.show();
1095 };
1096
1097 this.show = function() {
1098     if (this.result === undefined){
1099         this.updateData();
1100         return;
1101     }
1102     var dt = $('#achivement_table').DataTable();
1103     dt.clear();
1104     dt.rows.add(this.result[this.months[this.selectedIndex]]).draw();
1105     this.showChart(this.months[this.selectedIndex]);
1106 };
1107 </script>
1108 </achivement-table>
1109
1110 <sortie-stat>
1111 <div show={mainTabs[mainTab] === "出撃統計"}>
1112
1113 <ul class="tab tabsub" style="float: left; margin-right: 0.2em">
1114 <li each={tabs} class={select: parent.type === type} onclick={parent.changeTab}>{label}</li>
1115 </ul>
1116
1117 <div style="padding: 0.2em 0;">
1118 <input type="text" id="sortie_stat_from" style="width: 10em">~<input type="text" id="sortie_stat_to" style="width: 10em">
1119 </div>
1120
1121 <div style="clear: both;" show={type === "recent"}>
1122 <h3>今日</h3>
1123 <table id="sortie_stat_day">
1124 </table>
1125 <h3>今週</h3>
1126 <table id="sortie_stat_week">
1127 </table>
1128 <h3>今月</h3>
1129 <table id="sortie_stat_month">
1130 </table>
1131 </div>
1132
1133 <div show={type === "range"}>
1134 <table id="sortie_stat_all" style="width: 100%;">
1135 </table>
1136 </div>
1137
1138 </div>
1139
1140 <script>
1141 this.tabs = [
1142     {
1143         type: "recent",
1144         label: "直近"
1145     },
1146     {
1147         type: "range",
1148         label: "期間指定"
1149     }
1150 ];
1151 this.type = "recent";
1152 this.changeTab = function(e) {
1153     this.type = e.item.type;
1154     this.show();
1155 }.bind(this);
1156
1157 this.mainTab = 0;
1158 var self = this;
1159
1160 this.on("mount", function() {
1161     $("[id^=sortie]").addClass('display compact cell-border');
1162     this.init();
1163 });
1164
1165 opts.observable.on("mainTabChanged", function(idx) {
1166     self.update({mainTab: idx});
1167     if (self.mainTabs[self.mainTab] === "出撃統計")
1168         self.show();
1169 });
1170
1171 this.init = function() {
1172     self.initPicker('#sortie_stat_from', '#sortie_stat_to', function() {
1173         if (self.type === "range")
1174             self.show();
1175     });
1176 };
1177
1178 var self = this;
1179
1180 this.loadData = function() {
1181     var from, to;
1182     if (this.type === "recent") {
1183         from = moment().subtract(1, 'months').subtract(1, 'day').valueOf();
1184         to = new Date().valueOf();
1185     } else {
1186         var fromDate = $('#sortie_stat_from').datetimepicker("getValue");
1187         var toDate = $('#sortie_stat_to').datetimepicker("getValue");
1188         if (fromDate === null || toDate === null) {
1189             this.show([]);
1190             return;
1191         }
1192         from = fromDate.valueOf();
1193         to = toDate.valueOf();
1194     }
1195     $.ajax({
1196         url: "./海戦・ドロップ報告書.json?from=" + from + "&to=" + to,
1197         success: function(data) { self.show(data.data); },
1198         dataType: "json", cache: false
1199     });
1200 };
1201
1202 this.initResult = function() {
1203     var now = moment();
1204     var r;
1205     if (this.type === "recent") {
1206         r = {
1207             day: { stat: {} },
1208             week: { stat: {} },
1209             month: { stat: {} }
1210         };
1211         r.day.begin = moment(now).hour(5).minute(0).second(0);
1212         if (now.hour() < 5) {
1213             r.day.begin.subtract(1, 'days');
1214         }
1215         r.week.begin = moment(now).day(1).hour(5).minute(0).second(0);
1216         if (now.day() === 0 || now.day() === 1 && now.hour() < 5) {
1217             r.week.begin.subtract(1, 'weeks');
1218         }
1219         if (moment(now).endOf('month').date() === now.date() &&
1220             now.hour() >= 22) { // 月末22時以降
1221             r.month.begin = moment(now).hour(22).minute(0).second(0);
1222         } else {
1223             r.month.begin =
1224                 moment(now).date(1).subtract(1, 'days').
1225                     hour(22).minute(0).second(0);
1226         }
1227     } else {
1228         r = { all: { stat: {} } };
1229         r.all.begin = moment(0);
1230     }
1231     return r;
1232 };
1233
1234 this.gatherData = function(data) {
1235     var initStat = function() {
1236         return { start: "-", S: 0, A: 0, B: 0, C: 0, D: 0, R: 0 };
1237     };
1238     var r = this.initResult();
1239     for (var i = 0; i < data.length; i++) {
1240         var row = data[i];
1241         var date = moment(row[0]);
1242         var map = row[1];
1243         var isBoss = row[3].indexOf("ボス") !== -1;
1244         var isStart = row[3].indexOf("出撃") !== -1;
1245         var resR = 0;
1246         for (var j = 22; j < row.length; j++) {
1247             if (/^輸送/.test(row[j]) && /^0\x2f/.test(row[j + 1]))
1248                 resR++;
1249         }
1250         var item = /アイテム/.test(row[9]) ? /[^+]+$/.exec(row[10])[0] : null;
1251         var res = row[4];
1252         if (res === "E")
1253             res = "D";
1254         for (var term in r) {
1255             if (!r.hasOwnProperty(term))
1256                 continue;
1257             var to = r[term];
1258             if (to.begin.isAfter(date))
1259                 continue;
1260             for (var b = 0; b < 4; b++) {
1261                 var name = b < 2 ? "合計" : map;
1262                 if (b === 1 || b === 3) {
1263                     if (!isBoss)
1264                         continue;
1265                     name = name + " - ボス";
1266                 }
1267                 var mo = to.stat[name];
1268                 if (!mo) {
1269                     mo = to.stat[name] = initStat();
1270                     if (name === "合計")
1271                         to.stat["合計 - ボス"] = initStat();
1272                 }
1273                 if ((b === 0 || b === 2) && isStart) {
1274                     if (mo.start === "-")
1275                         mo.start = 0;
1276                     mo.start++;
1277                 }
1278                 if (/^基地航空隊/.test(row[11]))
1279                     continue;
1280                 mo["R"] += resR;
1281                 mo[res]++;
1282                 if (item) {
1283                     if (!mo[item])
1284                         mo[item] = 0;
1285                     mo[item]++;
1286                 }
1287             }
1288         }
1289     }
1290     return r;
1291 };
1292
1293 this.isItemColumn = function(col) {
1294     return !/^(?:map|start|[SABCDR])$/.test(col);
1295 };
1296
1297 this.sortItemOrder = function(items) {
1298     ["お米", "梅干", "海苔", "お茶"].reverse().forEach(function(item) {
1299         var idx = items.indexOf(item);
1300         if (idx !== -1) {
1301             items.splice(idx, 1);
1302             items.unshift(item);
1303         }
1304     });
1305 };
1306
1307 this.setupColumns = function(r) {
1308     for (var term in r) {
1309         var columns = [{ data: "map", title: "マップ" },
1310                        { data: "start", title: "出撃" },
1311                        { data: "S", title: "S" },
1312                        { data: "A", title: "A" },
1313                        { data: "B", title: "B" },
1314                        { data: "C", title: "C" },
1315                        { data: "D", title: "D" },
1316                        { data: "R", title: "輸送船" }];
1317         if (term === "month")
1318             columns.pop();
1319         var items = [];
1320         for (var col in r[term].stat["合計"]) {
1321             if (this.isItemColumn(col))
1322                 items.push(col);
1323         }
1324         this.sortItemOrder(items);
1325         items.forEach(function(item) {
1326             columns.push({data: item, title: item});
1327         });
1328         r[term].columns = columns;
1329     }
1330 };
1331
1332 this.fillupItemRecords = function(r) {
1333     for (var term in r) {
1334         for (var col in r[term].stat["合計"]) {
1335             if (!this.isItemColumn(col))
1336                 continue;
1337             for (var map in r[term].stat) {
1338                 if (map === "合計")
1339                     continue;
1340                 if (!r[term].stat[map][col]){
1341                     r[term].stat[map][col] = 0;
1342                 }
1343             }
1344         }
1345     }
1346 };
1347
1348 this.reorderRows = function(r) {
1349     for (var term in r) {
1350         if (!r.hasOwnProperty(term))
1351             continue;
1352         var table = [];
1353         var pushed = {};
1354         for (var map in r[term].stat) {
1355             if (!r[term].stat.hasOwnProperty(map))
1356                 continue;
1357             if (pushed[map])
1358                 continue;
1359             var e = r[term].stat[map];
1360             e.map = map;
1361             table.push(e);
1362             pushed[map] = 1;
1363             var boss = map + " - ボス";
1364             e = r[term].stat[boss];
1365             if (!e)
1366                 continue;
1367             e.map = boss;
1368             table.push(e);
1369             pushed[boss] = 1;
1370         }
1371         r[term].table = table;
1372     }
1373 };
1374
1375 this.show = function(data) {
1376     if (!data) {
1377         $('#loading').show();
1378         this.loadData();
1379         return;
1380     }
1381     var r = this.gatherData(data);
1382     this.setupColumns(r);
1383     this.fillupItemRecords(r);
1384     this.reorderRows(r);
1385     for (var term in r) {
1386         var table = $("#sortie_stat_" + term);
1387         if ($.fn.dataTable.isDataTable(table))
1388             table.DataTable().destroy();
1389         table.html("<thead><tr>" +
1390                    r[term].columns.reduce(function(acc, cur) {
1391                        return acc + "<th>" + cur.title + "</th>";
1392                    }, "") + "</tr></thead>");
1393         table.DataTable({
1394             paging: false,
1395             searching: false,
1396             ordering: false,
1397             columns: r[term].columns,
1398             data: r[term].table
1399         });
1400     }
1401     $('#loading').hide();
1402 };
1403 </script>
1404 </sortie-stat>