let loop_colors = [ '#7f0000', '#006400', '#708090', '#808000', '#483d8b', '#3cb371', '#00008b', '#32cd32', '#7f007f', '#b03060', '#ff4500', '#00ced1', '#ffa500', '#00ff00', '#00fa9a', '#8a2be2', '#dc143c', '#00bfff', '#f4a460', '#0000ff', '#f08080', '#adff2f', '#ff00ff', '#1e90ff', '#f0e68c', '#ffff54', '#ff1493', '#7b68ee', '#ee82ee', '#dcdcdc' ]; let length_colors = ['#3293e2', '#3ad46b', '#facb2a', '#ff8132', '#843a53']; window.onload = function(e) { change_color_mode(); draw(); } function reset() { document.getElementById('select').innerHTML = ''; document.getElementById('arrows').innerHTML = ''; let lw = document.getElementById('linewidth').value; let hover = document.getElementById('hover').checked; let msg = document.getElementById('message'); let main = document.getElementById('loops'); let arrow = document.getElementById('arrows'); let parent = document.getElementById('img'); msg.title = ''; msg.innerHTML = ''; main.innerHTML = ''; main.setAttribute('stroke-width', lw + 'px'); if (hover) { parent.classList.add('hover'); parent.style.cursor = 'crosshair'; arrow.setAttribute('onclick', 'loop_hover_click(this)'); main.setAttribute('onclick', 'loop_hover_click(this)'); } else { parent.classList.remove('hover'); parent.removeAttribute('style'); arrow.removeAttribute('onclick'); main.removeAttribute('onclick'); } } function draw(changes_graph=false) { let sel = document.getElementById('select') let prev_sel = sel.value; reset(); let colorMode = document.getElementById('color').value; let mul = document.getElementById('multiply').value; let mod = document.getElementById('modulo').value; // draw background circle and numbers / marks let radius = 440; let center = radius + 60; let circle_points = get_circle_points(center, radius, mod); draw_background(center, radius, circle_points); // calculate loop groups let loops = get_loops(mul, mod); // (optionally) draw arrow heads if (document.getElementById('arrow').checked) { let lw = document.getElementById('linewidth').value; draw_arrows(loops, circle_points, lw); } // draw either individual lines or chained loop if (colorMode == 'segment') { draw_lines_separate(loops, circle_points); } else { draw_lines_group(loops, circle_points); } set_selection_options(); // apply coloring if (colorMode == 'loop') { set_colors_loop(); } else if (colorMode == 'length') { set_colors_loop_length(mod); } else if (colorMode == 'segment') { set_colors_line_length(circle_points); } else if (colorMode == 'select') { if (prev_sel && !changes_graph) { sel.value = prev_sel; } update_highlight(); } } // ----------------- // Main loop drawing // ----------------- function get_circle_points(cp, radius, divisor) { var points = []; let piece = 2 * Math.PI / divisor; for (var i = 0; i < divisor; i++) { let x = radius * Math.cos(i * piece - Math.PI / 2); let y = radius * Math.sin(i * piece - Math.PI / 2); points.push([cp + x, cp + y]); } return points; } function get_loops(multiplier, modulo) { var all_loops = []; var visited = [0]; for (var i = 1; i < modulo; i++) { if (visited.includes(i)) continue; visited.push(i); var one_loop = [i]; var u = i; while (true) { u = (u * multiplier) % modulo; one_loop.push(u); if (visited.includes(u)) break; visited.push(u); } all_loops.push(one_loop); } return all_loops; } function draw_background(cp, radius, points) { let fs = document.getElementById('fontsize').value; // show every n-th text let max_chr_count = 1 + Math.floor(Math.log10(points.length - 1)); let label_dist = dist(points[0], points[1]); let label_mod = Math.floor(fs / 2 * max_chr_count / label_dist) + 1; // draw circle var src_txt = ''; var src_bg = `\n`; for (var i = 0; i < points.length; i++) { let x = points[i][0] - cp; let y = points[i][1] - cp; if (fs > 0 && i % label_mod == 0) { let chr_count = i < 1 ? 1 : 1 + Math.floor(Math.log10(i)); let mi = 1.01; // inner line startpoint multiply let mo = 1.05; // outer line endpoint multiply src_bg += `\n`; // or // additional font offset let ls_fix = .15 * 2 * (chr_count-1); // letter spacing fix let fsdx = chr_count * fs / (3.35 + ls_fix) * (1-x/radius*1.2); let fsdy = fs / 3.35 * (1+y/radius*1.5); src_txt += `\n${i}`; } } let bgtxt = document.getElementById('bgtxt'); bgtxt.setAttribute('font-size', fs + 'px'); document.getElementById('bg').innerHTML = src_bg + '\n'; bgtxt.innerHTML = src_txt + '\n'; } function draw_arrows(loops, points, linewidth) { var txt = ''; for (var o = 0; o < loops.length; o++) { let loop = loops[o]; txt += '\n'; } document.getElementById('arrows').innerHTML = txt + '\n'; } function draw_lines_group(loops, points) { var txt = ''; for (var o = 0; o < loops.length; o++) { let loop = loops[o]; txt += '\n`; } document.getElementById('loops').innerHTML = txt + '\n'; } // ------------------- // Color by loop group // ------------------- function set_colors_loop() { let children = document.getElementById('loops').children; // colors for (var i = 0; i < children.length; i++) { children[i].setAttribute('stroke', loop_colors[i % loop_colors.length]); } // info message document.getElementById('message').innerHTML = children.length + ' loops'; } // -------------------- // Color by loop length // -------------------- function set_colors_loop_length(total) { let split = 5 * (1 + Math.floor(Math.log10(total))); // colors let children = document.getElementById('loops').children; for (var i = 0; i < children.length; i++) { let count = children[i].getAttribute('data-loop').split(',').length; let idx = Math.min(Math.floor(count / total * split), length_colors.length - 1); children[i].setAttribute('stroke', length_colors[idx]); } // info message var info = ''; for (var i = 0; i < length_colors.length; i++) { let lower = Math.ceil(i * total / split); let upper; if (i + 1 == length_colors.length) { upper = total; } else { upper = Math.floor(((i + 1) * total - 1) / split); } if (lower > upper || upper < 2) continue; info += ' , ' + color_label(i, lower + ' – ' + upper); } document.getElementById('message').innerHTML = info.slice(3); } // -------------------- // Color by line length // -------------------- function draw_lines_separate(loops, points) { var txt = ''; for (var o = 0; o < loops.length; o++) { let loop = loops[o]; txt += '\n'; for (var i = 1; i < loop.length; i++) { let p1 = points[loop[i - 1]]; let p2 = points[loop[i]]; txt += '\n'; } txt += '\n'; } document.getElementById('loops').innerHTML = txt + '\n'; } function set_colors_line_length(points) { let min = dist(points[0], points[1]); let max = Math.max(1, dist(points[0], points[Math.floor(points.length/2)]) - min); var counter = []; var count_dots = 1; // 0 * x is always a point // colors let children = document.querySelectorAll('#loops path'); for (var i = 0; i < children.length; i++) { let len = children[i].getAttribute('data-len') ; if (len == 0) { ++count_dots; continue; } let percent = (len - min) / max; // OR len / (2 * radius); let idx = Math.round((1 - percent) * (length_colors.length - 1)); children[i].setAttribute('stroke', length_colors[idx]); counter[idx] = (counter[idx] || 0) + 1; } // info message var info = ''; for (var i = 0; i < length_colors.length; i++) { info += ' , ' + color_label(i, (counter[i] || 0) + ' lines'); } info += ' , [' + count_dots + ' points]'; document.getElementById('message').innerHTML = info.slice(3); } // ----------------------------------- // Color by selection (highlight mode) // ----------------------------------- function set_selection_options() { var txt = ''; let children = document.getElementById('loops').children; for (var i = 0; i < children.length; i++) { let loop = children[i].getAttribute('data-loop').split(','); let abbrev; if (loop.length > 10) { abbrev = loop.slice(0, 5).join(',') + ` + ${loop.length - 5} more`; } else { abbrev = loop.join(','); } txt += ``; } document.getElementById('select').innerHTML = txt; } function loop_hover_click(sender) { var node = sender.querySelector(':hover'); var indexOfChild = 0; while (node = node.previousSibling) { if (node.nodeType === 1) { ++indexOfChild; } } document.getElementById('select').value = indexOfChild; let color = document.getElementById('color'); if (color.value == 'select') { update_highlight(); } else { color.value = 'select'; change_color_mode(); draw(); } } function update_highlight() { if (document.getElementById('color').value != 'select') { return; } remove_highlight(); let msg = document.getElementById('message'); let idx = document.getElementById('select').value; let child = document.getElementById('loops').children[idx]; let arrow = document.getElementById('arrows').children[idx]; if (!child) { msg.innerHTML = ''; return; } child.classList.add('selected'); if (arrow) arrow.classList.add('selected'); let txt = child.getAttribute('data-loop'); msg.innerHTML = txt.length > 52 ? txt.slice(0, 50) + '…' : txt; msg.title = txt; } function remove_highlight() { let selected = document.querySelectorAll('.selected'); for (var i = selected.length - 1; i >= 0; i--) { selected[i].classList.remove('selected'); } } // --------------------------- // Input interaction callbacks // --------------------------- function change_color_mode() { let colorMode = document.getElementById('color').value; document.getElementById('select').hidden = colorMode != 'select'; } function svg_download() { let mul = document.getElementById('multiply').value; let mod = document.getElementById('modulo').value; let svg = document.getElementById('img').innerHTML; let blob = new Blob([svg], {type:'image/svg+xml'}); var tmp = document.createElement('a'); tmp.href = window.URL.createObjectURL(blob); tmp.download = `graph_x${mul}_mod${mod}.svg`; document.body.appendChild(tmp); tmp.click(); document.body.removeChild(tmp); } // -------------- // Helper methods // -------------- function f3(num) { return Math.floor(num * 10) / 10; } function P(p1, p2) { return f3(p1) + ',' + f3(p2); } function color_label(idx, label) { return '' + label + ''; } function dist(p1, p2) { let dx = p1[0] - p2[0]; let dy = p1[1] - p2[1]; return Math.sqrt(dx * dx + dy * dy); } function arrow_path(p1, p2, linewidth) { let arrow_width = linewidth * 2.5; let arrow_length = arrow_width * 3; let arrow_offset = -arrow_length - 20; let diff = dist(p1, p2); let xnorm = (p1[0] - p2[0]) / diff; let ynorm = (p1[1] - p2[1]) / diff; let x1 = p1[0] + arrow_offset * xnorm; let y1 = p1[1] + arrow_offset * ynorm; let x2 = xnorm * arrow_length - ynorm * arrow_width; let y2 = ynorm * arrow_length + xnorm * arrow_width; let x3 = +ynorm * 2 * arrow_width; let y3 = -xnorm * 2 * arrow_width; return 'M' + P(x1, y1) + 'l' + P(x2, y2) + 'l' + P(x3, y3) + 'Z'; }