PageSourceSearch

https://kangyou-siken.com/js/kakomonApp.js?20260927

js kangyou-siken.com collected 2026-10-04 13:21:30 UTC 74,751 bytes, 2,320 lines download raw bytes

1const
2adminFormHtml =
3`<form id="adminForm">
4<ul>
5<li><a id="history"><i class="history"></i>学習履歴を見る</a>
6<li><a id="coverage"><i class="coverage"></i>達成度を見る</a>
7<li><a id="testgroup"><i class="testgroup"></i>試験回別の成績を見る</a>
8<li><a id="progress"><i class="progress"></i>学習の推移を見る</a>
9<li><a id="review"><i class="review"></i>間違えた問題を復習する</a>
10<li><a id="unanswer"><i class="unanswer"></i>未回答問題だけを出題する</a>
11<li><a id="bookmark"><i class="bookmark"></i>チェックした問題を出題する</a>
12<li><a id="bookmark_edit"><i class="bookmark_edit"></i>チェックした問題の管理</a>
13<li class="not-membership-menu"><a href="/membership/" id="membership_register"><i class="membership"></i>メンバーシップに登録する</a>
14<li class="membership-menu"><a href="/membership/tokuten/" target="_blank"><i class="tokuten"></i>メンバーシップ特典</a>
15<li><a id="account"><i class="account"></i>アカウント管理</a>
16<li><a id="logout">ログアウト</a>
17</ul>
18</form>`;
19
20$(function(){
21var
22mode = null,
23maxQcount = function(){ return 50 };
24
25$('body').append('<output id="stateMessage" role="state"></output><div id="overLayerWrap"><div id="overLayer"></div></div>');//closeBtnの関係でoverlayerを後にする
26
27//トップページのみ前回の選択数と前回の選択分野を自動適用
28if ($('#doujoulogo')[0]) {
29	localStorage.removeItem("CBT");
30	
31	const checkCat = localStorage.getItem("CHECK_CATEGORIES");
32	if (checkCat) {
33		$('[name="categories[]"]').val(checkCat.split(','));
34	}
35	const checkTime = localStorage.getItem("CHECK_TIMES");
36	if (checkTime) {
37		$('[name="times[]"]').val(checkTime.split(','));
38	}
39}
40
41$('#tabs > div').hide();
42if (isReview() || isUnanswer() || isBookmark()) {
43	//タブ部分にメッセージとボタンを追加
44	let mes1, mes2;
45	if (isReview()) {
46		mes1 = '学習履歴内の復習';
47		mes2 = '復習';
48	} else if (isUnanswer()) {
49		mes1 = '未回答問題の出題';
50		mes2 = '未回答';
51	} else if (isBookmark()) {
52		mes1 = 'チェック問題の出題';
53		mes2 = '復習';
54	}
55	$('#tabs').append('<div class="content" style="display:block">現在、'+mes1+'中です。'+mes2+'モードを終了するには試験回指定・分野指定等の各タブを選択してから「次の問題」ボタンを押してください。</div>');
56
57	//直前の選択状態を復元
58	$('#tabs > ul:not(:has(.active)) a').on('click.restoreSelect', function() {
59		const checkCat = localStorage.getItem("CHECK_CATEGORIES");
60		if (checkCat) {
61			$('#configform [name="categories[]"]').val(checkCat.split(','));
62			updateSmallCategoryCount();
63		}
64		$('#tabs > ul a').off('click.restoreSelect');
65		return false;
66	})
67} else {
68	if ($('#tabs li.active a')[0]) {
69		mode = $('#tabs li.active a').attr('href').replace(/#tab/, '');
70		$('#tab'+mode).show();
71	}
72}
73if (isMoshi()) {
74	const mes = '模擬試験モード中のためオプション・';
75	const btn = '模擬試験モードを終了する';
76	$('#tabs').hide().before('<div id="reviewWrap" class="content">※現在、'+mes+'出題範囲の指定ができません。<div class="img_margin"><button type="button" id="review_end">'+btn+'</button></div></div>');
77
78	$('#configform').on('click', '#review_end', function(){
79		if (isMoshi()) {
80			$('#tabs>ul>li.active').remove();
81			$('#tabs>ul>li').first().addClass('active');
82			$('#tab1').show();
83		}
84		$('#reviewWrap').remove();
85		$('#tabs').fadeIn('fast');
86	})
87}
88$('#tabs>ul a[href="#tab4"]').click(function(){
89	if (!$('#tab4').html()) {
90		var matrix = {};
91		var q = $('#configform [name="q"]').val().split(',');
92		var r = ($('#configform [name="r"]').val() + ',' + $('#result').val()).split(',');
93
94		for (var len=q.length,i=len-1; i>=0; i--) {
95			if (!q[i]) continue;
96			//直前の1問のみを記録(_r[i]の型チェックを入れると未回答問題が追加されてしまう)
97			if (matrix[q[i]] === undefined && r[i] != 0) {
98				matrix[q[i]] = r[i];
99			}
100		}
101		var reviewArr = [];
102		for (var key in matrix) {
103			if (matrix[key] == 2) {
104				reviewArr.push(key);
105			}
106		}
107		reviewArr.reverse();
108		var reviewCount = reviewArr.length;
109		var html = '';
110		if (reviewCount === 0) {
111			html = '見直し対象問題はありません。';
112		} else {
113			html = '見直し対象問題数は<b>'+reviewCount+'</b>問です。<br>';
114			var dispMax = 20;
115			for (i=0; i<=reviewCount-1; i++) {
116				if (i == dispMax) {
117					html += '<br>その他 '+(reviewCount-dispMax)+'問';
118					break;
119				}
120				var qidObj = splitQid(reviewArr[i]);
121				html += '<span>'+AD2year_wa(getDirname(qidObj.eid))+' 問'+qidObj.q+'</span>';
122			}
123		}
124		$('#tab4').html('<div class="content">'+html+'</div>');
125	}
126})
127if (mode == 4) {
128	$('#tabs>ul a[href="#tab4"]').click();
129}
130$('button[name="check_all"]').click(function(){
131	const f = $(this).html() == "ON";
132	const isTimes = $(this).is('#tab1 [name="check_all"]');
133	$('[type="checkbox"]', $(this).parent().next()).prop('checked', f);
134	updateSmallCategoryCount();
135
136	if (isTimes) {
137		updateCheckTime();
138	} else {
139		updateCheckCat();
140	}
141})
142$('button[name="recommend"]').click(function(){
143	const last12 = getAllTimes().slice(-12);
144	$('.input_times [type="checkbox"]').val(last12);
145	updateSmallCategoryCount();
146	updateCheckTime();
147})
148$('input[name="fields[]"], .input_categories>dt>.count').click(function(e){
149	let cb, f;
150	if ($(this).is('.count')) {
151		cb = $(this).prev();
152		f = !cb.prop("checked");
153	} else {
154		cb = $(this);
155		f = cb.prop("checked");
156	}
157	const dd = $(this).parent().next();
158	$('[type="checkbox"]', dd).prop('checked', f);
159
160	updateBigCategoryCount(cb.val());
161	updateCheckCat();
162
163	e.stopPropagation();
164})
165$('input[name="categories[]"]').change(function(){
166	updateBigCategoryCount($(this).val()[0]);
167	updateCheckCat();
168})
169$('input[name="times[]"]').change(function(){
170	updateSmallCategoryCount();
171	updateCheckTime();
172})
173function updateSmallCategoryCount(){
174	const times = $('input[name="times[]"]:checked').map(function(){
175		return $('input[name="times[]"]').index($(this));
176	}).get();
177	const len = times.length;
178
179	$('input[name="categories[]"]').each(function(){
180		const dataCount = $(this).attr('data-count');
181		let cnt = 0;
182		for (let i=0; i<len; i++) {
183			cnt += Number(dataCount.charAt(times[i]));
184		}
185		$(this).siblings('.count').text(cnt);
186	})
187	for (let i=1; i<=bunyalen; i++) {
188		updateBigCategoryCount(i);
189	}
190}
191function updateBigCategoryCount(idx){
192	var count = 0, checkflag = false;
193	var dt = $('.input_categories dt').eq(idx-1);
194	$('[type="checkbox"]', dt.next()).each(function(){
195		var elem = $(this).nextAll('.count');
196		if ($(this).prop('checked')) {
197			checkflag = true;
198			count += Number(elem.text());
199		}
200	});
201	$('.count', dt).text(count);
202	$('[type="checkbox"]', dt).prop("checked", checkflag);
203}
204function updateCheckCat() {
205	const checkCat = $('input[name="categories[]"]:checked').map(function(){
206		return $(this).val();
207	}).get();
208
209	if (checkCat.length) {
210		localStorage.setItem("CHECK_CATEGORIES", checkCat.join(','));
211	} else {
212		localStorage.removeItem("CHECK_CATEGORIES");
213	}
214}
215function updateCheckTime() {
216	const checkTime = $('input[name="times[]"]:checked').map(function(){
217		return $(this).val();
218	}).get();
219
220	if (checkTime.length) {
221		localStorage.setItem("CHECK_TIMES", checkTime.join(','));
222	} else {
223		localStorage.removeItem("CHECK_TIMES");
224	}
225}
226updateSmallCategoryCount();
227
228//オプションチェックボックスの同期
229$('.options [type="checkbox"]').change(function(){
230	$('.options [value="'+$(this).val()+'"]').prop('checked', $(this).prop('checked'));
231})
232$('body').on('click', '.sendConfigform', function(){
233	if (!validateConfigform()) return false;
234	
235	$('#configform').submit();
236})
237function validateConfigform(){
238	function showError(mes) {
239		$('#configform').show();
240		$('.error', $('#configform')).html(mes).fadeIn('fast');
241		$('html,body').animate({ scrollTop: $('#tabs').offset().top-40 }, 'normal');
242	}
243	$('.error', $('#configform')).html('').hide();
244	
245	if ($('#tabs .active')[0]) {
246		mode = $('#tabs .active a').attr('href').replace(/#tab/, '');
247		$('[name="mode"]', $('#configform')).val(mode);
248	}
249	if ((mode == 1 || mode == 2) && ($('#tab1 .input_times input:checked').length === 0 || $('#tab2 .input_categories input:checked').length === 0)) {
250		showError('※試験回と分野を各1つ以上チェックしてください。');
251		return false;
252	}
253	if (mode == 4 && !$('#tab4 span')[0]) {
254		showError('※見直し対象問題がありません。<br>試験回または分野で出題範囲を指定してください。');
255		return false;
256	}
257	if (mode == 3 && $('[value="harf"]').prop('checked') && !$('#loginBtn.member')[0]) {
258		showError('模擬試験の問題数指定はメンバー限定の機能です。<br>出題開始するにはメンバーとしてログインするかチェックをはずしてください。');
259		return false;
260	}
261	return true;
262}
263$("#config").click(function(){
264	if ($('#configform').is(':visible')) {
265		$(this).html('<i class="config"></i>出題設定');
266		$('#configform').hide();
267	} else {
268		$(this).html('▲閉じる');
269		$('#configform').show();
270	}
271	return false;
272})
273$('#pdf_print, #pdf_download').click(function(){
274	var form = $('<form></form>', {
275		'action' : './generate_test.php',
276		'method' : 'post',
277		'target' : '_blank'
278	});
279	var compo = '';
280	compo += '<input type="hidden" name="q" value="'+$(this).attr('id').split('_').pop()+'">';
281	compo += '<input type="hidden" name="moshi" value="'+$('#configform [name="moshi"]:checked').val()+'">';
282	$('#tab3 [name="options[]"]:checked').each(function(){
283		compo += '<input type="hidden" name="'+$(this).val()+'" value="1">';
284	})
285	compo += '<input type="hidden" name="moshi_cnt" value="'+$('#configform [name="moshi_cnt"]').val()+'">';
286	form.html(compo).appendTo($('body')).submit();
287})
288
289var ranking = '';
290function loadUserlog($i){
291	var dfd = new $.Deferred();
292	if (!$.isEmptyObject(userlog)) {
293		dfd.resolve();
294		return dfd.promise();
295	}
296	
297	const iClass = $i.attr('class');
298	$i.attr('class', 'loading large');
299
300	var inputParams = {
301		"action" : 'load'
302	};
303	$.ajax({
304		type : "POST",
305		url : ajaxScript,
306		cache : false,
307		data : $.param(inputParams),
308		dataType : "json"})
309	.done(function(data){
310		if (data.status == "success") {
311			userlog = data.log;
312			ranking = data.ranking;
313			dfd.resolve();
314		} else if (data.status == "error") {
315			switch (data.errorcode) {
316			case 1: showStateMessage('<i class="caution"></i>まだ学習履歴が1問も記録されていません。'); break;
317			case 2: showStateMessage('<i class="ng"></i>エラーにより学習履歴を取得できませんでした。'); break;
318			case 3: showStateMessage('<i class="ng"></i>時間経過によりログイン状態が切れています。再ログインしてお試しください。'); break;
319			}
320			dfd.reject();
321		}
322	}).fail(function(){
323		dfd.reject();
324	}).always(function(){
325		$i.attr('class', iClass);
326	});
327	return dfd.promise();
328}
329$('#showReport').click(function() {
330	history();
331	return false;
332})
333$('body').on('click', '#history', function(){
334	if (!uid || !$('#loginBtn').hasClass('login')) {
335		showStateMessage('<i class="caution"></i>不正なリクエストです。');
336		return false;
337	}
338	const $i = $('i', $(this));
339	loadUserlog($i).done(function(){
340		history('all');
341	});
342	return false;
343})
344$('#overLayer').on('change', '[name="targetDate"]', function(){
345	if ($(this).val() !== "custom") {
346		$('.customDateWrap').hide();
347		history($(this).val());
348	} else {
349		$('.rankingStr').hide();
350		$('.customDateWrap').fadeIn('fast');
351	}
352})
353$('#overLayer').on('click', '#custom_ok', function(){
354	history("custom");
355})
356async function history(target) {
357	var i, len, d = [], report = '';
358
359	if (!target) {
360		var _q = $('#configform [name="q"]').val().split(','),
361			_r = $('#configform [name="r"]').val().split(','),
362			_c = $('#configform [name="c"]').val().split(',');
363		if (!isErrorPage()) {
364			_q.pop();
365			_c.pop();
366		}
367		var time = Math.floor(Date.now() / 1000);
368		for (i=0,len=_q.length; i<len; i++) {
369			d.push(time+','+_q[i]+','+_r[i]+','+_c[i]);
370		}
371	} else {
372		d = narrowData(target);
373	}
374
375	if (d.length === 0) {
376		report = '<div id="reportWrap" style="height:650px">'+emptySVG('この期間のデータはありません')+'</div>';
377	} else {
378		const q2c = await getQ2c();
379		//大分類ごとの出題数・正解数・正解率を集計する多次元配列
380		var bData = [];
381		for (j=0; j<=2; j++) {
382			bData[j] = [];
383			for (i=0; i<=bunyalen; i++) {
384				bData[j].push(0);
385			}
386		}
387		//中分類ごとの出題数・正解数・正解率を集計する多次元配列
388		var mData = [];
389		for (k=1; k<=bunyalen; k++) {
390			mData[k] = [];
391			for (j=0; j<=2; j++) {
392				mData[k][j] = [];
393				len = bunyalist[k].length;
394				for (i=1; i<len; i++) {
395					mData[k][j][i] = 0;
396				}
397			}
398		}
399
400		bData[0][0] = d.length;
401		bData[1][0] = $.grep(d, function(val){ return val.indexOf(',1,') !== -1; }).length;
402		bData[2][0] = (bData[1][0] / bData[0][0] * 100).toFixed(1);
403
404		var qdata, qid, categoryId, midx, sidx;
405		for (i=0,len=d.length; i<len; i++){
406			if (!d[i]) continue;
407			qdata = d[i].split(',');
408			qid = qdata[1];
409			categoryId = q2c[qid].split('_');
410			midx = Number(categoryId[0]);
411			sidx = Number(categoryId[1]);
412			bData[0][midx]++;
413			mData[midx][0][sidx]++;
414			if (qdata[2] === "1"){
415				bData[1][midx]++;
416				mData[midx][1][sidx]++;
417			}
418		}
419		//分野ごとの正解率を計算、分野ごとのHTMLを生成
420		var overviewHtml = '', resultClass, percentage;
421		for (i=1; i<=bunyalen; i++) {
422			if (bData[0][i] > 0) {
423				bData[2][i] = (bData[1][i] / bData[0][i] * 100).toFixed(1);
424				percentage = bData[2][i]+'<span class="unit">%</span>';
425			} else {
426				percentage = '';
427			}
428			resultClass = (bData[0][i] > 0) ? (bData[2][i] >= passRate) ? ' over' : ' under' : '';
429			overviewHtml += '<dt>'+bunyalist[i][0]+'<dd><p>'+bData[0][i]+'問中 '+bData[1][i]+'問正解</p><p class="percentage'+resultClass+'">'+percentage+'</p>';
430			try {
431				overviewHtml += '<input type="checkbox" name="more_check" id="more_check'+i+'" class="more_check"><label for="more_check'+i+'" class="more_btn"></label><div class="more_table_wrap">';
432				overviewHtml += '<table class="more_table"><tr><th>細目<th>正解/出題<th>正解率';
433				var sub_percentage, n1, n2;
434				for (j=1; j<bunyalist[i].length; j++) {
435					if (mData[i][0][j]) {
436						n1 = mData[i][0][j];
437						n2 = mData[i][1][j];
438					} else {
439						n1 = n2 = 0;
440					}
441					if (n1 > 0) {
442						sub_percentage = (Math.round(n2 / n1 * 1000) / 10)+'%';
443					} else {
444						sub_percentage = '-';
445					}
446					midx = i;
447					sidx = j;
448					overviewHtml += '<tr><td>'+bunyalist[midx][sidx]+'<td>'+n2+' / '+n1+'<td>'+sub_percentage;
449				}
450				overviewHtml += '</table></div>';
451			} catch (e) {
452				console.log(e.message);
453			}
454		}
455		//各問のデータを列挙
456		var detailRow = '', processingDate = '', currentDate = '', span_open = '', span_end = '';
457		historyTableRow = [];
458		for (i=0,len=d.length; i<len; i++){
459			if (!d[i]) continue;
460			qdata = d[i].split(',');
461			span_open = span_end = '';
462			processingDate = new Date(qdata[0]*1000).toyyyymmdd();
463			if (processingDate > currentDate) {
464				currentDate = processingDate;
465				span_open = '<span class="dem" data-date="'+currentDate.slice(0,4)+'.'+Number(currentDate.slice(4,6))+'.'+Number(currentDate.slice(6,8))+'">';
466				span_close = '</span>';
467			}
468			qid = qdata[1];
469			var qidObj = splitQid(qid);
470			var dirname = getDirname(qidObj.eid), q = qidObj.q;
471			var year_wa = AD2year_wa(dirname, true);
472			categoryId = q2c[qid].split('_');
473			midx = Number(categoryId[0]);
474			sidx = Number(categoryId[1]);
475			var bunya = bunyalist[midx][sidx];
476			resultClass = (qdata[2] === "1") ? 'maru' : (qdata[2] == 2) ? 'batu' : 'un';
477			detailRow += '<tr><td>'+span_open+(i+1)+span_end+'<td><a href="/kakomon/'+dirname+'/'+('0'+q).slice(-2)+'.html" target="_blank">'+year_wa+' 問'+q+'</a><td>'+bunya+'<td><i class="'+resultClass+'"></i>';
478			if (i % 100 == 99) {
479				historyTableRow.push(detailRow);
480				detailRow = '';
481			}
482		}
483		historyTableRow.push(detailRow);
484		const detailHtml = historyTableRow.shift();
485
486		var rankingStr = '';
487		if (target && target != 'custom' && ranking && /^[0-9]+\/[0-9]+$/.test(ranking)) {
488			var ranknum_userCount = ranking.split('/');
489			var ranknum = Number(ranknum_userCount[0]) || '- ';
490			var userCount = Number(ranknum_userCount[1]);
491			var crown = '';
492			if (ranknum <= 3) {
493				crown = '<i class="rank'+(4 - ranknum)+'" style="transform:scale(1.8);transform-origin:center bottom;margin-right:1em"></i>';
494			}
495			rankingStr = '<div class="rankingStr">'+crown+'総合ランキング <span><span>'+ranknum+'</span> 位</span> /'+userCount.toLocaleString("ja-JP")+'人中</div>';
496		}
497
498		var ls = '';
499		if (bData[0][0] >= 10000) {
500			ls = ' style="letter-spacing:0"';
501		}
502
503		report =
504`<div id="reportWrap">
505<style>
506.more_check{display:none}
507.more_btn{cursor:pointer;position:absolute;width:22px;height:22px;bottom:100%;right:0;border-radius:50%;user-select:none}
508.more_btn:hover{box-shadow:0 0 4px rgba(var(--pure-black), 0.3)}
509.more_btn::before, .more_btn::after{content:"";position:absolute;width:10px;height:2px;top:50%;left:6px;margin-top:-1px;background:#999;transition:0.4s}
510.more_btn::after{transform:rotate(90deg)}
511.more_check:checked + .more_btn::after{opacity:0;transform:rotate(360deg)}
512.more_table_wrap{width:100%;position:absolute;left:103%;top:0;z-index:1;transform:translateY(-50%) scale(1, 0);transition:transform 0.6s var(--spring-snappy)}
513.more_check:checked + .more_btn + .more_table_wrap{transform:translateY(-50%) scale(1, 1)}
514.more_table{width:100%;font-size:12px;background:var(--main-bg);border:1px solid var(--color-eee);box-shadow:4px 4px 8px rgba(var(--pure-black),0.1)}
515.more_table tr:nth-child(odd){background:var(--main-bg-sub)}
516.more_table th, .more_table td{white-space:nowrap;padding:5px 8px}
517.more_table td{color:var(--color-444)}
518.more_table::before, .more_table::after{content:"";display:block;position:absolute;width:0;height:0;border:8px solid transparent;border-right-color:var(--color-eee);top:calc(50% - 1.75em);right:100%}
519.more_table::after{border-right-color:var(--main-bg);right:calc(100% - 1px)}
520@media (max-width:770px){
521.more_table_wrap{position:static;height:0;overflow:hidden}
522.more_check:checked + .more_btn + .more_table_wrap{height:auto;padding:10px 0;transform:none}
523.more_table{box-shadow:none}
524.more_table::before, .more_table::after{display:none}
525}
526</style>
527<div class="overview">
528<h3>概要</h3>
529<div class="content">
530<dl><dt class="total"${ls}><span class="unit">🏆総合: </span>${bData[0][0]}<span class="unit">問中</span>${bData[1][0]}<span class="unit">問正解</span>
531<p id="canvas_percentage" data-percentage="${bData[2].join(',')}"><span id="NoP"></span><span class="unit">%</span></p>
532</dt></dl>${rankingStr}
533<div id="raderChart">
534<canvas id="canvas_rader_front" width="560" height="420"></canvas>
535<canvas id="canvas_rader_back" width="560" height="420"></canvas>
536</div>
537</div>
538<div class="content">
539<dl>${overviewHtml}</dl>
540</div>
541</div>
542<div class="detail">
543<div id="csv-download" style="float:right;font-size:0.75rem;cursor:pointer"><span class="material-symbols-outlined" style="font-size:20px">download</span>CSV形式でダウンロード</div>
544<h3>詳細</h3>
545<div class="historyTableWrap" style="clear:both"><table><tr class="h"><td>#<td>出典<td>分野<td>結果${detailHtml}</table></div>
546</div>
547</div>`;
548	}
549	if ($('body').hasClass('modal')) {
550		$('#reportWrap').fadeOut(300, function(){
551			$(this).replaceWith(report).show();
552			tableLazyLoad();
553			updateGraph();
554		});
555	} else {
556		const h2 = isMoshi() ? '模擬試験結果' : '学習履歴分析';
557		const header = '<h2>'+h2+'</h2>';
558		let selectHtml = '';
559		if (target) {
560			const today = new Date().toyyyymmdd(true);
561
562			const lastYear = new Date();
563			lastYear.setFullYear(lastYear.getFullYear() - 1);
564
565			const savedStVal = localStorage.getItem('CUSTOM_START_DATE');
566			const savedEdVal = localStorage.getItem('CUSTOM_END_DATE');
567			const stVal = isValidDateString(savedStVal) && savedStVal <= today
568					? savedStVal
569					: lastYear.toyyyymmdd(true);
570			const edVal = isValidDateString(savedEdVal) && savedEdVal <= today
571					? savedEdVal
572					: today;
573
574			selectHtml =
575`<div class="datePicker">
576現在の期間 &raquo; <select name="targetDate">
577<option value="all">全期間
578<option value="today">今日(現時点まで)
579<option value="yesterday">昨日
580<option value="thisMonth">今月(現時点まで)
581<option value="lastMonth">先月
582<option value="past7days">過去7日間
583<option value="past30days">過去30日間
584<option value="past90days">過去90日間
585<option value="past180days">過去180日間
586<option value="past1years">過去1年間
587<option value="custom">カスタムの期間
588</select>
589<div class="customDateWrap" style="display:none">
590<input type="date" name="startDate" min="2017-01-01" max="${today}" value="${stVal}"> ~ 
591<input type="date" name="endDate" min="2017-01-01" max="${today}" value="${edVal}"> <button id="custom_ok">表示</button>
592</div>
593</div>`;
594		}
595		showModal(header+selectHtml+report);
596		tableLazyLoad();
597		updateGraph();
598	}
599}
600$('body').on('click', '#testgroup', function(){
601	if (!uid || !$('#loginBtn').hasClass('login')) {
602		showStateMessage('<i class="caution"></i>不正なリクエストです。');
603		return false;
604	}
605	const $i = $('i', $(this));
606	loadUserlog($i).done(function(){
607		testgroup();
608	});
609	return false;
610})
611$('#overLayer').on('change', '[name="targetTest"]', function(){
612	testgroup($(this).val());
613})
614async function testgroup(target) {
615	var results = [];
616	var arr = getAllTimes().reverse();
617	target = target || arr[0]; //targetが空なら最新試験
618
619	const q2c = await getQ2c();
620
621	var i, len, d = [];
622	d = narrowData(target);
623	//分野ごとのデータを集計 //0要素目は全成績用 1~bunyalenは分野ごとの成績
624	var bData = [];
625	for (j=0; j<=2; j++) {
626		bData[j] = [];
627		for (i=0; i<=bunyalen; i++) {
628			bData[j].push(0);
629		}
630	}
631
632	bData[0][0] = d.length;
633	bData[1][0] = $.grep(d, function(val){ return val.indexOf(',1,') !== -1; }).length;
634	bData[2][0] = (bData[1][0] / bData[0][0] * 100).toFixed(1) || '-';
635
636	for (i=0,len=d.length; i<len; i++){
637		if (!d[i]) continue;
638		var qdata = d[i].split(',');
639		var qid = qdata[1];
640		var categoryId = q2c[qid];
641		var midx = categoryId.split('_')[0];
642
643		bData[0][midx]++;
644		if (qdata[2] === "1"){
645			bData[1][midx]++;
646		}
647		var qidObj = splitQid(qid);
648		var q = qidObj.q;
649		if (results[q] === undefined) {
650			results[q] = '';
651		}
652		results[q] += String(qdata[2]);
653	}
654	//分野ごとの正解率を計算、分野ごとのHTMLを生成
655	var overviewHtml = '', resultClass, percentage;
656	for (i=1; i<=bunyalen; i++) {
657		if (bData[0][i] > 0) {
658			bData[2][i] = (bData[1][i] / bData[0][i] * 100).toFixed(1);
659			percentage = bData[2][i]+'<span class="unit">%</span>';
660		} else {
661			percentage = '';
662		}
663		resultClass = (bData[0][i] > 0) ? (bData[2][i] >= passRate) ? ' over' : ' under' : '';
664		overviewHtml += '<dt>'+bunyalist[i][0]+'<dd><p>'+bData[0][i]+'問中 '+bData[1][i]+'問正解</p><p class="percentage'+resultClass+'">'+percentage+'</p></dd>';
665	}
666	//各問のデータを列挙
667	var detailHtml = '', dirname = getDirname(target);
668	const maxQ = maxQcount(dirname);
669	for (i=1,len=maxQ; i<=len; i++){
670		var current_correctStr = '未出題', current_correctProb = '-', resultClass = '', historyStr = '';
671		if (results[i]) {
672			current_qCount = results[i].length; //問ごとの出題数
673			current_correctCount = results[i].split('1').length-1; //問ごとの正解数
674			current_correctStr = '正解 '+current_correctCount+' / '+current_qCount;
675			current_correctProb = Math.round(current_correctCount / current_qCount * 1000) / 10; //問ごとの正解率
676			historyStr = results[i].replace(/1/g, '<i class="maru"></i>').replace(/2/g, '<i class="batu"></i>').replace(/0/g, '<i class="un"></i>');
677			resultClass = (current_correctProb >= passRate) ? 'ok' : (current_correctProb >= passRate-20) ? 'caution' : 'ng';
678			current_correctProb += '%';
679		}
680		var qid = target+'_'+i;
681		var categoryId = q2c[qid].split('_');
682		var midx = categoryId[0];
683		var sidx = categoryId[1];
684		var bunya = bunyalist[midx][sidx];
685		detailHtml += '<tr class="tr-first"><td rowspan="2"><a href="/kakomon/'+dirname+'/'+('0'+i).slice(-2)+'.html" target="_blank">問'+i+'</a><td>'+bunya+'<td>'+current_correctStr+'<td><span class="'+resultClass+'">'+current_correctProb+'</span><tr class="tr-second"><td colspan="3">'+historyStr;
686	}
687
688	const report =
689`<div id="reportWrap">
690<div class="overview">
691<h3>概要</h3>
692<div class="content">
693<dl><dt class="total"><span class="unit">🏆総合: </span>${bData[0][0]}<span class="unit">問中</span>${bData[1][0]}<span class="unit">問正解</span>
694<p id="canvas_percentage" data-percentage="${bData[2].join(',')}"><span id="NoP"></span><span class="unit">%</span></p>
695</dt></dl>
696<div id="raderChart">
697<canvas id="canvas_rader_front" width="560" height="420"></canvas>
698<canvas id="canvas_rader_back" width="560" height="420"></canvas>
699</div>
700</div>
701<div class="content">
702<dl>${overviewHtml}</dl>
703</div>
704</div>
705<div class="detail">
706<h3>詳細</h3>
707<div class="historyTableWrap"><table class="testgroup"><tr class="h"><td>&#8470;<td>分野<td>結果<td>正解率${detailHtml}</table></div>
708</div>
709</div>`;
710
711	if ($('body').hasClass('modal')) {
712		$('#reportWrap').fadeOut(300, function(){
713			$(this).replaceWith(report).show();
714			updateGraph();
715		});
716	} else {
717		var header = '<h2>試験回別分析</h2>';
718		var options = '';
719		for (i=0,len=arr.length; i<len; i++){
720			var year_wa = AD2year_wa(getDirname(arr[i]));
721			options += '<option value="'+arr[i]+'">'+year_wa;
722		}
723		var selectHtml = '<div class="datePicker">現在の試験 &raquo; <select name="targetTest">'+options+'</select></div>';
724
725		showModal(header+selectHtml+report);
726		updateGraph();
727	}
728}
729function updateGraph(){
730	if (!$('#canvas_percentage')[0]) return;
731
732	const animTime1 = 1500, animTime2 = 1500, cx = 140, cy = 105, max_r = 85;
733	let r, x, y, x1, x2, y1, y2, px = [], py = [];
734	
735	const probs = $('#canvas_percentage').attr('data-percentage').split(',');
736	const anglelen = probs.length - 1;
737	let cr = $('#canvas_rader_back').get(0).getContext('2d');
738
739	const Nop = probs[0] || NaN;
740	animateNumber('NoP', 0, Nop, animTime1, 1)
741
742	//レーダチャート(六角形)
743	cr.scale(2, 2);
744	cr.lineWidth = 1;
745	cr.fillStyle = getStyleValue('--color-ddd');
746	cr.strokeStyle = getStyleValue('--color-ddd');
747	cr.textBaseline = "bottom";
748	cr.textAlign = "right";
749	cr.font = '9px'+fm;
750
751	for (var i=1; i<=5; i++) {
752		r = max_r / 5 * i;
753		x = r * Math.sqrt(3) / 2;
754		y = r * 0.5;
755		cr.beginPath();
756		cr.moveTo(cx, cy-r);
757		cr.lineTo(cx+x, cy-y);
758		cr.lineTo(cx+x, cy+y);
759		cr.lineTo(cx, cy+r);
760		cr.lineTo(cx-x, cy+y);
761		cr.lineTo(cx-x, cy-y);
762		cr.closePath();
763		cr.stroke();
764		cr.fillText((i * 20) + '%', cx, cy - r + 2);
765	}
766
767	x = max_r * Math.sqrt(3) / 2;
768	y = max_r * 0.5;
769	cr.strokeStyle = getStyleValue('--color-ccc');
770	for (i=1; i<=3; i++) {
771		cr.beginPath();
772		switch (i) {
773		case 1: x1 = cx; y1 = cy-max_r;
774				x2 = cx; y2 = cy+max_r;
775				break;
776		case 2: x1 = cx+x; y1 = cy-y;
777				x2 = cx-x; y2 = cy+y;
778				break;
779		case 3: x1 = cx-x; y1 = cy-y;
780				x2 = cx+x; y2 = cy+y;
781				break;
782		}
783		cr.moveTo(x1, y1);
784		cr.lineTo(x2, y2);
785		cr.stroke();
786	}
787
788	//分野名を出力
789	cr.fillStyle = getStyleValue('--color-555');
790	cr.font = 'bold 12px'+fm;
791	const lineHeight = cr.measureText("あ").width;
792	const normalFont = '12px' + fm;
793	const boldFont = 'bold 11px' + fm;
794	const positions = [
795		null,
796		{ x: cx,     y: cy-max_r-7, align: 'center', baseline: 'bottom' },
797		{ x: cx+x+3, y: cy-y-3,     align: 'left',   baseline: 'bottom' },
798		{ x: cx+x+3, y: cy+y+3,     align: 'left',   baseline: 'bottom' },
799		{ x: cx,     y: cy+max_r-5, align: 'center', baseline: 'top' },
800		{ x: cx-x-3, y: cy+y+3,     align: 'right',  baseline: 'bottom' },
801		{ x: cx-x-3, y: cy-y-3,     align: 'right',  baseline: 'bottom' },
802	];
803	for (i=1; i<=anglelen; i++) {
804		const pos = positions[i];
805		if (!pos) continue;
806
807		cr.textAlign = pos.align;
808		cr.textBaseline = pos.baseline;
809
810		cr.font = normalFont;
811		cr.fillText(bunyalist_short[i], pos.x, pos.y);
812
813		cr.font = boldFont;
814		cr.fillText(probs[i] + '%', pos.x, pos.y + lineHeight);
815	}
816
817	//分野成績を打点
818	for (i=1; i<=anglelen; i++) {
819		r = max_r * probs[i] / 100;
820		x = r * Math.sqrt(3) / 2;
821		y = r * 0.5;
822		switch (i) {
823		case 1: px[i] = cx; py[i] = cy-r;
824				break;
825		case 2: px[i] = cx+x; py[i] = cy-y;
826				break;
827		case 3: px[i] = cx+x; py[i] = cy+y;
828				break;
829		case 4: px[i] = cx; py[i] = cy+r;
830				break;
831		case 5: px[i] = cx-x; py[i] = cy+y;
832				break;
833		case 6: px[i] = cx-x; py[i] = cy-y;
834				break;
835		}
836		//cr.fillStyle = 'rgba(230,152,0,.8)';
837		//cr.fillRect(px[i]-2, py[i]-2, 4, 4);
838	}
839	px.push(px[1]); py.push(py[1]); //多角形の最後用
840
841	cr = $('#canvas_rader_front').get(0).getContext('2d');
842	cr.scale(2, 2);
843	cr.fillStyle = 'rgba(255,232,162,.2)';
844	cr.strokeStyle = 'rgba(230,152,0,.8)';
845	cr.lineWidth = 2;
846	var stx, sty, edx, edy, dx, dy, mx, my, weight;
847	let startTime = null;
848	function loop2(timestamp){
849		if (!startTime) startTime = timestamp;
850		var e = Math.min((timestamp - startTime) / animTime2, 1); //ミリ秒単位
851		cr.clearRect(0, 0, cx*2, cy*2);
852		cr.beginPath();
853		cr.moveTo(cx, cy); //ポリゴン開始位置
854		var phase = Math.floor(e * 6) +1;
855		for (var i=1; i<=phase+1; i++) {
856			cr.lineTo(px[i], py[i]);
857		}
858		stx = px[phase]; sty = py[phase];
859		edx = px[phase+1]; edy = py[phase+1];
860		dx = edx - stx;
861		dy = edy - sty;
862		const local = e * 6 - (phase - 1);
863		weight = (local < 0.2) ? 0 : local * 1.25 - 0.25;
864		mx = stx + dx * weight;
865		my = sty + dy * weight;
866		cr.lineTo(mx, my);
867		cr.closePath();
868		cr.fill();
869		cr.beginPath();
870		cr.moveTo(px[1], py[1]); //ポリゴン枠開始位置
871		for (i=2; i<=phase; i++) {
872			cr.lineTo(px[i], py[i]);
873		}
874		cr.lineTo(mx, my);
875		cr.stroke();
876
877		if (e < 1) window.requestAnimationFrame(loop2);
878	}
879	window.requestAnimationFrame(loop2);
880}
881let historyTableRow = null;
882function tableLazyLoad(){
883	$tableWrap = $('.historyTableWrap');
884	$table = $tableWrap.find('table');
885	$lastRow = $('tr', $table).eq(-15);
886	const $scrollElem = $(window).width() > 768 ? $tableWrap : $('#overLayer');
887	$scrollElem.scroll(function(){
888		if (!historyTableRow.length) {
889			$scrollElem.off('scroll');
890			return;
891		}
892		// 表示領域の下端の位置
893		const scrollElemBottom = $scrollElem.height() + $scrollElem.offset().top;
894		// 末尾の要素の上端の位置
895		const lastRowTop = $lastRow.offset().top;
896		if (lastRowTop < scrollElemBottom) {
897			$table.append(historyTableRow.shift());
898			$lastRow = $('tr', $table).eq(-15);
899		}
900	});
901}
902$('#overLayer').on('click', '.coverageTableWrap a', function(){
903	var matchs = $(this).attr('href').match(/\/(20[0-9]{2})\/([0-9]{2})\.html$/);
904	if (matchs && confirm(AD2year_wa(matchs[1], true)+' 問'+Number(matchs[2])+'を新しいタブで開きます。')) {
905		return true;
906	}
907	return false;
908})
909$('body').on('click', '#coverage', function(){
910	if (!uid || !$('#loginBtn').hasClass('login')) {
911		showStateMessage('<i class="caution"></i>不正なリクエストです。');
912		return false;
913	}
914	const $i = $('i', $(this));
915	loadUserlog($i).done(function(){
916		coverage();
917	});
918	return false;
919})
920function coverage() {
921	var matrix = {};
922	var arr = getAllTimes().reverse();
923	for (var i=0,len=arr.length; i<len; i++) {
924		matrix[arr[i]] = [];
925	}
926	for (i=0,len=userlog.length; i<len; i++) {
927		if (!userlog[i]) continue;
928		var qdata = userlog[i].split(',');
929		var r = qdata[2];
930		if (!Number(r)) continue;
931
932		var qidObj = splitQid(qdata[1]);
933		var eid = qidObj.eid, q = qidObj.q;
934		if (matrix[eid]) {
935			if (matrix[eid][q] === undefined) {
936				matrix[eid][q] = '';
937			}
938			matrix[eid][q] += String(r);
939		}
940	}
941	let totalCount=0, totalAnswerCount=0, rankCount = [0,0,0,0], rows='';
942	//問番号の行を追加
943	rows += '<tr class="header"><td class="fixedCol">&nbsp;';
944	const maxCol = maxQcount(2020);
945	for (i=1; i<=maxCol; i++) {
946		rows += '<td>'+i;
947	}
948	for (i=0,len=arr.length; i<len; i++) {
949		var eid = arr[i];
950		var dirname = getDirname(eid);
951		var year_wa = AD2year_wa(dirname, true);
952		var maxQ = maxQcount(dirname);
953		var star = Object.keys(matrix[eid]).length == maxQ ? '<span class="star">★</span>' : '';
954		rows += '<tr><td class="fixedCol">'+year_wa+star;
955		let partAnswerCount = 0;
956		for (var j=1; j<=maxQ; j++) {
957			let rank, panelTitle;
958			if (matrix[eid][j] && matrix[eid][j] != 0) {//回答歴がある場合
959				const last2 = ('0' + matrix[eid][j]).slice(-2);
960				if (last2 == '11') {
961					rank = 3;
962				} else if (last2.charAt(1) == '1') {
963					rank = 2;
964				} else if (last2.charAt(1) == '2') {
965					rank = 1;
966				}
967				rankCount[rank]++;
968				partAnswerCount++;
969				panelTitle = matrix[eid][j].replace(/2/g, '×').replace(/1/g, '○').replace(/0/g, '-');
970			} else {
971				panelTitle = '未出題';
972				rank = '-';
973			}
974			rows += '<td title="'+panelTitle+'"><a href="/kakomon/'+dirname+'/'+('0'+j).slice(-2)+'.html" target="_blank"><i class="rank'+rank+'"></i></a>';
975		}
976		totalCount += maxQ;
977		totalAnswerCount += partAnswerCount;
978	}
979
980	const coveragePer = (totalAnswerCount / totalCount * 100).toFixed(1);
981	const rankReiewBtn = [];
982	const dashOffset = [];
983	const dashArray = [];
984	const gap = 0.4;
985	dashOffset[3] = 0;
986	for (let i=3; i>=1; i--) {
987		const rankPer = (rankCount[i] / totalCount * 100).toFixed(1);
988		dashArray[i] = Math.max(rankPer - gap, 0) + ',100';
989		dashOffset[i-1] = dashOffset[i] - rankPer;
990		rankReiewBtn[i] = rankCount[i] ? '<button class="rank-review-btn" data-target="rank'+i+'">復習</button>' : '<button disabled style="visibility:hidden">復習</button>';
991	}
992
993	const html =
994`<h2>達成度分析</h2>
995<div class="coverageSummary">
996<div class="donutGraphWrap">
997<svg class="donutGraph" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
998<defs>
999<filter id="innerShadowLike" x="-50%" y="-50%" width="200%" height="200%">
1000<feDropShadow dx="-0.3" dy="0.3" stdDeviation="1" flood-color="rgb(0 0 0 / 15%)"/>
1001</filter>
1002<linearGradient id="goldGrad" x1="0%" y1="0%" x2="100%" y2="100%">
1003<stop offset="0%" stop-color="#F59E0B" />
1004<stop offset="50%" stop-color="#FDE68A" />
1005<stop offset="100%" stop-color="#F59E0B" />
1006</linearGradient>
1007<linearGradient id="silverGrad" x1="0%" y1="0%" x2="100%" y2="100%">
1008<stop offset="0%" stop-color="#a7b5c7" />
1009<stop offset="50%" stop-color="#e6e6e6" />
1010<stop offset="100%" stop-color="#a7b5c7" />
1011</linearGradient>
1012<linearGradient id="bronzeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
1013<stop offset="0%" stop-color="#914200" />
1014<stop offset="50%" stop-color="#FDBA74" />
1015<stop offset="100%" stop-color="#914200" />
1016</linearGradient>
1017</defs>
1018<circle id="donutBackground" cx="18" cy="18" r="15.915" fill="transparent" stroke="var(--color-eee)" stroke-width="4" class="donut-ring"></circle>
1019<circle id="ringRank1" cx="18" cy="18" r="15.915" fill="transparent" stroke="url(#bronzeGrad)" stroke-width="4" class="donut-ring donutProgress"></circle>
1020<circle id="ringRank2" cx="18" cy="18" r="15.915" fill="transparent" stroke="url(#silverGrad)" stroke-width="4" class="donut-ring donutProgress"></circle>
1021<circle id="ringRank3" cx="18" cy="18" r="15.915" fill="transparent" stroke="url(#goldGrad)" stroke-width="4" class="donut-ring donutProgress"></circle>
1022<circle cx="18" cy="18" r="14" fill="var(--main-bg)" filter="url(#innerShadowLike)"></circle>
1023</svg>
1024<div class="donutGraph-text"><div class="text-top">🎖️達成率</div><div class="text-percentage"><span id="NoP"></span><span class="unit">%</span></div><div class="text-bottom">${totalAnswerCount} / ${totalCount} 問</div></div>
1025</div>
1026<div class="rank3"><div class="summary-img"><i class="rank3"></i></div><div class="summary-text"><div class="summary-text-label">連続正解中</div><div class="summary-text-num">${rankCount[3]}<span class="unit"> 問</span></div></div>${rankReiewBtn[3]}</div>
1027<div class="rank2"><div class="summary-img"><i class="rank2"></i></div><div class="summary-text"><div class="summary-text-label">前回正解</div><div class="summary-text-num">${rankCount[2]}<span class="unit"> 問</span></div></div>${rankReiewBtn[2]}</div>
1028<div class="rank1"><div class="summary-img"><i class="rank1"></i></div><div class="summary-text"><div class="summary-text-label">前回不正解</div><div class="summary-text-num">${rankCount[1]}<span class="unit"> 問</span></div></div>${rankReiewBtn[1]}</div>
1029<span id="review_ok" style="display:none"></span>
1030</div>
1031<div class="coverageTableWrap">
1032<table>${rows}</table>
1033※表の各セルをクリックすると、その問題を新しいタブで開けます。`;
1034	showModal(html, "coverage");
1035
1036	setTimeout(function(){
1037		animateNumber('NoP', 0, coveragePer, 2000, 1)
1038		$('#donutBackground').css('stroke-dashoffset', 0);
1039		$('#ringRank3').css({
1040			'stroke-dashoffset': dashOffset[3],
1041			'stroke-dasharray': dashArray[3],
1042		});
1043		$('#ringRank2').css({
1044			'stroke-dashoffset': dashOffset[2],
1045			'stroke-dasharray': dashArray[2],
1046		});
1047		$('#ringRank1').css({
1048			'stroke-dashoffset': dashOffset[1],
1049			'stroke-dasharray': dashArray[1],
1050		});
1051	}, 300);
1052}
1053function loadChartJS()
1054{
1055	var dfd = new $.Deferred();
1056	if (typeof Chart == 'function') {
1057		dfd.resolve();
1058		return dfd.promise();
1059	} else {
1060		var script1 = document.createElement('script');
1061		script1.src = 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js';
1062		script1.async = false;
1063		script1.onload = function() {
1064			var script2 = document.createElement('script');
1065			script2.async = false;
1066			script2.src = 'https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js';
1067			script2.onload = function() {
1068				dfd.resolve();
1069			}
1070			script2.onerror = function() {
1071				dfd.reject();
1072			}
1073			document.body.appendChild(script2);
1074		}
1075		script1.onerror = function() {
1076			dfd.reject();
1077		}
1078		document.body.appendChild(script1);
1079	}
1080	return dfd.promise();
1081}
1082$('#overLayer').on('change', '[name="targetPeriod"]', function(){
1083	progress($(this).val());
1084})
1085//学習推移分析
1086$('body').on('click', '#progress', function(){
1087	if (!uid || !$('#loginBtn').hasClass('login')) {
1088		showStateMessage('<i class="caution"></i>不正なリクエストです。');
1089		return false;
1090	}
1091	const $i = $('i', $(this));
1092	loadUserlog($i).done(function(){
1093		loadChartJS().done(function() {
1094			progress('4');
1095		});
1096	});
1097	return false;
1098})
1099
1100function progress(weekCount) {
1101	const DAY_PER_WEEK = 7;
1102	const SECOND_PER_DAY = 86400;
1103	
1104	function drawGraph()
1105	{
1106		var i, j;
1107		var dataCount = totalResult.length;
1108		var dayWeek, colorPrefix;
1109		if (daysInSection == 1) {
1110			dayWeek = '日';
1111			colorPrefix = 'rgb';
1112		} else {
1113			dayWeek = '週';
1114			colorPrefix = 'rgba';
1115		}
1116		//Object.assign、structuredCloneでは関数をディープコピーできない(参照渡しになる)
1117		var questionOptions = {
1118			devicePixelRatio: Math.max(2, window.devicePixelRatio),
1119			responsive: true,
1120			maintainAspectRatio: false,
1121			scales: {
1122				y1: {
1123					grace: '15%',
1124					position: 'left',
1125					min: 0,
1126					suggestedMax: 50,
1127					ticks: {
1128						font: {size: 10},
1129						padding: 0,
1130						maxTicksLimit: 6,
1131						callback: function(value) {
1132							return value+'問';
1133						}
1134					},
1135					grid: {
1136						tickLength: 2,
1137					},
1138				},
1139				y2: {
1140					display: false,
1141					min: 0,
1142					suggestedMax: 50,
1143				},
1144				x: {
1145					ticks: {
1146						color: getStyleValue('--color-666'),
1147						font: {size: 10},
1148						maxRotation: 0,
1149					},
1150					grid: {display: false}
1151				}
1152			},
1153			plugins: {
1154				tooltip: {
1155					callbacks: {
1156						title: function(ctx) {
1157							if (ctx[0].dataset.borderColor.indexOf('rgba') != -1) {
1158								//m/d - m/d
1159								var m_d = ctx[0].label.split('/');
1160								var d = new Date(new Date().getFullYear(), m_d[0]-1, m_d[1]);
1161								d.setDate(d.getDate()-6);
1162								return (d.getMonth()+1)+'/'+d.getDate()+' - '+ctx[0].label;
1163							} else {
1164								//m/d
1165								return ctx[0].label;
1166							}
1167						},
1168					},
1169				},
1170				legend: {
1171					reverse: true,
1172					labels: {
1173						color: getStyleValue('--color-666'),
1174					},
1175				},
1176			},
1177			layout: {
1178				padding: {right: 8}
1179			},
1180		};
1181
1182		var accuracyOptions = {
1183			devicePixelRatio: Math.max(2, window.devicePixelRatio),
1184			responsive: true,
1185			maintainAspectRatio: false,
1186			scales: {
1187				y: {
1188					grace: '15%',
1189					position: 'left',
1190					suggestedMin: 50,
1191					max: 100,
1192					ticks: {
1193						font: {size: 10},
1194						stepSize: 10,
1195						padding: 0,
1196						callback: function(value) {
1197							return value+'%';
1198						}
1199					},
1200					grid: {
1201						color: getStyleValue('--color-eee'),
1202						tickLength: 2,
1203					},
1204				},
1205				x: {
1206					ticks: {
1207						color: getStyleValue('--color-666'),
1208						font: {size: 10},
1209						maxRotation: 0,
1210					},
1211					grid: {display: false}
1212				}
1213			},
1214			plugins: {
1215				tooltip: {
1216					callbacks: {
1217						title: function(ctx) {
1218							if (ctx[0].dataset.borderColor.indexOf('rgba') != -1) {
1219								//m/d - m/d
1220								var m_d = ctx[0].label.split('/');
1221								var d = new Date(new Date().getFullYear(), m_d[0]-1, m_d[1]);
1222								d.setDate(d.getDate()-6);
1223								return (d.getMonth()+1)+'/'+d.getDate()+' - '+ctx[0].label;
1224							} else {
1225								//m/d
1226								return ctx[0].label;
1227							}
1228						},
1229					},
1230				},
1231				legend: {
1232					reverse: true,
1233					labels: {
1234						color: getStyleValue('--color-666'),
1235					},
1236				},
1237			},
1238			layout: {
1239				padding: {right: 8}
1240			},
1241		};
1242		
1243		var line_datalabels = {
1244			font: {size: 10},
1245			borderRadius: 4,
1246			backgroundColor: 'rgb(228,67,67)',
1247			color: '#fff',
1248			padding: 2,
1249			display: function(ctx) {
1250				if (!ctx.dataset.data[ctx.dataIndex]) {
1251					return false;
1252				}
1253				var step = Math.ceil(30 / (ctx.chart.chartArea.width / dataCount));
1254				return (ctx.dataIndex + 1 - dataCount) % step &&
1255					(!isNaN(ctx.dataset.data[ctx.dataIndex-1]) || !isNaN(ctx.dataset.data[ctx.dataIndex+1]))
1256					? false : true;
1257			},
1258			align: function(ctx) {
1259				return ctx.dataset.data[ctx.dataIndex] % 100 ? 'end' : 'center';
1260			},
1261			formatter: function(value, ctx) {
1262				if (ctx.dataset.label.indexOf('問題数') !== -1) {
1263					return value ? value+'問' : '';
1264				} else {
1265					return Number(value).toFixed(0)+'%';
1266				}
1267			}
1268		};
1269		var bar_datalabels = {
1270			color: getStyleValue('--color-444'),
1271			font: {size: 10},
1272			anchor: 'end',
1273			align: 'end',
1274			offset: 0,
1275			display: false,
1276			/*function(ctx) {
1277				return false;
1278				var step = Math.ceil(30 / (ctx.chart.chartArea.width / dataCount));
1279				return (ctx.dataIndex + 1 - dataCount) % step &&
1280					(ctx.dataset.data[ctx.dataIndex-1] || ctx.dataset.data[ctx.dataIndex+1])
1281					? false : true;
1282			}
1283			formatter: function(value, ctx) {
1284				if (ctx.dataset.label.indexOf('問題数') !== -1) {
1285					return value ? value+'問' : '';
1286				} else {
1287					return value ? Number(value).toFixed(0)+'%' : '';
1288				}
1289			}*/
1290		};
1291		var line_obj = {
1292			type: 'line',
1293			tension: 0.2,
1294			borderWidth: 2,
1295			pointStyle: 'cross',
1296			pointBorderWidth: 1,
1297			backgroundColor: 'transparent',
1298			borderColor: colorPrefix+'(228,67,67,1)',
1299			pointRadius: 2,
1300			spanGaps: true,
1301			datalabels: line_datalabels,
1302		}
1303		var bar_obj = {
1304			type: 'bar',
1305			barPercentage: 0.95,
1306			categoryPercentage: 1,
1307			datalabels: bar_datalabels,
1308		}
1309		
1310		var dataset1 = [];
1311		var dataset2 = [];
1312
1313		//折れ線グラフ:累計(前面・後面の関係で先に)
1314		var obj1 = Object.assign({}, line_obj);
1315		obj1.label = '問題数(累計)';
1316		obj1.yAxisID = 'y2';
1317
1318		var obj2 = Object.assign({}, line_obj);
1319		obj2.label = '正解率(累計)';
1320		obj2.yAxisID = 'y';
1321
1322		//datasets.dataを設定
1323		var questionDatasets = [];
1324		var accuracyDatasets = [];
1325		for (j=0; j<dataCount; j++) {
1326			questionDatasets.push(totalResult[j].category[0]);
1327			if (totalResult[j].category[0]) {
1328				accuracyDatasets.push((totalResult[j].correct / totalResult[j].category[0] * 100).toFixed(1));
1329			} else {
1330				accuracyDatasets.push(0);
1331			}
1332		}
1333
1334		obj1.data = questionDatasets;
1335		dataset1.push(obj1);
1336		obj2.data = accuracyDatasets;
1337		dataset2.push(obj2);
1338
1339		//棒グラフ:区間
1340		obj1 = Object.assign({}, bar_obj);
1341		obj1.label = '問題数('+dayWeek+')';
1342		obj1.yAxisID = 'y1';
1343		obj1.borderColor = colorPrefix+'(255,200,99,0.8)';
1344		obj1.backgroundColor = 'rgba(255,200,99,0.6)';
1345
1346		obj2 = Object.assign({}, bar_obj);
1347		obj2.label = '正解率('+dayWeek+')';
1348		obj2.borderColor = colorPrefix+'(54,162,235,0.8)';
1349		obj2.backgroundColor = 'rgba(54,162,235,0.6)';
1350		obj2.yAxisID = 'y';
1351
1352		questionDatasets = [];
1353		accuracyDatasets = [];
1354		for (j=0; j<dataCount; j++) {
1355			questionDatasets.push(periodResult[j].category[0]);
1356			if (periodResult[j].category[0]) {
1357				accuracyDatasets.push((periodResult[j].correct / periodResult[j].category[0] * 100).toFixed(1));
1358			} else {
1359				accuracyDatasets.push(0);
1360			}
1361		}
1362
1363		obj1.data = questionDatasets;
1364		dataset1.push(obj1);
1365		obj2.data = accuracyDatasets;
1366		dataset2.push(obj2);
1367
1368		//分野ごとレインボーバージョン
1369		/*for (i=1; i<=bunyalen; i++) {
1370			totalDatasets = [];
1371			periodDatasets = [];
1372			var obj1 = {
1373				type: 'bar',
1374				label: bunyanameShort[i],
1375				backgroundColor: colors[i],
1376				borderColor: colors[i],
1377				yAxisID: 'y1',
1378				datalabels: {
1379					display: false
1380				}
1381			}
1382			var obj2 = {
1383				type: 'bar',
1384				label: bunyanameShort[i],
1385				backgroundColor: colors[i],
1386				borderColor: colors[i],
1387				yAxisID: 'y1',
1388				datalabels: {
1389					display: false
1390				}
1391			}
1392			for (j=0; j<weeksInPeriod; j++) {
1393				totalDatasets.push(totalResult[j].category[i]);
1394				periodDatasets.push(periodResult[j].category[i]);
1395			}
1396			obj1.data = totalDatasets;
1397			totalGraphObj.data.datasets.push(obj1);
1398			obj2.data = periodDatasets;
1399			periodGraphObj.data.datasets.push(obj2);
1400		}
1401		*/
1402
1403		//x軸のラベルを設定
1404		var questionLabels = [], accuracyLabels = [];
1405		for (i=0; i<dataCount; i++) {
1406			//var st = new Date((periodStartTime + (SECOND_PER_DAY * daysInSection) * i) * 1000);
1407			var ed = new Date((periodStartTime + (SECOND_PER_DAY * daysInSection) * (i+1) - SECOND_PER_DAY) * 1000);
1408			
1409			questionLabels.push((ed.getMonth() + 1)+'/'+ed.getDate());
1410			accuracyLabels.push((ed.getMonth() + 1)+'/'+ed.getDate());
1411		}
1412
1413		var ctx1 = document.getElementById('questionGraph').getContext('2d');
1414		var ctx2 = document.getElementById('accuracyGraph').getContext('2d');
1415		ctx1.canvas.height = 250;
1416		ctx2.canvas.height = 250;
1417		new Chart(ctx1, {
1418			plugins: [ChartDataLabels],
1419			data: {
1420				labels: questionLabels,
1421				datasets: dataset1
1422			},
1423			options: questionOptions,
1424		});
1425		new Chart(ctx2, {
1426			plugins: [ChartDataLabels],
1427			data: {
1428				labels: accuracyLabels,
1429				datasets: dataset2
1430			},
1431			options: accuracyOptions,
1432		});
1433	}
1434
1435	var i, len = [], report = '';
1436	if (userlog.length === 0) {
1437		report = '<div id="reportWrap" style="height:632px">'+emptySVG('学習履歴が見つかりません')+'</div>';
1438	} else {
1439		var midnight = new Date().setHours(0, 0, 0, 0) / 1000;
1440		var weeksInPeriod;
1441		if (weekCount == 'all') {
1442			var firstlog = userlog[0].split(',');
1443			var firstlogtime = new Date(firstlog[0] * 1000).getTime() / 1000;
1444			weeksInPeriod = Math.ceil((midnight - firstlogtime) / (SECOND_PER_DAY * DAY_PER_WEEK));
1445		} else {
1446			weeksInPeriod = weekCount;
1447		}
1448		var periodStartTime = midnight - (SECOND_PER_DAY * DAY_PER_WEEK * weeksInPeriod);
1449		var daysInPeriod = weeksInPeriod * DAY_PER_WEEK; //対象期間の総日数
1450		var daysInSection = weeksInPeriod > 8 ? DAY_PER_WEEK : 1; //データ1つの集計日数
1451
1452		//各分野の学習数を集計するための配列 [0]総数、[1]-[bunyalen]各分野の数
1453		var categoryCnt = [];
1454		for (i=0; i<=bunyalen; i++) {
1455			categoryCnt.push(0);
1456		}
1457
1458		var
1459		current = 0,
1460		totalResult = [],
1461		totalCnt = categoryCnt.slice(); //配列コピー
1462		totalCorrect = 0;
1463
1464		periodResult = [],
1465		periodCnt = categoryCnt.slice(); //配列コピー
1466		periodCorrect = 0,
1467
1468		weekResult = [],
1469		periodDayCnt = 0,
1470		periodDay = [];
1471
1472		for (i=0,len=userlog.length; i<len; i++) {
1473			if (!userlog[i]) continue;
1474			var qdata = userlog[i].split(',');
1475			var time = qdata[0];
1476			var r = qdata[2];
1477			var c = qdata[3].charAt(0);
1478
1479			//今日の学習履歴まで達したら処理を抜ける
1480			if (time >= midnight) {
1481				break;
1482			}
1483			if (time >= periodStartTime) {
1484				//集計対象日まで進める
1485				while (time >= (periodStartTime + SECOND_PER_DAY * (current+1))){
1486					periodDay.push(periodDayCnt);
1487					periodDayCnt = 0;
1488					current++;
1489					if (current % daysInSection == 0) {
1490						//1週間分のデータを書き込む、配列の参照を渡さないようにコピーして渡す
1491						totalResult.push({'category': totalCnt.slice(), 'correct': totalCorrect});
1492						periodResult.push({'category': periodCnt, 'correct': periodCorrect});
1493						periodCnt = categoryCnt.slice(); //配列コピー
1494						periodCorrect = 0; 
1495					}
1496					if (current % DAY_PER_WEEK == 0) {
1497						weekResult.push(periodDay);
1498						periodDay = [];
1499					}
1500				}
1501				//集計対象期間記録
1502				periodCnt[0]++;
1503				periodCnt[c]++;
1504				if (r == 1) {
1505					periodCorrect++;
1506				}
1507				periodDayCnt++;
1508			}
1509			//全期間記録
1510			totalCnt[0]++;
1511			totalCnt[c]++;
1512			if (r == 1) {
1513				totalCorrect++;
1514			}		
1515		}
1516		//後ろの空白期間を埋める
1517		while (current < daysInPeriod){
1518			periodDay.push(periodDayCnt);
1519			periodDayCnt = 0;
1520			current++;
1521			if (current % daysInSection == 0) {
1522				//現在の期間のデータを書き込む
1523				totalResult.push({'category': totalCnt.slice(), 'correct': totalCorrect});
1524				periodResult.push({'category': periodCnt, 'correct': periodCorrect});
1525				periodCnt = categoryCnt.slice(); //配列コピー
1526				periodCorrect = 0; 
1527			}
1528			if (current % DAY_PER_WEEK == 0) {
1529				weekResult.push(periodDay);
1530				periodDay = [];
1531			}
1532		}
1533
1534		var midnightDate = new Date(midnight * 1000);
1535		midnightDate.setDate(midnightDate.getDate() - 1);
1536		midnightDate = midnightDate.getFullYear()+'/'+(midnightDate.getMonth() + 1)+'/'+midnightDate.getDate();
1537		var periodStartTimeDate = new Date(periodStartTime * 1000);
1538		periodStartTimeDate = periodStartTimeDate.getFullYear()+'/'+(periodStartTimeDate.getMonth() + 1)+'/'+periodStartTimeDate.getDate();
1539		
1540		var detailHtml = '';
1541		var dayOfWeekStr = ["日", "月", "火", "水", "木", "金", "土"];
1542		for (i=weekResult.length-1; i>=0; i--) {
1543			var flexItem = '';
1544			var sum = 0;
1545			for (j=weekResult[i].length-1; j>=0; j--) {
1546				var date = new Date((periodStartTime + SECOND_PER_DAY * (DAY_PER_WEEK * i + j)) * 1000);
1547				date = (date.getMonth() + 1)+'/'+date.getDate()+'('+dayOfWeekStr[date.getDay()]+')';
1548				var qcount = weekResult[i][j] ? '<b>'+weekResult[i][j]+'問</b>' : '<span class="un">'+weekResult[i][j]+'問</span>'
1549				flexItem += '<div><div class="day">'+date+'</div>'+qcount+'</div>';
1550				sum += weekResult[i][j];
1551			}
1552			var stDay = new Date((periodStartTime + SECOND_PER_DAY * DAY_PER_WEEK * i) * 1000);
1553			stDay = (stDay.getMonth() + 1)+'/'+stDay.getDate();
1554			var edDay = new Date((periodStartTime + SECOND_PER_DAY * (DAY_PER_WEEK * i + 6)) * 1000);
1555			edDay = (edDay.getMonth() + 1)+'/'+edDay.getDate();
1556
1557			var changeStr = '<span class="un">±0</span>';
1558			if (i > 0) {
1559				var cnt1 = weekResult[i].reduce(function(sum, val){
1560					return sum + val;
1561				});
1562				var cnt2 = weekResult[i-1].reduce(function(sum, val){
1563					return sum + val;
1564				});
1565				if (cnt1 > cnt2) {
1566					changeStr = '<span style="color:#1fad52">▲+'+(cnt1-cnt2)+'</span>'
1567				} else if (cnt1 < cnt2) {
1568					changeStr = '<span style="color:#e34a4a">▼-'+(cnt2-cnt1)+'</span>'
1569				}
1570			} else {
1571				changeStr = '';
1572			}
1573			detailHtml += '<div class="flexHeader">'+stDay+' - '+edDay+'</div><div id="flexTable">'+flexItem+'<div><div class="day">計<b>'+sum+'</b>問</div>'+changeStr+'</div></div>';
1574		}
1575
1576		report =
1577`<div id="reportWrap">
1578<style>
1579#reportWrap{flex-direction:column}
1580.radioTable{border:1px solid #555;border-radius:4px;width:max-content;margin:1em auto;font-size:90%;line-height:1.8}
1581.radioTable [type="radio"]{display:none}
1582.radioTable label{display:inline-block;width:100px}
1583.radioTable span{display:block;text-align:center;cursor:pointer}
1584.radioTable [type="radio"]:checked + span{background:#555;color:#fff}
1585#flexTable{display:flex}
1586#flexTable > div{box-sizing:border-box;background:var(--main-bg);flex:1;padding:5px 0;font-size:80%;border-bottom:1px solid var(--dark-border);text-align:center}
1587#flexTable > div b{font-size:120%}
1588.flexHeader{background:var(--main-bg-sub);font-size:80%;font-weight:bold;color:var(--color-444);padding-left:3px}
1589.progressGraphWrap{width:100%;overflow:hidden;background:var(--main-bg);user-select:none}
1590#progressGraph{display:flex;width:200%;transition:transform 0.3s ease-out;height:250px}
1591#progressGraph.slide{transform:translateX(-50%)}
1592#progressGraph > div{flex:1}
1593.progressTableWrap{max-height:280px;overflow-y:scroll;background:var(--main-bg)}
1594.un{color:#888}
1595@media (max-width:770px){
1596.progressGraphWrap{padding-bottom:30px}
1597.progressTableWrap{max-height:none;overflow:auto}
1598}
1599@media (max-width:600px){
1600#flexTable{flex-wrap:wrap}
1601#flexTable > div{flex-basis:25%}
1602}
1603</style>
1604<div class="progressGraphWrap"><h3>学習問題数と正解率(${periodStartTimeDate} - ${midnightDate})</h3>
1605<div class="radioTable">
1606<label><input type="radio" name="graphChange" value="1" checked><span>問題数</span></label>
1607<label><input type="radio" name="graphChange" value="2"><span>正解率</span></label>
1608</div>
1609<div id="progressGraph">
1610<div><canvas id="questionGraph"></canvas></div>
1611<div><canvas id="accuracyGraph"></canvas></div>
1612</div>
1613</div>
1614<div class="progressTableWrap"><h3>日々の学習状況</h3>${detailHtml}</div>
1615</div>`;
1616	}
1617	if ($('body').hasClass('modal')) {
1618		$('#reportWrap').fadeOut(300, function(){
1619			$(this).replaceWith(report).show();
1620			setTimeout(drawGraph, 50);
1621		});
1622	} else {
1623		var header = '<h2>学習推移分析</h2>';
1624		var selectHtml =
1625`<div class="datePicker">
1626現在の期間 &raquo; <select name="targetPeriod">
1627<option value="all">全期間
1628<option value="4" selected>過去4週間
1629<option value="8">過去8週間
1630<option value="13">過去3カ月間
1631<option value="26">過去6カ月間
1632<option value="52">過去1年間
1633</select>
1634</div>`;
1635		showModal(header+selectHtml+report);
1636		setTimeout(drawGraph, 350);
1637	}
1638}
1639$('body').on('change', '[name="graphChange"]', function(){
1640	$('#progressGraph').toggleClass('slide');
1641});
1642
1643let confirmTableCache;
1644function getConfirmTable() {
1645	if (!confirmTableCache) {
1646		initReviewWindowEvents();
1647
1648		const $div = $('<div>').append($('#tab2 .input_categories').clone());
1649		$div.find('[type="checkbox"]:not(.dummy)').attr('checked', 'checked'); //Property変更では反映されない
1650		const $dl = $div.html();
1651		
1652		confirmTableCache =
1653`<style>
1654#overLayer.review{width:650px;height:600px;display:flex;flex-direction:column}
1655#review-checkbox-wrap{flex:1;overflow:auto;scrollbar-gutter:stable;margin-bottom:72px}
1656#floatBtn{bottom:0 !important;padding:15px 0}
1657</style>
1658<dl class="confirmTable"><dt>対象問題数<dd><i class="note"></i><span id="questionCount"></span></dl>
1659<div id="review-checkbox-wrap">
1660<div class="checkall"><span>全項目チェック</span> <button type="button" name="check_all" value="review-checkbox-wrap">ON</button> <button type="button" name="check_all" value="review-checkbox-wrap">OFF</button></div>
1661${$dl}
1662</div>`;
1663	}
1664	return confirmTableCache;
1665}
1666function updateFieldState($dt)
1667{
1668	const $field = $dt.find('[name="fields[]"]');
1669	const $categories = $dt.next('dd').find('[name="categories[]"]');
1670	let count = 0;
1671	let checked = false;
1672
1673	$categories.each(function(){
1674		if ($(this).prop('checked')) {
1675			checked = true;
1676			count += Number($(this).closest('label').children('.count').text()) || 0;
1677		}
1678	});
1679
1680	$field.prop('checked', checked);
1681	$dt.children('.count').text(count);
1682}
1683function initReviewWindowEvents()
1684{
1685	const $overLayer = $('#overLayer');
1686
1687	$overLayer.on('click', '#review-checkbox-wrap .accordion dt', function(){
1688		$(this).toggleClass('open');
1689	});
1690	$overLayer.on('click', '#review-checkbox-wrap [name="check_all"]', function(){
1691		const f = $(this).html() == 'ON';
1692		$('#review-checkbox-wrap [type="checkbox"]:not(.dummy)').prop('checked', f);
1693		updateQuestionCount();
1694	});
1695	$overLayer.on('click', '#review-checkbox-wrap [name="fields[]"], #review-checkbox-wrap .accordion dt>.count', function(e){
1696		e.stopPropagation();
1697
1698		const $dt = $(this).closest('dt');
1699		const $field = $dt.find('[name="fields[]"]');
1700		const checked = $(this).is('[name="fields[]"]') ? $field.prop('checked') : !$field.prop('checked');
1701
1702		$field.prop('checked', checked);
1703		$dt.next('dd').find('[name="categories[]"]').prop('checked', checked);
1704
1705		updateQuestionCount($dt);
1706	});
1707	$overLayer.on('click', '#review-checkbox-wrap [name="categories[]"]', function(){
1708		const $dt = $(this).closest('dd').prev('dt');
1709		updateQuestionCount($dt);
1710	});
1711
1712	$overLayer.on('click', '#review_ok, #unanswer_ok, #bookmark_ok', function(){
1713		let mode;
1714		if ($('#review_ok')[0]) {
1715			mode = 'review';
1716		} else if ($('#unanswer_ok')[0]) {
1717			mode = 'unanswer';
1718		} else if ($('#bookmark_ok')[0]) {
1719			mode = 'bookmark';
1720		}
1721
1722		$('[name="mode"]', $('#configform')).val(mode);
1723
1724		//復習対象の選択分野と分野指定の選択分野を同期させる
1725		const selectedCategories = $('#review-checkbox-wrap input[name="categories[]"]:checked').map(function(){
1726				return this.value;
1727			}).get();
1728		$('#configform .input_categories input[name="categories[]"]').val(selectedCategories);	
1729
1730		$('#configform').submit();
1731	})
1732	$overLayer.on('click', '#review_cancel, #unanswer_cancel, #bookmark_cancel', function(){
1733		hideModal();
1734	})
1735}
1736
1737$('body').on('click', '#review', function(){
1738	if (!uid || !$('#loginBtn').hasClass('login')) {
1739		showStateMessage('<i class="caution"></i>不正なリクエストです。');
1740		return false;
1741	}
1742	const $i = $('i', $(this));
1743	loadUserlog($i).done(function(){
1744		review();
1745	});
1746	return false;
1747})
1748async function review()
1749{
1750	const q2c = await getQ2c();
1751
1752	const answered = {};
1753	for (let i=0,len=userlog.length; i<len; i++) {
1754		if (!userlog[i] || userlog[i].indexOf(',0,') !== -1) continue;
1755
1756		const qdata = userlog[i].split(',');
1757		const qid = qdata[1];
1758		const categoryId = q2c[qid] ?? qdata[3];
1759
1760		if (!categoryId) continue;
1761
1762		if (!answered[categoryId]) {
1763			answered[categoryId] = {};
1764		}
1765		answered[categoryId][qid] = qdata[2];
1766	}
1767
1768	questionCounts = {};
1769	for (const categoryId in answered) {
1770		questionCounts[categoryId] = Object.values(answered[categoryId]).filter(function(val){
1771			return val == 2;
1772		}).length;
1773	}
1774
1775	const html =
1776`<h2>間違えた問題の復習</h2>
1777<p style="margin:10px">直前の1回が不正解だった問題(達成度レポートでの<i class="rank1"></i>)だけを出題することができます。</p>
1778${getConfirmTable()}
1779<div id="floatBtn"><button id="review_cancel">キャンセル</button><button id="review_ok">復習開始</button></div>`;
1780
1781	showModal(html, 'review');
1782	initCounts = true;
1783	updateQuestionCount();
1784}
1785$('body').on('click', '#unanswer', function(){
1786	if (!uid || !$('#loginBtn').hasClass('login')) {
1787		showStateMessage('<i class="caution"></i>不正なリクエストです。');
1788		return false;
1789	}
1790	const $i = $('i', $(this));
1791	loadUserlog($i).done(function(){
1792		unanswer();
1793	});
1794	return false;
1795})
1796async function unanswer()
1797{
1798	const q2c = await getQ2c();
1799
1800	if (!bunyaCounts) {
1801		bunyaCounts = {};
1802		for (const qid in q2c) {
1803			const categoryId = q2c[qid];
1804			if (bunyaCounts[categoryId] === undefined) {
1805				bunyaCounts[categoryId] = 0;
1806			}
1807			bunyaCounts[categoryId]++;
1808		}
1809	}
1810	
1811	const answered = {};
1812	for (let i=0, len=userlog.length; i<len; i++) {
1813		if (!userlog[i] || userlog[i].indexOf(',0,') !== -1) continue;
1814
1815		const qdata = userlog[i].split(',');
1816		const qid = qdata[1];
1817		const categoryId = q2c[qid];
1818
1819		if (!categoryId) continue;
1820
1821		if (!answered[categoryId]) {
1822			answered[categoryId] = new Set();
1823		}
1824		answered[categoryId].add(qid);
1825	}
1826
1827	questionCounts = {};
1828	for (const categoryId in answered) {
1829		questionCounts[categoryId] = answered[categoryId].size;
1830	}
1831
1832	const html =
1833`<h2>未回答問題の出題</h2>
1834<p style="margin:10px">現在までに回答歴がない問題だけを出題することができます。</p>
1835${getConfirmTable()}
1836<div id="floatBtn"><button id="unanswer_cancel">キャンセル</button><button id="unanswer_ok">出題開始</button></div>`;
1837
1838	showModal(html, 'review');
1839	initCounts = true;
1840	updateQuestionCount();
1841}
1842$('body').on('click', '#bookmark', function(){
1843	if (!uid || !$('#loginBtn').hasClass('login')) {
1844		showStateMessage('<i class="caution"></i>不正なリクエストです。');
1845		return false;
1846	}
1847	const $i = $('i', $(this));
1848	loadBookmarkData($i).done(function(){
1849		bookmark();
1850	});
1851	return false;
1852})
1853async function bookmark() {
1854	const q2c = await getQ2c();
1855
1856	const bookmarkCount = bookmarks.length || 0;
1857	const bookmarked = {};
1858	for (let i=0; i<bookmarkCount; i++) {
1859		const qid = bookmarks[i];
1860		const categoryId = q2c[qid];
1861
1862		if (!categoryId) continue;
1863
1864		if (!bookmarked[categoryId]) {
1865			bookmarked[categoryId] = {};
1866		}
1867		bookmarked[categoryId][qid] = 1; //キーのリストが出来ればいいので入れるデータは何でもOK
1868	}
1869
1870	questionCounts = {};
1871	for (const categoryId in bookmarked) {
1872		questionCounts[categoryId] = Object.keys(bookmarked[categoryId]).length;
1873	}
1874
1875	const html =
1876`<h2>チェックした問題の復習</h2>
1877<p style="margin:10px">チェックが付いている問題だけを出題することができます。</p>
1878${getConfirmTable()}
1879<div id="floatBtn"><button id="bookmark_cancel">キャンセル</button><button id="bookmark_ok">出題開始</button></div>`;
1880
1881	showModal(html, 'review');
1882	initCounts = true;
1883	updateQuestionCount();
1884}
1885
1886let questionCounts = {}, bunyaCounts = null, initCounts = false;
1887function updateQuestionCount($dt)
1888{
1889	let mode = null, questionCount = 0, str = '', zeroMessage = '';
1890
1891	if ($('#review_ok')[0]) {
1892		mode = 'review';
1893		str = '復習対象';
1894	} else if ($('#unanswer_ok')[0]) {
1895		mode = 'unanswer';
1896		str = '未回答';
1897	} else if ($('#bookmark_ok')[0]) {
1898		mode = 'bookmark';
1899		str = 'チェック済';
1900	}
1901
1902	if (initCounts) {
1903		const $wrap = $('#review-checkbox-wrap');
1904		$wrap.find('[name="categories[]"]').each(function(){
1905			const categoryId = $(this).val();
1906			let count = 0;
1907
1908			if (mode == 'unanswer') {
1909				count = (bunyaCounts[categoryId] ?? 0) - (questionCounts[categoryId] ?? 0);
1910			} else {
1911				count = questionCounts[categoryId] ?? 0;
1912			}
1913			$(this).closest('label').children('.count').text(count);
1914		});
1915		initCounts = false;
1916	}
1917
1918	if ($dt && $dt.length) {
1919		updateFieldState($dt);
1920	} else {
1921		$('#review-checkbox-wrap .accordion dt').each(function(){
1922			updateFieldState($(this));
1923		});
1924	}
1925
1926	$('#review-checkbox-wrap input[name="categories[]"]:checked').each(function(){
1927		const categoryId = $(this).val();
1928		
1929		if (mode == 'review' || mode == 'bookmark') {
1930			if (questionCounts[categoryId] === undefined) return true;
1931			questionCount += questionCounts[categoryId];
1932		} else {
1933			questionCount += (bunyaCounts[categoryId] ?? 0) - (questionCounts[categoryId] ?? 0);
1934		}
1935	});
1936
1937	if (questionCount === 0) {
1938		zeroMessage = ' ※'+str+'の問題はありません。';
1939		$('#'+mode+'_ok').prop('disabled', true);
1940	} else {
1941		$('#'+mode+'_ok').prop('disabled', false);
1942	}
1943	$('#questionCount').html(questionCount+'問'+zeroMessage);
1944}
1945$('body').on('click', '.rank-review-btn', function(){
1946	if ($(this).hasClass('action')) {
1947		const mode = $(this).data('target');
1948		$('[name="mode"]', $('#configform')).val(mode);
1949		$('#configform').trigger("submit", [true]);
1950	} else {
1951		$('.rank-review-btn').removeClass('action').text('復習');
1952		$(this).addClass('action').html("開始");
1953	}
1954})
1955//bookmark変更マーク関連
1956function bookmarkChange(target, state)
1957{
1958	if (!/^([0-9]{1,2}_[0-9]{1,2},?)*[0-9]{1,2}_[0-9]{1,2}$/.test(target) || $.inArray(state, ["true", "false"]) === -1) return false;
1959	var dfd = new $.Deferred();
1960	var inputParams = {
1961		"target" : target,
1962		"state" : state,
1963		"action": "bookmark"
1964	};
1965	$.ajax({
1966		type : "POST",
1967		url : ajaxScript,
1968		cache : false,
1969		data : $.param(inputParams),
1970		dataType : "json"})
1971	.done(function(data){
1972		if (data.status == "success") {
1973			bookmark_change = true;
1974			dfd.resolve();
1975		} else if (data.status == "error") {
1976			dfd.reject();
1977		}
1978	}).fail(function(d, s){
1979		dfd.reject();
1980	})
1981	return dfd.promise();
1982}
1983let bookmarks = [], bookmark_change = true;
1984function loadBookmarkData($i) {
1985	var dfd = new $.Deferred();
1986	if (!bookmark_change) {
1987		dfd.resolve();
1988		return dfd.promise();
1989	}
1990
1991	const iClass = $i.attr('class');
1992	$i.attr('class', 'loading large');
1993
1994	var inputParams = {
1995		"action" : "loadbookmark"
1996	};
1997	$.ajax({
1998		type : "POST",
1999		url : ajaxScript,
2000		cache : false,
2001		data : $.param(inputParams),
2002		dataType : "json"})
2003	.done(function(data){
2004		if (data.status == "success") {
2005			bookmarks = data.bookmarkdata;
2006			bookmark_change = false;
2007			dfd.resolve();
2008		} else if (data.status == "error") {
2009			switch (data.errorcode) {
2010				case 1: showStateMessage('<i class="caution"></i>チェックリストの形式が不正です。'); break;
2011				case 2: showStateMessage('<i class="caution"></i>チェックリストの取得に失敗しました。'); break;
2012				case 3: showStateMessage('<i class="caution"></i>接続のタイムアウトにより取得に失敗しました。'); break;
2013			}
2014			dfd.reject();
2015		}
2016	}).fail(function(d, s){
2017		showStateMessage('<i class="ng"></i>['+s+']サーバとの通信に失敗しました。');
2018		dfd.reject();
2019	}).always(function(){
2020		$i.attr('class', iClass);
2021	});
2022	return dfd.promise();
2023}
2024$('#bookmarkBtn_checkbox').change(function() {
2025	if (!uid || !$('#loginBtn').hasClass('login')) {
2026		return false;
2027	}
2028	var _this = $(this);
2029	var target = _this.attr('data-key');
2030	var state = _this.prop('checked') ? 'true' : 'false';
2031	_this.prop('disabled', true);
2032	bookmarkChange(target, state).always(function(){
2033		_this.prop('disabled', false);
2034	});
2035})
2036$('body').on('click', '#bookmark_edit', function(){
2037	if (!uid || !$('#loginBtn').hasClass('login')) {
2038		showStateMessage('<i class="caution"></i>不正なリクエストです。');
2039		return false;
2040	}
2041	const $i = $('i', $(this));
2042	loadBookmarkData($i).done(function(){
2043		bookmark_edit();
2044	});
2045	return false;
2046})
2047async function bookmark_edit() {
2048	const q2c = await getQ2c();
2049
2050	const bookmarked = {};
2051	for (let i=1; i<=bunyalen; i++) {
2052		bookmarked[i] = new Set();
2053	}
2054
2055	const bookmarkCount = bookmarks.length || 0;
2056	for (let i=0; i<bookmarkCount; i++) {
2057		const qid = bookmarks[i];
2058		const categoryId = q2c[qid];
2059		if (!categoryId) continue;
2060
2061		const fieldId = categoryId.split('_')[0]
2062		bookmarked[fieldId].add(qid);
2063	}
2064
2065	let bookmarkList = [], li = '';
2066	for (const i in bookmarked) {
2067		bookmarkList[i] = [...bookmarked[i]];
2068		bookmarkList[i].sort(function(a, b){
2069			return parseInt(a) < parseInt(b) ? -1 : 1;
2070		});
2071		for (let j=0,len=bookmarkList[i].length; j<len; j++) {
2072			if (j == 0) {
2073				li += '<dt class="c'+i+'">'+bunyalist[i][0]+'</dt>';
2074			}
2075			const qid = bookmarkList[i][j];
2076			const qidObj = splitQid(qid);
2077			const dirname = getDirname(qidObj.eid), q = qidObj.q;
2078			const year_wa = AD2year_wa(dirname, true);
2079			li += '<dd class="c'+i+'"><label><input type="checkbox" value="'+qid+'">'+year_wa+' 問'+q+'</label><a href="/kakomon/'+dirname+'/'+('0'+q).slice(-2)+'.html" target="_blank">&#8689;</a></dd>';
2080		}
2081	}
2082
2083	if (li) {
2084		li = 
2085`<style>
2086#overLayer.review{width:650px;height:600px;display:flex;flex-direction:column}
2087#review-checkbox-wrap{flex:1;overflow:auto;scrollbar-gutter:stable;margin-bottom:72px}
2088#floatBtn{bottom:0 !important;padding:15px 0;background:var(--main-bg)}
2089.checkEditWrap{display:flex;align-items:center;padding:3px 5px}
2090#checkDelete{margin-left:10px;display:none;padding:3px 12px 3px 8px}
2091#checkDelete .material-symbols-outlined{font-size:24px;vertical-align:-7px;margin-right:2px}
2092@media(max-width:770px){
2093.checkEditWrap{padding:8px 0}
2094}
2095</style>
2096<div class="checkEditWrap">
2097<label style="padding:5px"><input type="checkbox" name="check-edit-all"></label>
2098<span id="checkCount">すべて選択</span>
2099<button id="checkDelete"><span class="material-symbols-outlined">delete</span>削除</button>
2100</div>
2101<dl class="confirmTable bookmarkList">${li}</dl>`;
2102	} else {
2103		li = emptySVG('チェック済の問題がありません');
2104	}
2105	const html = '<h2>チェックした問題の管理</h2>'+li;
2106
2107	showModal(html, 'review');
2108}
2109$('#overLayer').on('change', '.bookmarkList [type="checkbox"]', function(){
2110	if ($(this).prop('checked')){
2111		$(this).closest('dd').addClass('checked');
2112	} else {
2113		$(this).closest('dd').removeClass('checked');
2114	}
2115	const checkedCount = $('.bookmarkList [type="checkbox"]:checked').length;
2116	if (checkedCount > 0){
2117		$('#checkCount').html(checkedCount+' selected');
2118		$('#checkDelete').show();
2119	} else {
2120		$('#checkCount').html('すべて選択');
2121		$('#checkDelete').hide();
2122	}
2123})
2124$('#overLayer').on('change', '[name="check-edit-all"]', function(){
2125	var f = $(this).prop('checked');
2126	$('.bookmarkList [type="checkbox"]').prop('checked', f).change();
2127})
2128$('#overLayer').on('click', '#checkDelete', function(){
2129	if (!confirm('チェックした項目をリストから削除します。')) return false;
2130
2131	var arr = [];
2132	$('.bookmarkList [type="checkbox"]:checked').each(function(){
2133		arr.push($(this).val());
2134	})
2135	if (arr.length === 0) return false;
2136
2137	showStateMessage('<i class="loading"></i>削除しています。');
2138
2139	var promise = bookmarkChange(arr.join(','), 'false');
2140	promise.done(function(){
2141		$('.checkEdit').css('visibility', 'hidden');
2142		$('.bookmarkList [type="checkbox"]:checked').closest('dd').fadeOut('fast', function(){
2143			$(this).remove();
2144			if ($('.bookmarkList dd').length === 0){
2145				$('.checkEditWrap').replaceWith(emptySVG('チェック済の問題がありません'));
2146			}
2147		});
2148		$('.bookmarkList dt').each(function(){
2149			if ($('.'+$(this).attr('class')+':not(:animated)').length === 1) {
2150				$(this).fadeOut('fast', function(){
2151					$(this).remove();
2152				});
2153			}
2154		})
2155		//出題中の問題のブックマークボタンをOFFにする
2156		if (arr.indexOf($('#bookmarkBtn_checkbox').attr('data-key')) !== -1) {
2157			$('#bookmarkBtn_checkbox').prop('checked', false);
2158		}
2159		showStateMessage('<i class="ok"></i>'+arr.length+'項目を削除しました。');
2160	});
2161	promise.fail(function(){
2162		showStateMessage('<i class="ng"></i>サーバとの通信に失敗しました。');
2163	});
2164})
2165$('#overLayer').on('click', '#csv-download', function(){
2166	$('#csvForm').remove();
2167	
2168	var d = [];
2169	var target = $('[name="targetDate"]').val();
2170	if (!target) {
2171		var _q = $('#configform [name="q"]').val().split(','),
2172			_r = $('#configform [name="r"]').val().split(','),
2173			_c = $('#configform [name="c"]').val().split(',');
2174		if (!isErrorPage()) {
2175			_q.pop();
2176			_c.pop();
2177		}
2178		var time = Math.floor(Date.now() / 1000);
2179		for (i=0,len=_q.length; i<len; i++) {
2180			d.push(time+','+_q[i]+','+_r[i]+','+_c[i]);
2181		}
2182	} else {
2183		d = narrowData(target);
2184	}
2185	d = d.join('|');
2186
2187	$('<form/>', {
2188		action: './php/csv_download.php',
2189		method: 'post',
2190		id: 'csvForm',
2191	})
2192	.append($('<input/>', {type:'hidden', name:'data', value:d}))
2193	.append($('<input/>', {type:'hidden', name:'app', value:'doujou'}))
2194	.appendTo(document.body)
2195	.submit();
2196
2197	return false;
2198})
2199//userlogから範囲を切り出す
2200function narrowData(target) {
2201	var filtered = [];
2202	if ($('[name="times[]"][value="'+target+'"]').length) {
2203		var ptn = new RegExp('^[0-9]{10},'+target+'_');
2204		filtered = $.grep(userlog, function(val) {
2205			return ptn.test(val);
2206		});
2207	} else {
2208		var st, ed;
2209		const now = new Date();
2210		const y = now.getFullYear(),
2211			m = now.getMonth(),
2212			d = now.getDate();
2213		switch (target) {
2214		case 'today':
2215			st = new Date(y, m, d).getTime();
2216			ed = new Date(y, m, d+1).getTime();
2217			break;
2218		case 'yesterday':
2219			st = new Date(y, m, d-1).getTime();
2220			ed = new Date(y, m, d).getTime();
2221			break;
2222		case 'thisMonth':
2223			st = new Date(y, m).getTime();
2224			ed = new Date(y, m+1).getTime();
2225			break;
2226		case 'lastMonth':
2227			st = new Date(y, m-1).getTime();
2228			ed = new Date(y, m).getTime();
2229			break;
2230		case 'past7days':
2231			st = new Date(y, m, d-7).getTime();
2232			ed = new Date(y, m, d).getTime();
2233			break;
2234		case 'past30days':
2235			st = new Date(y, m, d-30).getTime();
2236			ed = new Date(y, m, d).getTime();
2237			break;
2238		case 'past90days':
2239			st = new Date(y, m, d-90).getTime();
2240			ed = new Date(y, m, d).getTime();
2241			break;
2242		case 'past180days':
2243			st = new Date(y, m, d-180).getTime();
2244			ed = new Date(y, m, d).getTime();
2245			break;
2246		case 'past1years':
2247			st = new Date(y-1, m, d).getTime();
2248			ed = new Date(y, m, d).getTime();
2249			break;
2250		case 'custom':
2251			const stVal = $('[name="startDate"]').val();
2252			const edVal = $('[name="endDate"]').val();
2253			if (isValidDateString(stVal) && isValidDateString(edVal)) {
2254				if (stVal > edVal) {
2255					showStateMessage('<i class="caution"></i>終了日が開始日よりも昔の日付です。');
2256					return filtered;
2257				}
2258				const stYmd = stVal.split('-');
2259				const edYmd = edVal.split('-');
2260				// + で数値に変換してから加減しないと文字列の連結になってしまう
2261				st = new Date(+stYmd[0], +stYmd[1]-1, +stYmd[2]).getTime();
2262				ed = new Date(+edYmd[0], +edYmd[1]-1, +edYmd[2]+1).getTime();
2263
2264				localStorage.setItem('CUSTOM_START_DATE', stVal);
2265				localStorage.setItem('CUSTOM_END_DATE', edVal);
2266			}
2267			break;
2268		case 'all': return userlog;
2269		}
2270		//秒単位に変換
2271		st = Math.floor(st / 1000);
2272		ed = Math.floor(ed / 1000);
2273		filtered = $.grep(userlog, function(val, idx) {
2274			var time = val.split(',')[0];
2275			return time >= st && time < ed;
2276		});
2277	}
2278	return filtered;
2279}
2280let allTimesCache = null;
2281function getAllTimes() {
2282	if (!allTimesCache) {
2283		allTimesCache = $('[name="times[]"]').map(function(){
2284			return $(this).val();
2285		}).get().sort((a, b) => a.localeCompare(b, undefined, {numeric: true, sensitivity: 'base'}));
2286	}
2287	return [...allTimesCache];
2288}
2289function isErrorPage() {
2290	return $('#errorPage')[0];
2291}
2292function isMoshi() {
2293	return mode == 3;
2294}
2295function isReview() {
2296	return $('#configform [name="mode"]').val() == 'review';
2297}
2298function isUnanswer() {
2299	return $('#configform [name="mode"]').val() == 'unanswer';
2300}
2301function isBookmark() {
2302	return $('#configform [name="mode"]').val() == 'bookmark';
2303}
2304Date.prototype.toyyyymmdd = function(fDemiliter) {
2305	var dem = (fDemiliter) ? '-' : '';
2306	return this.getFullYear() + dem + ('0' + (this.getMonth()+1)).slice(-2) + dem + ('0' + this.getDate()).slice(-2);
2307}
2308function isValidDateString(dateStr) {
2309	if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return false;
2310	
2311	const [y, m, d] = dateStr.split('-').map(Number);
2312	const date = new Date(y, m - 1, d);
2313	
2314	return (
2315		date.getFullYear() === y &&
2316		date.getMonth() === m - 1 &&
2317		date.getDate() === d
2318	);
2319}
2320})

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.