OSDN Git Service

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