| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
|
|
||
|
|
||
|
|
||
| // Compute x and y axes limits | ||
| function compute_limits(chart) { | ||
|
|
||
| var min_x, min_y, max_x, max_y, gap_x, gap_y; | ||
| var settings = chart.settings(); | ||
| var data = chart.data(); | ||
|
|
||
| // x and y limits | ||
| if (settings.xlim === null) { | ||
| min_x = d3v6.min(data, d => d.x); | ||
| max_x = d3v6.max(data, d => d.x); | ||
| gap_x = (max_x - min_x) * 0.2; | ||
| if (min_x == max_x) { | ||
| min_x = min_x * 0.8; | ||
| max_x = max_x * 1.2; | ||
| gap_x = 0; | ||
| } | ||
| if (min_x == 0 && max_x == 0) { | ||
| min_x = -1; | ||
| max_x = 1; | ||
| gap_x = 0.1; | ||
| } | ||
| } else { | ||
| min_x = settings.xlim[0]; | ||
| max_x = settings.xlim[1]; | ||
| gap_x = 0; | ||
| } | ||
| if (settings.ylim === null) { | ||
| min_y = d3v6.min(data, d => d.y); | ||
| max_y = d3v6.max(data, d => d.y); | ||
| gap_y = (max_y - min_y) * 0.2; | ||
| if (min_y == max_y) { | ||
| min_y = min_y * 0.8; | ||
| max_y = max_y * 1.2; | ||
| gap_y = 0; | ||
| } | ||
| if (min_y == 0 && max_y == 0) { | ||
| min_y = -1; | ||
| max_y = 1; | ||
| gap_y = 0.1; | ||
| } | ||
| } else { | ||
| min_y = settings.ylim[0]; | ||
| max_y = settings.ylim[1]; | ||
| gap_y = 0; | ||
| } | ||
|
|
||
| min_x = settings.x_log ? min_x * 0.8 : min_x - gap_x; | ||
| max_x = settings.x_log ? max_x * 1.3 : max_x + gap_x; | ||
| min_y = settings.y_log ? min_y * 0.9 : min_y - gap_y; | ||
| max_y = settings.y_log ? max_y * 1.1 : max_y + gap_y; | ||
|
|
||
| // Fixed ratio | ||
| var range_x = max_x - min_x; | ||
| var mid_x = (max_x + min_x) / 2; | ||
| var range_y = max_y - min_y; | ||
| var mid_y = (max_y + min_y) / 2; | ||
| if (settings.fixed && settings.xlim === null && settings.ylim === null) { | ||
| var ratio = (range_y / range_x); | ||
| if (ratio > 1) { | ||
| range_x = range_x * ratio; | ||
| min_x = mid_x - range_x / 2; | ||
| max_x = mid_x + range_x / 2; | ||
| } else { | ||
| range_y = range_y / ratio; | ||
| min_y = mid_y - range_y / 2; | ||
| max_y = mid_y + range_y / 2; | ||
| } | ||
| } | ||
| if (settings.fixed && settings.xlim != null) { | ||
| range_y = range_x; | ||
| min_y = mid_y - range_y / 2; | ||
| max_y = mid_y + range_y / 2; | ||
| } | ||
| if (settings.fixed && settings.ylim != null) { | ||
| range_x = range_y; | ||
| min_x = mid_x - range_x / 2; | ||
| max_x = mid_x + range_x / 2; | ||
| } | ||
|
|
||
| return {min_x: min_x, max_x: max_x, min_y: min_y, max_y: max_y}; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| // Compute and setup scales | ||
| function setup_scales(chart) { | ||
|
|
||
| var scales = {}; | ||
| var settings = chart.settings(); | ||
| var dims = chart.dims(); | ||
| var data = chart.data(); | ||
|
|
||
| // if data is empty | ||
| if (data.length == 0) { | ||
| settings.x_categorical = false; | ||
| settings.y_categorical = false; | ||
| chart.settings(settings); | ||
| chart.data([{ x: 0, y: 0, key: 1 }]); | ||
| } | ||
|
|
||
| var limits = compute_limits(chart); | ||
|
|
||
| // x, y scales | ||
| if (!settings.x_categorical) { | ||
| scales.x = settings.x_log ? d3v6.scaleLog() : d3v6.scaleLinear(); | ||
| scales.x.range([0, dims.width]) | ||
| .domain([limits.min_x, limits.max_x]); | ||
| } else { | ||
| var x_domain = settings.x_levels === null ? | ||
| [...new Set(data.map(d => d.x))].sort() : | ||
| settings.x_levels; | ||
| scales.x = d3v6.scalePoint() | ||
| .range([0, dims.width]) | ||
| .padding(0.9) | ||
| .domain(x_domain); | ||
| } | ||
| if (!settings.y_categorical) { | ||
| scales.y = settings.y_log ? d3v6.scaleLog() : d3v6.scaleLinear(); | ||
| scales.y.range([dims.height, 0]) | ||
| .domain([limits.min_y, limits.max_y]); | ||
| } else { | ||
| var y_domain = settings.y_levels === null ? | ||
| [...new Set(data.map(d => d.y))].sort() : | ||
| settings.y_levels; | ||
| scales.y = d3v6.scalePoint() | ||
| .range([dims.height, 0]) | ||
| .padding(0.9) | ||
| .domain(y_domain); | ||
| } | ||
| // Keep track of original scales | ||
| scales.x_orig = scales.x; | ||
| scales.y_orig = scales.y; | ||
| // x and y axis functions | ||
| scales.xAxis = d3v6.axisBottom(scales.x) | ||
| .tickSize(-dims.height); | ||
| if (!settings.x_categorical) { | ||
| scales.xAxis.tickFormat(d3v6.format("")); | ||
| } | ||
| scales.yAxis = d3v6.axisLeft(scales.y) | ||
| .tickSize(-dims.width); | ||
| if (!settings.y_categorical) { | ||
| scales.yAxis.tickFormat(d3v6.format("")); | ||
| } | ||
|
|
||
| // Continuous color scale | ||
| if (settings.col_continuous) { | ||
| if (settings.colors === null) { | ||
| scales.color = d3v6.scaleSequential(d3v6.interpolateViridis); | ||
| } else { | ||
| scales.color = d3v6.scaleSequential(d3v6[settings.colors]); | ||
| } | ||
| scales.color = scales.color | ||
| .domain([d3v6.min(data, d => d.col_var), | ||
| d3v6.max(data, d => d.col_var)]); | ||
| } | ||
| // Ordinal color scale | ||
| else { | ||
| if (settings.colors === null) { | ||
| // Number of different levels. See https://github.com/mbostock/d3/issues/472 | ||
| var n = new Set(data.map(d => d.col_var)).size; | ||
| scales.color = n <= 10 ? d3v6.scaleOrdinal(custom_scheme10()) : d3v6.scaleOrdinal(d3v6.schemePaired); | ||
| } else if (Array.isArray(settings.colors)) { | ||
| scales.color = d3v6.scaleOrdinal().range(settings.colors); | ||
| } else if (typeof (settings.colors) === "string") { | ||
| // Single string given | ||
| scales.color = d3v6.scaleOrdinal().range(Array(settings.colors)); | ||
| } else if (typeof (settings.colors) === "object") { | ||
| scales.color = d3v6.scaleOrdinal() | ||
| .range(Object.values(settings.colors)) | ||
| .domain(Object.keys(settings.colors)); | ||
| } | ||
| } | ||
|
|
||
| // Symbol scale | ||
| var symbol_table = { | ||
| "circle": d3v6.symbolCircle, | ||
| "cross": d3v6.symbolCross, | ||
| "diamond": d3v6.symbolDiamond, | ||
| "square": d3v6.symbolSquare, | ||
| "star": d3v6.symbolStar, | ||
| "triangle": d3v6.symbolTriangle, | ||
| "wye": d3v6.symbolWye, | ||
| } | ||
| if (settings.symbols === null) { | ||
| scales.symbol = d3v6.scaleOrdinal().range(d3v6.symbols); | ||
| } else if (Array.isArray(settings.symbols)) { | ||
| scales.symbol = d3v6.scaleOrdinal().range(settings.symbols.map(d => symbol_table[d])); | ||
| } else if (typeof (settings.symbols) === "string") { | ||
| // Single string given | ||
| scales.symbol = d3v6.scaleOrdinal().range(Array(symbol_table[settings.symbols])); | ||
| } else if (typeof (settings.symbols) === "object") { | ||
| scales.symbol = d3v6.scaleOrdinal() | ||
| .range(Object.values(settings.symbols).map(d => symbol_table[d])) | ||
| .domain(Object.keys(settings.symbols)) | ||
| } | ||
|
|
||
| // Size scale | ||
| if (settings.sizes === null) { | ||
| scales.size = d3v6.scaleLinear() | ||
| .range(settings.size_range) | ||
| .domain([d3v6.min(data, d => d.size_var), | ||
| d3v6.max(data, d => d.size_var)]); | ||
| } else if (typeof(settings.sizes) === "object") { | ||
| scales.size = d3v6.scaleOrdinal() | ||
| .range(Object.values(settings.sizes)) | ||
| .domain(Object.keys(settings.sizes)); | ||
| } | ||
|
|
||
| // Opacity scale | ||
| if (settings.opacities === null) { | ||
| scales.opacity = d3v6.scaleLinear() | ||
| .range([0.1, 1]) | ||
| .domain([d3v6.min(data, d => d.opacity_var), | ||
| d3v6.max(data, d => d.opacity_var)]); | ||
| } else if (typeof(settings.opacities) === "object") { | ||
| scales.opacity = d3v6.scaleOrdinal() | ||
| .range(Object.values(settings.opacities)) | ||
| .domain(Object.keys(settings.opacities)); | ||
| } | ||
|
|
||
| return scales; | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Clean variables levels to be valid CSS classes | ||
| function css_clean(s) { | ||
| if (s === undefined) return ""; | ||
| return s.toString().replace(/[^\w-]/g, "_"); | ||
| } | ||
|
|
||
| // Default translation function for points and labels | ||
| function translation(d, scales) { | ||
| return "translate(" + scales.x(d.x) + "," + scales.y(d.y) + ")"; | ||
| } | ||
|
|
||
| // Key function to identify rows when interactively filtering | ||
| function key(d) { | ||
| return d.key_var; | ||
| } | ||
|
|
||
|
|
||
| // Create tooltip content function | ||
| function tooltip_content(d, chart) { | ||
|
|
||
| var settings = chart.settings(); | ||
|
|
||
| // no tooltips | ||
| if (!settings.has_tooltips) return null; | ||
| if (settings.has_custom_tooltips) { | ||
| // custom tooltipsl | ||
| return d.tooltip_text; | ||
| } else { | ||
| // default tooltips | ||
| var text = Array(); | ||
| if (settings.has_labels) text.push("<b>" + d.lab + "</b>"); | ||
| var x_value = settings.x_categorical ? d.x : d.x.toFixed(3); | ||
| var y_value = settings.y_categorical ? d.y : d.y.toFixed(3); | ||
| text.push("<b>" + settings.xlab + ":</b> " + x_value); | ||
| text.push("<b>" + settings.ylab + ":</b> " + y_value); | ||
| if (settings.has_color_var) text.push("<b>" + settings.col_lab + ":</b> " + d.col_var); | ||
| if (settings.has_symbol_var) text.push("<b>" + settings.symbol_lab + ":</b> " + d.symbol_var); | ||
| if (settings.has_size_var) text.push("<b>" + settings.size_lab + ":</b> " + d.size_var); | ||
| if (settings.has_opacity_var) text.push("<b>" + settings.opacity_lab + ":</b> " + d.opacity_var); | ||
| return text.join("<br />"); | ||
| } | ||
| } | ||
|
|
||
| // Custom color scheme | ||
| function custom_scheme10() { | ||
| // slice() to create a copy | ||
| var scheme = d3v6.schemeCategory10.slice(); | ||
| // Switch orange and red | ||
| var tmp = scheme[3]; | ||
| scheme[3] = scheme[1]; | ||
| scheme[1] = tmp; | ||
| return scheme; | ||
| } | ||
|
|
||
| // Gear icon path | ||
| function gear_path() { | ||
| return "m 24.28,7.2087374 -1.307796,0 c -0.17052,-0.655338 -0.433486,-1.286349 -0.772208,-1.858846 l 0.927566,-0.929797 c 0.281273,-0.281188 0.281273,-0.738139 0,-1.019312 L 21.600185,1.8728727 C 21.319088,1.591685 20.863146,1.5914219 20.582048,1.8726096 L 19.650069,2.8001358 C 19.077606,2.4614173 18.446602,2.1982296 17.791262,2.0278389 l 0,-1.30783846 c 0,-0.39762 -0.313645,-0.72 -0.711262,-0.72 l -2.16,0 c -0.397618,0 -0.711262,0.32238 -0.711262,0.72 l 0,1.30783846 c -0.65534,0.1703907 -1.286345,0.43344 -1.858849,0.7722138 L 11.420092,1.8724435 c -0.281185,-0.2812846 -0.738131,-0.2812846 -1.019315,0 L 8.8728737,3.3998124 C 8.5916888,3.6809174 8.591427,4.1368574 8.872612,4.4179484 l 0.9275234,0.931984 c -0.3388099,0.572456 -0.6019076,1.203467 -0.7722956,1.858805 l -1.3078398,0 c -0.3976159,0 -0.72,0.313643 -0.72,0.711263 L 7,10.08 c 0,0.397661 0.3223841,0.711263 0.72,0.711263 l 1.3078398,0 c 0.170388,0.655338 0.4334414,1.286349 0.7722084,1.858846 L 8.872349,13.579906 c -0.2811836,0.281105 -0.2811836,0.738139 0,1.019188 l 1.527378,1.527951 c 0.281185,0.28127 0.737041,0.281533 1.018224,3.04e-4 l 0.931981,-0.927484 c 0.572461,0.338718 1.203466,0.601823 1.858806,0.772338 l 0,1.307797 c 0,0.397662 0.313644,0.72 0.711262,0.72 l 2.16,0 c 0.39766,0 0.711262,-0.32238 0.711262,-0.72 l 0,-1.307797 c 0.65534,-0.170515 1.286344,-0.433481 1.858849,-0.772214 l 0.929797,0.927568 c 0.281098,0.281271 0.738131,0.281271 1.019184,0 l 1.527947,-1.527369 c 0.281273,-0.281105 0.281534,-0.737045 3.06e-4,-1.018136 l -0.92748,-0.931984 c 0.338723,-0.572456 0.601819,-1.203467 0.772339,-1.858805 l 1.307796,0 c 0.39766,0 0.72,-0.313643 0.72,-0.711263 l 0,-2.1599996 c 0,-0.39762 -0.322384,-0.711263 -0.72,-0.711263 z M 16,12.6 c -1.988258,0 -3.6,-1.611789 -3.6,-3.5999996 0,-1.988252 1.611742,-3.6 3.6,-3.6 1.988258,0 3.6,1.611748 3.6,3.6 C 19.6,10.988252 17.988258,12.6 16,12.6 Z"; | ||
| } | ||
|
|
||
| // Caption icon path | ||
| function caption_path() { | ||
| return "m 9.0084765,0.0032163 q 1.8249645,0 3.4939395,0.7097539 1.668974,0.7096153 2.87781,1.918523 1.208839,1.2087707 1.91855,2.8777847 0.709702,1.6690155 0.709702,3.4939387 0,1.8249234 -0.709702,3.4939384 -0.709711,1.669015 -1.91855,2.877784 -1.208836,1.208907 -2.87781,1.918523 -1.668975,0.709754 -3.4939395,0.709754 -1.8249502,0 -3.493924,-0.709754 Q 3.8455651,16.583846 2.6367267,15.374939 1.4278885,14.16617 0.71819,12.497155 0.0084778,10.82814 0.0084778,9.0032166 0.0084778,7.1782934 0.71819,5.5092779 1.4278885,3.8402625 2.6367267,2.6314932 3.8455651,1.4225855 5.5145525,0.7129702 7.1835263,0.0032163 9.0084765,0.0032163 Z m 1.1698475,2.760786 -2.339681,0 q -0.1559905,0 -0.2729632,0.1176924 -0.117,0.1163076 -0.117,0.2730462 l 0,2.3395847 q 0,0.1560462 0.117,0.2730447 0.117,0.1176924 0.2729632,0.1176924 l 2.339681,0 q 0.155977,0 0.272963,-0.1176924 0.117,-0.1163076 0.117,-0.2730447 l 0,-2.3395847 q 0,-0.1560462 -0.117,-0.2730462 -0.117,-0.1176924 -0.272963,-0.1176924 z m 0,4.6794463 -3.8994777,0 q -0.1559772,0 -0.272963,0.1176924 -0.117,0.1163076 -0.117,0.2729078 l 0,0.7799524 q 0,0.1559078 0.117,0.2729078 0.117,0.1163076 0.272963,0.1163076 l 1.1698475,0 0,3.1195394 -1.1698475,0 q -0.1559772,0 -0.272963,0.117696 -0.117,0.116304 -0.117,0.273046 l 0,0.779815 q 0,0.156046 0.117,0.273046 0.117,0.116304 0.272963,0.116304 l 5.4592747,0 q 0.155977,0 0.272964,-0.116304 0.117,-0.116304 0.117,-0.273046 l 0,-0.779815 q 0,-0.156046 -0.117,-0.273046 -0.117,-0.117696 -0.272964,-0.117696 l -1.169848,0 0,-4.2893996 q 0,-0.1559078 -0.117,-0.2729078 -0.117,-0.1176924 -0.272963,-0.1176924 z"; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
|
|
||
| // Init and returns a zoom behavior | ||
| function zoom_behavior(chart) { | ||
|
|
||
| var root = chart.svg().select(".root"); | ||
| var viewport = chart.svg().select(".viewport"); | ||
|
|
||
| var root_bb = root.node().getBoundingClientRect(); | ||
| var viewport_bb = viewport.node().getBoundingClientRect(); | ||
|
|
||
| var x0 = viewport_bb.left - root_bb.left; | ||
| var y0 = viewport_bb.top - root_bb.top; | ||
| var x1 = viewport_bb.right - root_bb.left; | ||
| var y1 = viewport_bb.bottom - root_bb.top; | ||
|
|
||
| // Zoom behavior | ||
| var zoom = d3v6.zoom() | ||
| .extent([[0, y0], [x1 - x0, y1]]) | ||
| .scaleExtent([0, 32]) | ||
| .on("zoom", (event) => zoomed(event, chart)); | ||
|
|
||
| return zoom; | ||
| } | ||
|
|
||
| // Zoom function | ||
| function zoomed(event, chart) { | ||
|
|
||
| var root = chart.svg().select(".root"); | ||
|
|
||
| if (!chart.settings().x_categorical) { | ||
| chart.scales().x = event.transform.rescaleX(chart.scales().x_orig); | ||
| chart.scales().xAxis = chart.scales().xAxis.scale(chart.scales().x); | ||
| root.select(".x.axis").call(chart.scales().xAxis); | ||
| } | ||
| if (!chart.settings().y_categorical) { | ||
| chart.scales().y = event.transform.rescaleY(chart.scales().y_orig); | ||
| chart.scales().yAxis = chart.scales().yAxis.scale(chart.scales().y); | ||
| root.select(".y.axis").call(chart.scales().yAxis); | ||
| } | ||
|
|
||
| var chart_body = chart.svg().select(".chart-body"); | ||
|
|
||
| chart_body.selectAll(".dot, .point-label, .point-label-line") | ||
| .attr("transform", d => ( translation(d, chart.scales()) )); | ||
| chart_body.selectAll(".line").call(line_formatting, chart) | ||
| chart_body.selectAll(".arrow").call(draw_arrow, chart); | ||
| chart_body.selectAll(".ellipse").call(ellipse_formatting, chart); | ||
| chart.svg().select(".unit-circle").call(unit_circle_formatting, chart); | ||
|
|
||
| if (typeof chart.settings().zoom_callback === 'function') { | ||
| chart.settings().zoom_callback(chart.scales().x.domain()[0], chart.scales().x.domain()[1], | ||
| chart.scales().y.domain()[0], chart.scales().y.domain()[1]); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| // Reset zoom function | ||
| function reset_zoom(chart) { | ||
| var root = chart.svg().select(".root"); | ||
| root.transition().duration(1000) | ||
| .call(chart.zoom().transform, d3v6.zoomIdentity); | ||
| } | ||
|
|
||
| // Update zoom function | ||
| function update_zoom(chart) { | ||
| var root = chart.svg().select(".root"); | ||
| root.select(".x.axis") | ||
| .transition().duration(1000) | ||
| .call(chart.scales().xAxis); | ||
| root.select(".y.axis") | ||
| .transition().duration(1000) | ||
| .call(chart.scales().yAxis) | ||
| .on("end", function() { | ||
| root.call(chart.zoom().transform, d3v6.zoomIdentity); | ||
| }); | ||
| } | ||
|
|
||
|
|
||
| // Zoom on | ||
| function zoom_on(chart, duration) { | ||
|
|
||
| if (chart.settings().zoom_on === null) return; | ||
|
|
||
| var root = chart.svg().select(".root"); | ||
| var curZoom = d3v6.zoomTransform(root.node()); | ||
| var zoom_x = chart.scales().x(chart.settings().zoom_on[0]); | ||
| var zoom_y = chart.scales().y(chart.settings().zoom_on[1]); | ||
| var zoom_dx = (chart.dims().width / 2 - zoom_x) / curZoom.k; | ||
| var zoom_dy = (chart.dims().height / 2 - zoom_y) / curZoom.k; | ||
| root.transition().duration(duration) | ||
| .call(chart.zoom().translateBy, zoom_dx, zoom_dy) | ||
| .on("end", function() { | ||
| if (chart.settings().zoom_on_level != curZoom.k) { | ||
| root.transition().duration(duration) | ||
| .call(chart.zoom().scaleTo, chart.settings().zoom_on_level) | ||
| } | ||
| }) | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| /* TOOLTIPS */ | ||
|
|
||
| .scatterD3-tooltip { | ||
| position: absolute; | ||
| color: #222; | ||
| background: #fff; | ||
| padding: .5em; | ||
| text-shadow: #f5f5f5 0 1px 0; | ||
| border-radius: 2px; | ||
| box-shadow: 0px 0px 7px 1px #a6a6a6; | ||
| opacity: 0.95; | ||
| font-family: Open Sans, Droid Sans, Helvetica, Verdana, sans-serif; | ||
| font-size: 10px; | ||
| z-index: 10; | ||
| } | ||
|
|
||
| /* GEAR MENU */ | ||
|
|
||
| .scatterD3-menu { | ||
| position: absolute; | ||
| opacity: 0; | ||
| width: 0; | ||
| overflow: hidden; | ||
| top: 37px; | ||
| right: 15px; | ||
| z-index: 10; | ||
| background: #F5F7F9; | ||
| box-shadow: 0px 0px 2px 1px #d6d6d6; | ||
| font-family: Open Sans, Droid Sans, Helvetica, Verdana, sans-serif; | ||
| font-size: 11px; | ||
| text-align: right; | ||
| list-style: none; | ||
| padding: 0.2em; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .scatterD3-menu li { | ||
| white-space: nowrap; | ||
| padding: 0.6em 0.8em; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .scatterD3-menu li a { | ||
| cursor: pointer; | ||
| color: #727476; | ||
| /* font-weight: 600; */ | ||
| text-decoration: none; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| .scatterD3-menu li a:hover { | ||
| color: #25282A; | ||
| } | ||
|
|
||
| /* GEAR ICON */ | ||
|
|
||
| .scatterD3 .gear-menu { | ||
| cursor: pointer; | ||
| height: 25px; | ||
| width: 25px; | ||
| } | ||
|
|
||
| .scatterD3 .gear-menu path { | ||
| opacity: 0.2; | ||
| transition: opacity .3s; | ||
| } | ||
|
|
||
| .scatterD3 .gear-menu:hover path, | ||
| .scatterD3 .gear-menu.selected path { | ||
| opacity: 0.8; | ||
| transition: opacity .3s; | ||
| } | ||
|
|
||
| /* CAPTION */ | ||
|
|
||
| .scatterD3-caption { | ||
| background: #F5F7F9; | ||
| color: #727476; | ||
| box-shadow: 0px 0px 2px 1px #d6d6d6; | ||
| font-family: Open Sans, Droid Sans, Helvetica, Verdana, sans-serif; | ||
| font-size: 1px; | ||
| text-align: left; | ||
| padding: 10px 0; | ||
| margin: 0; | ||
| position: absolute; | ||
| z-index: 10; | ||
| width: 100%; | ||
| transition: all .3s; | ||
| opacity: 0; | ||
| } | ||
|
|
||
| .scatterD3-caption.visible { | ||
| opacity: 0.97; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .scatterD3-caption h1, | ||
| .scatterD3-caption h2, | ||
| .scatterD3-caption p | ||
| { | ||
| margin: 5px 15px; | ||
| line-height: 1.35; | ||
| padding: 0; | ||
| border: none; | ||
| } | ||
|
|
||
| .scatterD3-caption h1 { | ||
| font-size: 14px; | ||
| font-weight: bold; | ||
| } | ||
|
|
||
| .scatterD3-caption h2 { | ||
| font-size: 13px; | ||
| font-weight: normal; | ||
| } | ||
|
|
||
| .scatterD3-caption .caption { | ||
| font-size: 11px; | ||
| font-weight: normal; | ||
| } | ||
|
|
||
| /* CAPTION ICON */ | ||
|
|
||
| .scatterD3 .caption-icon { | ||
| cursor: pointer; | ||
| height: 25px; | ||
| width: 25px; | ||
| } | ||
|
|
||
| .scatterD3 .caption-icon path { | ||
| opacity: 0.2; | ||
| transition: opacity .3s; | ||
| } | ||
|
|
||
| .scatterD3 .caption-icon:hover path, | ||
| .scatterD3 .caption-icon.selected path { | ||
| opacity: 0.8; | ||
| transition: opacity .3s; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| /* OTHER STYLES */ | ||
|
|
||
| .hidden { | ||
| display: none; | ||
| } | ||
|
|
||
| .scatterD3 .dot { | ||
| cursor: default; | ||
| } | ||
|
|
||
| .scatterD3 .point-label { | ||
| cursor: pointer; | ||
| paint-order: stroke; | ||
| stroke-width: 4px; | ||
| stroke: #FFFFFF; | ||
| } | ||
|
|
||
| .scatterD3, .scatterD3:focus { | ||
| outline: 0px solid transparent | ||
| } | ||
|
|
||
| .scatterD3 .root { | ||
| cursor: move; | ||
| } | ||
|
|
||
| .scatterD3 .x-axis-label, | ||
| .scatterD3 .y-axis-label { | ||
| paint-order: stroke; | ||
| stroke-width: 5px; | ||
| stroke: #FFFFFF; | ||
| } | ||
|
|
||
| /* Specific styles for shiny apps */ | ||
|
|
||
| .shiny-bound-output .label { | ||
| font-size: 100%; | ||
| font-weight: normal; | ||
| text-align: left; | ||
| } | ||
|
|
||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| dependencies: | ||
| - name: d3v6 | ||
| version: 6.2.0 | ||
| src: htmlwidgets/lib/d3 | ||
| script: | ||
| - d3.v6.min.js | ||
| - d3-legend.min.js | ||
| - d3-labeler.js | ||
| - name: d3.lasso-plugin | ||
| version: 1.0.0 | ||
| src: htmlwidgets/lib/d3-lasso-plugin | ||
| script: lasso.js | ||
| stylesheet: lasso.css | ||
| - name: scatterD3 | ||
| version: 0.9.2 | ||
| src: htmlwidgets/ | ||
| stylesheet: scatterD3.css | ||
| script: | ||
| - scatterD3-utils.js | ||
| - scatterD3-scales.js | ||
| - scatterD3-dims.js | ||
| - scatterD3-menu.js | ||
| - scatterD3-axes.js | ||
| - scatterD3-dots.js | ||
| - scatterD3-arrows.js | ||
| - scatterD3-labels.js | ||
| - scatterD3-labels-lines.js | ||
| - scatterD3-lines.js | ||
| - scatterD3-ellipses.js | ||
| - scatterD3-zoom.js | ||
| - scatterD3-legend.js | ||
| - scatterD3-lasso.js | ||
| - scatterD3-exports.js | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // Pandoc 2.9 adds attributes on both header and div. We remove the former (to | ||
| // be compatible with the behavior of Pandoc < 2.8). | ||
| document.addEventListener('DOMContentLoaded', function(e) { | ||
| var hs = document.querySelectorAll("div.section[class*='level'] > :first-child"); | ||
| var i, h, a; | ||
| for (i = 0; i < hs.length; i++) { | ||
| h = hs[i]; | ||
| if (!/^h[1-6]$/i.test(h.tagName)) continue; // it should be a header h1-h6 | ||
| a = h.attributes; | ||
| while (a.length > 0) h.removeAttribute(a[0].name); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| Copyright (c) 2010-2015, Michael Bostock | ||
| All rights reserved. | ||
|
|
||
| Redistribution and use in source and binary forms, with or without | ||
| modification, are permitted provided that the following conditions are met: | ||
|
|
||
| * Redistributions of source code must retain the above copyright notice, this | ||
| list of conditions and the following disclaimer. | ||
|
|
||
| * Redistributions in binary form must reproduce the above copyright notice, | ||
| this list of conditions and the following disclaimer in the documentation | ||
| and/or other materials provided with the distribution. | ||
|
|
||
| * The name Michael Bostock may not be used to endorse or promote products | ||
| derived from this software without specific prior written permission. | ||
|
|
||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, | ||
| INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | ||
| BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | ||
| OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | ||
| NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, | ||
| EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,304 @@ | ||
| (function() { | ||
|
|
||
| d3v6.labeler = function() { | ||
| var lab = [], | ||
| anc = [], | ||
| w = 1, // box width | ||
| h = 1, // box width | ||
| labeler = {}; | ||
|
|
||
| var max_move = 15.0, | ||
| max_angle = 2, | ||
| acc = 0, | ||
| rej = 0; | ||
|
|
||
| // weights | ||
| var w_len = 0.1, // leader line length | ||
| w_inter = 30.0, // leader line intersection | ||
| w_lab2 = 30.0, // label-label overlap | ||
| w_lab_anc = 30.0; // label-anchor overlap | ||
| w_line_lab = 30.0; // line-label overlap | ||
| w_orient = 0.2; // orientation bias | ||
|
|
||
| // booleans for user defined functions | ||
| var user_energy = false, | ||
| user_schedule = false; | ||
|
|
||
| var user_defined_energy, | ||
| user_defined_schedule; | ||
|
|
||
| energy = function(index) { | ||
| // energy function, tailored for label placement | ||
|
|
||
| var m = lab.length, | ||
| ener = 0, | ||
| dx = lab[index].x - anc[index].x, | ||
| dy = anc[index].y - lab[index].y, | ||
| dist = Math.sqrt(dx * dx + dy * dy), | ||
| overlap = true, | ||
| amount = 0, | ||
| theta = 0; | ||
|
|
||
| // penalty for length of leader line | ||
| if (dist > 0) ener += dist * w_len; | ||
|
|
||
| // label orientation bias | ||
| dx /= dist; | ||
| dy /= dist; | ||
| if (dy < 0) { ener += w_orient; } | ||
| // if (dx > 0 && dy > 0) { ener += 0 * w_orient; } | ||
| // else if (dx < 0 && dy > 0) { ener += 1 * w_orient; } | ||
| // else if (dx < 0 && dy < 0) { ener += 2 * w_orient; } | ||
| // else { ener += 3 * w_orient; } | ||
|
|
||
| var x21 = lab[index].x - lab[index].width / 2, | ||
| y21 = lab[index].y - 3 * lab[index].height / 4 - 2, | ||
| x22 = lab[index].x + lab[index].width / 2, | ||
| y22 = lab[index].y + lab[index].height / 4 - 2; | ||
| var x11, x12, y11, y12, x_overlap, y_overlap, overlap_area; | ||
|
|
||
| for (var i = 0; i < m; i++) { | ||
| if (i != index) { | ||
|
|
||
| // penalty for intersection of leader lines | ||
| overlap = intersect(anc[index].x, lab[index].x, anc[i].x, lab[i].x, | ||
| anc[index].y, lab[index].y, anc[i].y, lab[i].y); | ||
| if (overlap) ener += w_inter; | ||
|
|
||
| // penalty for label-label overlap | ||
| x11 = lab[i].x - lab[i].width / 2 - 2; | ||
| y11 = lab[i].y - 3 * lab[i].height / 4 - 2; | ||
| x12 = lab[i].x + lab[i].width / 2 + 2; | ||
| y12 = lab[i].y + lab[i].height / 4 + 2; | ||
| x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21)); | ||
| y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21)); | ||
| overlap_area = x_overlap * y_overlap; | ||
| ener += (overlap_area * w_lab2); | ||
|
|
||
| // penalty for line-label overlap | ||
| overlap = intersect(anc[index].x, lab[index].x, x11, x12, | ||
| anc[index].y, lab[index].y, y11, y11) || | ||
| intersect(anc[index].x, lab[index].x, x11, x11, | ||
| anc[index].y, lab[index].y, y11, y12) || | ||
| intersect(anc[index].x, lab[index].x, x11, x12, | ||
| anc[index].y, lab[index].y, y12, y12) || | ||
| intersect(anc[index].x, lab[index].x, x12, x12, | ||
| anc[index].y, lab[index].y, y11, y12) || | ||
| intersect(anc[i].x, lab[i].x, x21, x22, | ||
| anc[i].y, lab[i].y, y21, y21) || | ||
| intersect(anc[i].x, lab[i].x, x21, x21, | ||
| anc[i].y, lab[i].y, y21, y22) || | ||
| intersect(anc[i].x, lab[i].x, x21, x22, | ||
| anc[i].y, lab[i].y, y22, y22) || | ||
| intersect(anc[i].x, lab[i].x, x22, x22, | ||
| anc[i].y, lab[i].y, y21, y22); | ||
| if (overlap) ener += w_line_lab; | ||
| } | ||
|
|
||
| // penalty for label-anchor overlap | ||
| x11 = anc[i].x - anc[i].r; | ||
| y11 = anc[i].y - anc[i].r; | ||
| x12 = anc[i].x + anc[i].r; | ||
| y12 = anc[i].y + anc[i].r; | ||
| x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21)); | ||
| y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21)); | ||
| overlap_area = x_overlap * y_overlap; | ||
| ener += (overlap_area * w_lab_anc); | ||
|
|
||
|
|
||
|
|
||
| } | ||
| return ener; | ||
| }; | ||
|
|
||
| mcmove = function(currT) { | ||
| // Monte Carlo translation move | ||
|
|
||
| // select a random label | ||
| var i = Math.floor(Math.random() * lab.length); | ||
|
|
||
| // save old coordinates | ||
| var x_old = lab[i].x; | ||
| var y_old = lab[i].y; | ||
|
|
||
| // old energy | ||
| var old_energy; | ||
| if (user_energy) {old_energy = user_defined_energy(i, lab, anc)} | ||
| else {old_energy = energy(i)} | ||
|
|
||
| // random translation | ||
| lab[i].x += (Math.random() - 0.5) * max_move; | ||
| lab[i].y += (Math.random() - 0.5) * max_move; | ||
|
|
||
| // hard wall boundaries | ||
| if (lab[i].x > w) lab[i].x = x_old; | ||
| if (lab[i].x < 0) lab[i].x = x_old; | ||
| if (lab[i].y > h) lab[i].y = y_old; | ||
| if (lab[i].y < 0) lab[i].y = y_old; | ||
|
|
||
| // new energy | ||
| var new_energy; | ||
| if (user_energy) {new_energy = user_defined_energy(i, lab, anc)} | ||
| else {new_energy = energy(i)} | ||
|
|
||
| // delta E | ||
| var delta_energy = new_energy - old_energy; | ||
|
|
||
| if (Math.random() < Math.exp(-delta_energy / currT)) { | ||
| acc += 1; | ||
| } else { | ||
| // move back to old coordinates | ||
| lab[i].x = x_old; | ||
| lab[i].y = y_old; | ||
| rej += 1; | ||
| } | ||
|
|
||
| }; | ||
|
|
||
| mcrotate = function(currT) { | ||
| // Monte Carlo rotation move | ||
|
|
||
| // select a random label | ||
| var i = Math.floor(Math.random() * lab.length); | ||
|
|
||
| // save old coordinates | ||
| var x_old = lab[i].x; | ||
| var y_old = lab[i].y; | ||
|
|
||
| // old energy | ||
| var old_energy; | ||
| if (user_energy) {old_energy = user_defined_energy(i, lab, anc)} | ||
| else {old_energy = energy(i)} | ||
|
|
||
| // random angle | ||
| var angle = (Math.random() - 0.5) * max_angle; | ||
|
|
||
| var s = Math.sin(angle); | ||
| var c = Math.cos(angle); | ||
|
|
||
| // translate label (relative to anchor at origin): | ||
| lab[i].x -= anc[i].x | ||
| lab[i].y -= anc[i].y | ||
|
|
||
| // rotate label | ||
| var x_new = lab[i].x * c - lab[i].y * s, | ||
| y_new = lab[i].x * s + lab[i].y * c; | ||
|
|
||
| // translate label back | ||
| lab[i].x = x_new + anc[i].x | ||
| lab[i].y = y_new + anc[i].y | ||
|
|
||
| // hard wall boundaries | ||
| if (lab[i].x > w) lab[i].x = x_old; | ||
| if (lab[i].x < 0) lab[i].x = x_old; | ||
| if (lab[i].y > h) lab[i].y = y_old; | ||
| if (lab[i].y < 0) lab[i].y = y_old; | ||
|
|
||
| // new energy | ||
| var new_energy; | ||
| if (user_energy) {new_energy = user_defined_energy(i, lab, anc)} | ||
| else {new_energy = energy(i)} | ||
|
|
||
| // delta E | ||
| var delta_energy = new_energy - old_energy; | ||
|
|
||
| if (Math.random() < Math.exp(-delta_energy / currT)) { | ||
| acc += 1; | ||
| } else { | ||
| // move back to old coordinates | ||
| lab[i].x = x_old; | ||
| lab[i].y = y_old; | ||
| rej += 1; | ||
| } | ||
|
|
||
| }; | ||
|
|
||
| intersect = function(x1, x2, x3, x4, y1, y2, y3, y4) { | ||
| // returns true if two lines intersect, else false | ||
| // from http://paulbourke.net/geometry/lineline2d/ | ||
|
|
||
| var mua, mub; | ||
| var denom, numera, numerb; | ||
|
|
||
| denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); | ||
| numera = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3); | ||
| numerb = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3); | ||
|
|
||
| /* Is the intersection along the the segments */ | ||
| mua = numera / denom; | ||
| mub = numerb / denom; | ||
| if (!(mua < 0 || mua > 1 || mub < 0 || mub > 1)) { | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| cooling_schedule = function(currT, initialT, nsweeps) { | ||
| // linear cooling | ||
| return (currT - (initialT / nsweeps)); | ||
| } | ||
|
|
||
| labeler.start = function(nsweeps) { | ||
| // main simulated annealing function | ||
| var m = lab.length, | ||
| currT = 1.0, | ||
| initialT = 1.0; | ||
| for (var i = 0; i < nsweeps; i++) { | ||
| for (var j = 0; j < m; j++) { | ||
| //mcmove(currT); | ||
| if (Math.random() < 0.5) { mcmove(currT); } | ||
| else { mcrotate(currT); } | ||
| } | ||
| currT = cooling_schedule(currT, initialT, nsweeps); | ||
| } | ||
| }; | ||
|
|
||
| labeler.width = function(x) { | ||
| // users insert graph width | ||
| if (!arguments.length) return w; | ||
| w = x; | ||
| return labeler; | ||
| }; | ||
|
|
||
| labeler.height = function(x) { | ||
| // users insert graph height | ||
| if (!arguments.length) return h; | ||
| h = x; | ||
| return labeler; | ||
| }; | ||
|
|
||
| labeler.label = function(x) { | ||
| // users insert label positions | ||
| if (!arguments.length) return lab; | ||
| lab = x; | ||
| return labeler; | ||
| }; | ||
|
|
||
| labeler.anchor = function(x) { | ||
| // users insert anchor positions | ||
| if (!arguments.length) return anc; | ||
| anc = x; | ||
| return labeler; | ||
| }; | ||
|
|
||
| labeler.alt_energy = function(x) { | ||
| // user defined energy | ||
| if (!arguments.length) return energy; | ||
| user_defined_energy = x; | ||
| user_energy = true; | ||
| return labeler; | ||
| }; | ||
|
|
||
| labeler.alt_schedule = function(x) { | ||
| // user defined cooling_schedule | ||
| if (!arguments.length) return cooling_schedule; | ||
| user_defined_schedule = x; | ||
| user_schedule = true; | ||
| return labeler; | ||
| }; | ||
|
|
||
| return labeler; | ||
| }; | ||
|
|
||
| })(); | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // Pandoc 2.9 adds attributes on both header and div. We remove the former (to | ||
| // be compatible with the behavior of Pandoc < 2.8). | ||
| document.addEventListener('DOMContentLoaded', function(e) { | ||
| var hs = document.querySelectorAll("div.section[class*='level'] > :first-child"); | ||
| var i, h, a; | ||
| for (i = 0; i < hs.length; i++) { | ||
| h = hs[i]; | ||
| if (!/^h[1-6]$/i.test(h.tagName)) continue; // it should be a header h1-h6 | ||
| a = h.attributes; | ||
| while (a.length > 0) h.removeAttribute(a[0].name); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /* el is the div, holding the rgl object as el.rglinstance, | ||
| which holds x as el.rglinstance.scene | ||
| x is the JSON encoded rglwidget. | ||
| */ | ||
|
|
||
|
|
||
| HTMLWidgets.widget({ | ||
|
|
||
| name: 'rglWebGL', | ||
|
|
||
| type: 'output', | ||
|
|
||
| factory: function(el, width, height) { | ||
| el.width = width; | ||
| el.height = height; | ||
| var rgl = new rglwidgetClass(), | ||
| onchangeselection = function(e) { | ||
| for (var i = 0; i < rgl.scene.crosstalk.sel_handle.length; i++) | ||
| rgl.clearBrush(except = e.rglSubsceneId); | ||
| rgl.selection(e, false); | ||
| }, | ||
| onchangefilter = function(e) { | ||
| rgl.selection(e, true); | ||
| }; | ||
|
|
||
| return { | ||
| renderValue: function(x) { | ||
| var i, pel, player, groups, | ||
| inShiny = (typeof Shiny !== "undefined"); | ||
|
|
||
| x.crosstalk.group = groups = [].concat(x.crosstalk.group); | ||
| x.crosstalk.id = [].concat(x.crosstalk.id); | ||
| x.crosstalk.key = [].concat(x.crosstalk.key); | ||
| x.crosstalk.sel_handle = new Array(groups.length); | ||
| x.crosstalk.fil_handle = new Array(groups.length); | ||
| x.crosstalk.selection = []; | ||
| for (i = 0; i < groups.length; i++) { | ||
| x.crosstalk.sel_handle[i] = new crosstalk.SelectionHandle(groups[i], {sharedId: x.crosstalk.id[i]}); | ||
| x.crosstalk.sel_handle[i].on("change", onchangeselection); | ||
| x.crosstalk.fil_handle[i] = new crosstalk.FilterHandle(groups[i], {sharedId: x.crosstalk.id[i]}); | ||
| x.crosstalk.fil_handle[i].on("change", onchangefilter); | ||
| } | ||
| if (inShiny) { | ||
| // Shiny calls this multiple times, so we need extra cleanup | ||
| // between | ||
| rgl.sphere = undefined; | ||
| } | ||
| rgl.initialize(el, x); | ||
| rgl.initGL(); | ||
|
|
||
| /* We might have been called after (some of) the players were rendered. | ||
| We need to make sure we respond to their initial values. */ | ||
|
|
||
| if (typeof x.players !== "undefined") { | ||
| var players = [].concat(x.players); | ||
| for (i = 0; i < players.length; i++) { | ||
| pel = document.getElementById(players[i]); | ||
| if (pel) { | ||
| player = pel.rglPlayer; | ||
| if (player && (!player.initialized || inShiny)) { | ||
| rgl.Player(pel, player); | ||
| player.initialized = true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| rgl.drag = 0; | ||
| rgl.drawScene(); | ||
| }, | ||
|
|
||
| resize: function(width, height) { | ||
| el.width = width; | ||
| el.height = height; | ||
| el.rglinstance.resize(el); | ||
| el.rglinstance.drawScene(); | ||
| } | ||
| }; | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| // These functions order the centers of displayed objects so they | ||
| // can be drawn using the painters algorithm, necessary to support | ||
| // transparency. | ||
|
|
||
| // Note that objid is not obj.id when drawing spheres. | ||
|
|
||
| rglwidgetClass.prototype.getPieces = function(context, objid, subid, obj) { | ||
| var n = obj.centers.length, | ||
| depth, | ||
| result = new Array(n), | ||
| z, w, i; | ||
| context = context.slice(); | ||
|
|
||
| for(i=0; i<n; i++) { | ||
| z = this.prmvMatrix.m13*obj.centers[i][0] + | ||
| this.prmvMatrix.m23*obj.centers[i][1] + | ||
| this.prmvMatrix.m33*obj.centers[i][2] + | ||
| this.prmvMatrix.m43; | ||
| w = this.prmvMatrix.m14*obj.centers[i][0] + | ||
| this.prmvMatrix.m24*obj.centers[i][1] + | ||
| this.prmvMatrix.m34*obj.centers[i][2] + | ||
| this.prmvMatrix.m44; | ||
| depth = z/w; | ||
| result[i] = {context: context, | ||
| objid: objid, | ||
| subid: subid, | ||
| index: i, | ||
| depth: depth}; | ||
| } | ||
| return result; | ||
| }; | ||
|
|
||
| rglwidgetClass.prototype.getSpherePieces = function(context, subid, obj) | ||
| { | ||
| if (obj.fastTransparency) | ||
| if (subid === 0) // Only compute pieces once | ||
| return this.getPieces(context, obj.id, -1, obj); | ||
| else | ||
| return []; | ||
| else | ||
| return this.getPieces(context, obj.id, subid, this.sphere); | ||
| }; | ||
|
|
||
| rglwidgetClass.prototype.mergePieces = function(pieces) { | ||
| var result = []; | ||
| if (pieces.length > 0) { | ||
| var i, | ||
| thiscontext = pieces[0].context, | ||
| thisobjid = pieces[0].objid, | ||
| thissubid = pieces[0].subid, | ||
| indices = []; | ||
| for (i= 0; i < pieces.length; i++) { | ||
| if (pieces[i].context !== thiscontext || | ||
| pieces[i].objid !== thisobjid || | ||
| pieces[i].subid !== thissubid) { | ||
| result.push({context: thiscontext, objid: thisobjid, | ||
| subid: thissubid, indices: indices}); | ||
| thiscontext = pieces[i].context; | ||
| thisobjid = pieces[i].objid; | ||
| thissubid = pieces[i].subid; | ||
| indices = []; | ||
| } | ||
| indices.push(pieces[i].index); | ||
| } | ||
| result.push({context: thiscontext, objid: thisobjid, | ||
| subid: thissubid, | ||
| indices: indices}); | ||
| } | ||
| return result; | ||
| }; | ||
|
|
||
| rglwidgetClass.prototype.sortPieces = function(pieces) { | ||
| var compare = function(i,j) { | ||
| var diff = j.depth - i.depth; | ||
| // We want to avoid context or obj changes, | ||
| // so sort on those next. | ||
| if (diff === 0) { | ||
| var c1 = j.context.slice(), | ||
| c2 = i.context.slice(); | ||
| diff = c1.length - c2.length; | ||
| while (diff === 0 && c1.length > 0) { | ||
| diff = c1.pop() - c2.pop(); | ||
| } | ||
| if (diff === 0) | ||
| diff = j.objid - i.objid; | ||
| if (diff === 0) | ||
| diff = j.subid - i.subid; | ||
| } | ||
| return diff; | ||
| }, result = []; | ||
| if (pieces.length) | ||
| result = pieces.sort(compare); | ||
| return result; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
|
|
||
| /** | ||
| * Get the viewport | ||
| */ | ||
|
|
||
| rglwidgetClass.prototype.getViewport = function(id) { | ||
| var vp = this.getObj(id).par3d.viewport, | ||
| x = vp.x*this.canvas.width, | ||
| y = vp.y*this.canvas.height, | ||
| width = vp.width*this.canvas.width, | ||
| height = vp.height*this.canvas.height; | ||
| this.vp = {x:x, y:y, width:width, height:height}; | ||
| }; | ||
|
|
||
| /** | ||
| * Set the gl viewport and scissor test | ||
| * @param { number } id - id of subscene | ||
| */ | ||
| rglwidgetClass.prototype.setViewport = function(id) { | ||
| var gl = this.gl || this.initGL(); | ||
| this.getViewport(id); | ||
| gl.viewport(this.vp.x, this.vp.y, this.vp.width, this.vp.height); | ||
| gl.scissor(this.vp.x, this.vp.y, this.vp.width, this.vp.height); | ||
| gl.enable(gl.SCISSOR_TEST); | ||
| }; | ||
|
|
||
| /** | ||
| * Set the projection matrix for a subscene | ||
| * @param { number } id - id of subscene | ||
| */ | ||
| rglwidgetClass.prototype.setprMatrix = function(id) { | ||
| var subscene = this.getObj(id), | ||
| embedding = subscene.embeddings.projection; | ||
| if (embedding === "replace") | ||
| this.prMatrix.makeIdentity(); | ||
| else | ||
| this.setprMatrix(subscene.parent); | ||
| if (embedding === "inherit") | ||
| return; | ||
| // This is based on the Frustum::enclose code from geom.cpp | ||
| var bbox = subscene.par3d.bbox, | ||
| scale = subscene.par3d.scale, | ||
| ranges = [(bbox[1]-bbox[0])*scale[0]/2, | ||
| (bbox[3]-bbox[2])*scale[1]/2, | ||
| (bbox[5]-bbox[4])*scale[2]/2], | ||
| radius = Math.sqrt(this.sumsq(ranges))*1.1; // A bit bigger to handle labels | ||
| if (radius <= 0) radius = 1; | ||
| var observer = subscene.par3d.observer, | ||
| distance = observer[2], | ||
| FOV = subscene.par3d.FOV, ortho = FOV === 0, | ||
| t = ortho ? 1 : Math.tan(FOV*Math.PI/360), | ||
| near = distance - radius, | ||
| far = distance + radius, | ||
| hlen, | ||
| aspect = this.vp.width/this.vp.height, | ||
| z = subscene.par3d.zoom, | ||
| userProjection = subscene.par3d.userProjection; | ||
| if (far < 0.0) | ||
| far = 1.0; | ||
| if (near < far/100.0) | ||
| near = far/100.0; | ||
| this.frustum = {near:near, far:far}; | ||
| hlen = t*near; | ||
| if (ortho) { | ||
| if (aspect > 1) | ||
| this.prMatrix.ortho(-hlen*aspect*z, hlen*aspect*z, | ||
| -hlen*z, hlen*z, near, far); | ||
| else | ||
| this.prMatrix.ortho(-hlen*z, hlen*z, | ||
| -hlen*z/aspect, hlen*z/aspect, | ||
| near, far); | ||
| } else { | ||
| if (aspect > 1) | ||
| this.prMatrix.frustum(-hlen*aspect*z, hlen*aspect*z, | ||
| -hlen*z, hlen*z, near, far); | ||
| else | ||
| this.prMatrix.frustum(-hlen*z, hlen*z, | ||
| -hlen*z/aspect, hlen*z/aspect, | ||
| near, far); | ||
| } | ||
| this.prMatrix.multRight(userProjection); | ||
| }; | ||
|
|
||
| /** | ||
| * Set the model-view matrix for a subscene | ||
| * @param { number } id - id of the subscene | ||
| */ | ||
| rglwidgetClass.prototype.setmvMatrix = function(id) { | ||
| var observer = this.getObj(id).par3d.observer; | ||
| this.mvMatrix.makeIdentity(); | ||
| this.setmodelMatrix(id); | ||
| this.mvMatrix.translate(-observer[0], -observer[1], -observer[2]); | ||
|
|
||
| }; | ||
|
|
||
| /** | ||
| * Set the model matrix for a subscene | ||
| * @param { number } id - id of the subscene | ||
| */ | ||
| rglwidgetClass.prototype.setmodelMatrix = function(id) { | ||
| var subscene = this.getObj(id), | ||
| embedding = subscene.embeddings.model; | ||
| if (embedding !== "inherit") { | ||
| var scale = subscene.par3d.scale, | ||
| bbox = subscene.par3d.bbox, | ||
| center = [(bbox[0]+bbox[1])/2, | ||
| (bbox[2]+bbox[3])/2, | ||
| (bbox[4]+bbox[5])/2]; | ||
| this.mvMatrix.translate(-center[0], -center[1], -center[2]); | ||
| this.mvMatrix.scale(scale[0], scale[1], scale[2]); | ||
| this.mvMatrix.multRight( subscene.par3d.userMatrix ); | ||
| } | ||
| if (embedding !== "replace") | ||
| this.setmodelMatrix(subscene.parent); | ||
| }; | ||
|
|
||
| /** | ||
| * Set the normals matrix for a subscene | ||
| * @param { number } subsceneid - id of the subscene | ||
| */ | ||
| rglwidgetClass.prototype.setnormMatrix = function(subsceneid) { | ||
| var self = this, | ||
| recurse = function(id) { | ||
| var sub = self.getObj(id), | ||
| embedding = sub.embeddings.model; | ||
| if (embedding !== "inherit") { | ||
| var scale = sub.par3d.scale; | ||
| self.normMatrix.scale(1/scale[0], 1/scale[1], 1/scale[2]); | ||
| self.normMatrix.multRight(sub.par3d.userMatrix); | ||
| } | ||
| if (embedding !== "replace") | ||
| recurse(sub.parent); | ||
| }; | ||
| self.normMatrix.makeIdentity(); | ||
| recurse(subsceneid); | ||
| }; | ||
|
|
||
| /** | ||
| * Set the combined projection-model-view matrix | ||
| */ | ||
| rglwidgetClass.prototype.setprmvMatrix = function() { | ||
| this.prmvMatrix = new CanvasMatrix4( this.mvMatrix ); | ||
| this.prmvMatrix.multRight( this.prMatrix ); | ||
| }; | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| .rglPlayer { | ||
| width: auto; | ||
| height: auto; | ||
| } | ||
|
|
||
| .rglPlayer .rgl-button { | ||
| width: auto; | ||
| display: inline-block; | ||
| font-size: 75%; | ||
| } | ||
|
|
||
| .rglPlayer .rgl-slider { | ||
| display: inline-block; | ||
| width: 30%; | ||
| } | ||
|
|
||
| .rglPlayer .rgl-label { | ||
| display: inline; | ||
| padding-left: 6px; | ||
| padding-right: 6px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| //// To generate the help pages for this library, use | ||
|
|
||
| // jsdoc --destination ../../../doc/rglwidgetClass --template ~/node_modules/jsdoc-baseline rglClass.src.js | ||
|
|
||
| // To validate, set environment variable RGL_DEBUGGING=true | ||
| // before building. | ||
|
|
||
| /* globals rglwidgetClass: true */ | ||
|
|
||
| /** | ||
| * The class of an rgl widget | ||
| * @class | ||
| */ | ||
| rglwidgetClass = function() { | ||
| this.canvas = null; | ||
| this.userMatrix = new CanvasMatrix4(); | ||
| this.types = []; | ||
| this.prMatrix = new CanvasMatrix4(); | ||
| this.mvMatrix = new CanvasMatrix4(); | ||
| this.vp = null; | ||
| this.prmvMatrix = null; | ||
| this.origs = null; | ||
| this.gl = null; | ||
| this.scene = null; | ||
| this.select = {state: "inactive", subscene: null, region: {p1: {x:0, y:0}, p2: {x:0, y:0}}}; | ||
| this.drawing = false; | ||
| }; | ||
|
|
||
|
|
||
| rglwidgetClass.prototype.f_is_lit = 1; | ||
| rglwidgetClass.prototype.f_is_smooth = 2; | ||
| rglwidgetClass.prototype.f_has_texture = 4; | ||
| rglwidgetClass.prototype.f_depth_sort = 8; | ||
| rglwidgetClass.prototype.f_fixed_quads = 16; | ||
| rglwidgetClass.prototype.f_is_transparent = 32; | ||
| rglwidgetClass.prototype.f_is_lines = 64; | ||
| rglwidgetClass.prototype.f_sprites_3d = 128; | ||
| rglwidgetClass.prototype.f_is_subscene = 256; | ||
| rglwidgetClass.prototype.f_is_clipplanes = 512; | ||
| rglwidgetClass.prototype.f_fixed_size = 1024; | ||
| rglwidgetClass.prototype.f_is_points = 2048; | ||
| rglwidgetClass.prototype.f_is_twosided = 4096; | ||
| rglwidgetClass.prototype.f_fat_lines = 8192; | ||
| rglwidgetClass.prototype.f_is_brush = 16384; | ||
| rglwidgetClass.prototype.f_has_fog = 32768; | ||
|
|
||
| rglwidgetClass.prototype.fogNone = 0; | ||
| rglwidgetClass.prototype.fogLinear = 1; | ||
| rglwidgetClass.prototype.fogExp = 2; | ||
| rglwidgetClass.prototype.fogExp2 = 3; | ||
|
|
||
| /** | ||
| * Start the writeWebGL scene. This is only used by writeWebGL; rglwidget has | ||
| no debug element and does the drawing in rglwidget.js. | ||
| */ | ||
| rglwidgetClass.prototype.start = function() { | ||
| if (typeof this.prefix !== "undefined") { | ||
| this.debugelement = document.getElementById(this.prefix + "debug"); | ||
| this.debug(""); | ||
| } | ||
| this.drag = 0; | ||
| this.drawScene(); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| /* globals rgltimerClass: true */ | ||
|
|
||
| /** | ||
| * The class of an rgl timer object | ||
| * @class | ||
| */ | ||
|
|
||
| /** | ||
| * Construct an rgltimerClass object | ||
| * @constructor | ||
| * @param { function } Tick - action when timer fires | ||
| * @param { number } startTime - nominal start time in seconds | ||
| * @param { number } interval - seconds between updates | ||
| * @param { number } stopTime - nominal stop time in seconds | ||
| * @param { number } stepSize - nominal step size | ||
| * @param { number } value - current nominal time | ||
| * @param { number } rate - nominal units per second | ||
| * @param { string } loop - "none", "cycle" or "oscillate" | ||
| * @param { Object } actions - list of actions | ||
| */ | ||
| rgltimerClass = function(Tick, startTime, interval, stopTime, stepSize, value, rate, loop, actions) { | ||
| this.enabled = false; | ||
| this.timerId = 0; | ||
| /** nominal start time in seconds */ | ||
| this.startTime = startTime; | ||
| /** current nominal time */ | ||
| this.value = value; | ||
| /** seconds between updates */ | ||
| this.interval = interval; | ||
| /** nominal stop time */ | ||
| this.stopTime = stopTime; | ||
| /** nominal step size */ | ||
| this.stepSize = stepSize; | ||
| /** nominal units per second */ | ||
| this.rate = rate; | ||
| /** "none", "cycle", or "oscillate" */ | ||
| this.loop = loop; | ||
| /** real world start time */ | ||
| this.realStart = undefined; | ||
| /** multiplier for fast-forward or reverse */ | ||
| this.multiplier = 1; | ||
| this.actions = actions; | ||
| this.Tick = Tick; | ||
| }; | ||
|
|
||
| /** | ||
| * Start playing timer object | ||
| */ | ||
| rgltimerClass.prototype.play = function() { | ||
| if (this.enabled) { | ||
| this.enabled = false; | ||
| window.clearInterval(this.timerId); | ||
| this.timerId = 0; | ||
| return; | ||
| } | ||
| var tick = function(self) { | ||
| var now = new Date(); | ||
| self.value = self.multiplier*self.rate*(now - self.realStart)/1000 + self.startTime; | ||
| self.forceToRange(); | ||
| if (typeof self.Tick !== "undefined") { | ||
| self.Tick(self.value); | ||
| } | ||
|
|
||
| }; | ||
| this.realStart = new Date() - 1000*(this.value - this.startTime)/this.rate/this.multiplier; | ||
| this.timerId = window.setInterval(tick, 1000*this.interval, this); | ||
| this.enabled = true; | ||
| }; | ||
|
|
||
| /** | ||
| * Force value into legal range | ||
| */ | ||
| rgltimerClass.prototype.forceToRange = function() { | ||
| if (this.value > this.stopTime + this.stepSize/2 || this.value < this.startTime - this.stepSize/2) { | ||
| if (!this.loop) { | ||
| this.reset(); | ||
| } else { | ||
| var cycle = this.stopTime - this.startTime + this.stepSize, | ||
| newval = (this.value - this.startTime) % cycle + this.startTime; | ||
| if (newval < this.startTime) { | ||
| newval += cycle; | ||
| } | ||
| this.realStart += (this.value - newval)*1000/this.multiplier/this.rate; | ||
| this.value = newval; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Reset to start values | ||
| */ | ||
| rgltimerClass.prototype.reset = function() { | ||
| this.value = this.startTime; | ||
| this.newmultiplier(1); | ||
| if (typeof this.Tick !== "undefined") { | ||
| this.Tick(this.value); | ||
| } | ||
| if (this.enabled) | ||
| this.play(); /* really pause... */ | ||
| if (typeof this.PlayButton !== "undefined") | ||
| this.PlayButton.value = "Play"; | ||
| }; | ||
|
|
||
| /** | ||
| * Increase the multiplier to play faster | ||
| */ | ||
| rgltimerClass.prototype.faster = function() { | ||
| this.newmultiplier(Math.SQRT2*this.multiplier); | ||
| }; | ||
|
|
||
| /** | ||
| * Decrease the multiplier to play slower | ||
| */ | ||
| rgltimerClass.prototype.slower = function() { | ||
| this.newmultiplier(this.multiplier/Math.SQRT2); | ||
| }; | ||
|
|
||
| /** | ||
| * Change sign of multiplier to reverse direction | ||
| */ | ||
| rgltimerClass.prototype.reverse = function() { | ||
| this.newmultiplier(-this.multiplier); | ||
| }; | ||
|
|
||
| /** | ||
| * Set multiplier for play speed | ||
| * @param { number } newmult - new value | ||
| */ | ||
| rgltimerClass.prototype.newmultiplier = function(newmult) { | ||
| if (newmult !== this.multiplier) { | ||
| this.realStart += 1000*(this.value - this.startTime)/this.rate*(1/this.multiplier - 1/newmult); | ||
| this.multiplier = newmult; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Take one step | ||
| */ | ||
| rgltimerClass.prototype.step = function() { | ||
| this.value += this.rate*this.multiplier; | ||
| this.forceToRange(); | ||
| if (typeof this.Tick !== "undefined") | ||
| this.Tick(this.value); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
|
|
||
| /** | ||
| * Respond to brush change | ||
| */ | ||
| rglwidgetClass.prototype.selectionChanged = function() { | ||
| var i, j, k, id, subid = this.select.subscene, subscene, | ||
| objids, obj, | ||
| p1 = this.select.region.p1, p2 = this.select.region.p2, | ||
| filter, selection = [], handle, keys, xmin, x, xmax, ymin, y, ymax, z, v, | ||
| someHidden; | ||
| if (!subid) | ||
| return; | ||
| subscene = this.getObj(subid); | ||
| objids = subscene.objects; | ||
| filter = this.scene.crosstalk.filter; | ||
| this.setmvMatrix(subid); | ||
| this.setprMatrix(subid); | ||
| this.setprmvMatrix(); | ||
| xmin = Math.min(p1.x, p2.x); | ||
| xmax = Math.max(p1.x, p2.x); | ||
| ymin = Math.min(p1.y, p2.y); | ||
| ymax = Math.max(p1.y, p2.y); | ||
| for (i = 0; i < objids.length; i++) { | ||
| id = objids[i]; | ||
| j = this.scene.crosstalk.id.indexOf(id); | ||
| if (j >= 0) { | ||
| keys = this.scene.crosstalk.key[j]; | ||
| obj = this.getObj(id); | ||
| someHidden = false; | ||
| for (k = 0; k < keys.length; k++) { | ||
| if (filter && filter.indexOf(keys[k]) < 0) { | ||
| someHidden = true; | ||
| continue; | ||
| } | ||
| v = [].concat(obj.vertices[k]).concat(1.0); | ||
| v = this.multVM(v, this.prmvMatrix); | ||
| x = v[0]/v[3]; | ||
| y = v[1]/v[3]; | ||
| z = v[2]/v[3]; | ||
| if (xmin <= x && x <= xmax && ymin <= y && y <= ymax && -1.0 <= z && z <= 1.0) { | ||
| selection.push(keys[k]); | ||
| } else | ||
| someHidden = true; | ||
| } | ||
| obj.someHidden = someHidden && (filter || selection.length); | ||
| obj.initialized = false; | ||
| /* Who should we notify? Only shared data in the current subscene, or everyone? */ | ||
| if (!this.equalArrays(selection, this.scene.crosstalk.selection)) { | ||
| handle = this.scene.crosstalk.sel_handle[j]; | ||
| handle.set(selection, {rglSubsceneId: this.select.subscene}); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Respond to selection or filter change from crosstalk | ||
| * @param { Object } event - crosstalk event | ||
| * @param { boolean } filter - filter or selection? | ||
| */ | ||
| rglwidgetClass.prototype.selection = function(event, filter) { | ||
| var i, j, ids, obj, keys, crosstalk = this.scene.crosstalk, | ||
| selection, someHidden; | ||
|
|
||
| // Record the message and find out if this event makes some objects have mixed values: | ||
|
|
||
| crosstalk = this.scene.crosstalk; | ||
|
|
||
| if (filter) { | ||
| filter = crosstalk.filter = event.value; | ||
| selection = crosstalk.selection; | ||
| } else { | ||
| selection = crosstalk.selection = event.value; | ||
| filter = crosstalk.filter; | ||
| } | ||
| ids = crosstalk.id; | ||
| for (i = 0; i < ids.length ; i++) { | ||
| obj = this.getObj(ids[i]); | ||
| obj.initialized = false; | ||
| keys = crosstalk.key[i]; | ||
| someHidden = false; | ||
| for (j = 0; j < keys.length && !someHidden; j++) { | ||
| if ((filter && filter.indexOf(keys[j]) < 0) || | ||
| (selection.length && selection.indexOf(keys[j]) < 0)) | ||
| someHidden = true; | ||
| } | ||
| obj.someHidden = someHidden; | ||
| } | ||
| this.drawScene(); | ||
| }; | ||
|
|
||
| /** | ||
| * Clear the selection brush | ||
| * @param { number } except - Subscene that should ignore this request | ||
| */ | ||
| rglwidgetClass.prototype.clearBrush = function(except) { | ||
| if (this.select.subscene !== except) { | ||
| this.select.region = {p1: {x:Infinity, y:Infinity}, | ||
| p2: {x:Infinity, y:Infinity}}; | ||
| this.selectionChanged(); | ||
| this.select.state = "inactive"; | ||
| this.delFromSubscene(this.scene.brushId, this.select.subscene); | ||
| } | ||
| this.drawScene(); | ||
| }; | ||
|
|
||
| /** | ||
| * Set the vertices in the selection box object | ||
| */ | ||
| rglwidgetClass.prototype.initSelection = function(id) { | ||
| if (typeof this.select.region === "undefined") | ||
| return; | ||
| var obj = this.getObj(id), | ||
| p1 = this.select.region.p1, | ||
| p2 = this.select.region.p2; | ||
|
|
||
| obj.vertices = [[p1.x, p1.y, 0.0], | ||
| [p2.x, p1.y, 0.0], | ||
| [p2.x, p2.y, 0.0], | ||
| [p1.x, p2.y, 0.0], | ||
| [p1.x, p1.y, 0.0]]; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| /** | ||
| * Is a particular id in a subscene? | ||
| * @returns { boolean } | ||
| * @param {number} id Which id? | ||
| * @param {number} subscene Which subscene id? | ||
| */ | ||
| rglwidgetClass.prototype.inSubscene = function(id, subscene) { | ||
| return this.getObj(subscene).objects.indexOf(id) > -1; | ||
| }; | ||
|
|
||
| /** | ||
| * Translate from window coordinates to viewport coordinates | ||
| * @returns { Object } translated coordinates | ||
| * @param { number } subsceneid - which subscene to use? | ||
| * @param { Object } coords - point to translate | ||
| */ | ||
| rglwidgetClass.prototype.translateCoords = function(subsceneid, coords) { | ||
| var viewport = this.getObj(subsceneid).par3d.viewport; | ||
| return {x: coords.x - viewport.x*this.canvas.width, | ||
| y: coords.y - viewport.y*this.canvas.height}; | ||
| }; | ||
|
|
||
| /** | ||
| * Check whether point is in viewport of subscene | ||
| * @returns {boolean} | ||
| * @param { Object } coords - screen coordinates of point | ||
| * @param { number } subsceneid - subscene to check | ||
| */ | ||
| rglwidgetClass.prototype.inViewport = function(coords, subsceneid) { | ||
| var viewport = this.getObj(subsceneid).par3d.viewport, | ||
| x0 = coords.x - viewport.x*this.canvas.width, | ||
| y0 = coords.y - viewport.y*this.canvas.height; | ||
| return 0 <= x0 && x0 <= viewport.width*this.canvas.width && | ||
| 0 <= y0 && y0 <= viewport.height*this.canvas.height; | ||
| }; | ||
|
|
||
| /** | ||
| * Find which subscene contains a point | ||
| * @returns { number } subscene id | ||
| * @param { Object } coords - coordinates of point | ||
| */ | ||
| rglwidgetClass.prototype.whichSubscene = function(coords) { | ||
| var self = this, | ||
| recurse = function(subsceneid) { | ||
| var subscenes = self.getChildSubscenes(subsceneid), i, id; | ||
| for (i=0; i < subscenes.length; i++) { | ||
| id = recurse(subscenes[i]); | ||
| if (typeof(id) !== "undefined") | ||
| return(id); | ||
| } | ||
| if (self.inViewport(coords, subsceneid)) | ||
| return(subsceneid); | ||
| else | ||
| return undefined; | ||
| }, | ||
| rootid = this.scene.rootSubscene, | ||
| result = recurse(rootid); | ||
| if (typeof(result) === "undefined") | ||
| result = rootid; | ||
| return result; | ||
| }; | ||
|
|
||
| /** | ||
| * Add an id to a subscene. | ||
| * @param {number} id Which id? | ||
| * @param {number} subscene Which subscene id? | ||
| */ | ||
| rglwidgetClass.prototype.addToSubscene = function(id, subscene) { | ||
| var thelist, | ||
| thesub = this.getObj(subscene), | ||
| ids = [id], | ||
| obj = this.getObj(id), i; | ||
| if (typeof obj !== "undefined" && typeof (obj.newIds) !== "undefined") { | ||
| ids = ids.concat(obj.newIds); | ||
| } | ||
| thesub.objects = [].concat(thesub.objects); | ||
| for (i = 0; i < ids.length; i++) { | ||
| id = ids[i]; | ||
| if (thesub.objects.indexOf(id) === -1) { | ||
| thelist = this.whichList(id); | ||
| thesub.objects.push(id); | ||
| thesub[thelist].push(id); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Delete an id from a subscene | ||
| * @param { number } id - the id to add | ||
| * @param { number } subscene - the id of the subscene | ||
| */ | ||
| rglwidgetClass.prototype.delFromSubscene = function(id, subscene) { | ||
| var thelist, | ||
| thesub = this.getObj(subscene), | ||
| obj = this.getObj(id), | ||
| ids = [id], i, j; | ||
| if (typeof obj !== "undefined" && typeof (obj.newIds) !== "undefined") | ||
| ids = ids.concat(obj.newIds); | ||
| thesub.objects = [].concat(thesub.objects); // It might be a scalar | ||
| for (j=0; j<ids.length;j++) { | ||
| id = ids[j]; | ||
| i = thesub.objects.indexOf(id); | ||
| if (i > -1) { | ||
| thesub.objects.splice(i, 1); | ||
| thelist = this.whichList(id); | ||
| i = thesub[thelist].indexOf(id); | ||
| thesub[thelist].splice(i, 1); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Set the ids in a subscene | ||
| * @param { number[] } ids - the ids to set | ||
| * @param { number } subsceneid - the id of the subscene | ||
| */ | ||
| rglwidgetClass.prototype.setSubsceneEntries = function(ids, subsceneid) { | ||
| var sub = this.getObj(subsceneid); | ||
| sub.objects = ids; | ||
| this.initSubscene(subsceneid); | ||
| }; | ||
|
|
||
| /** | ||
| * Get the ids in a subscene | ||
| * @returns {number[]} | ||
| * @param { number } subscene - the id of the subscene | ||
| */ | ||
| rglwidgetClass.prototype.getSubsceneEntries = function(subscene) { | ||
| return this.getObj(subscene).objects; | ||
| }; | ||
|
|
||
| /** | ||
| * Get the ids of the subscenes within a subscene | ||
| * @returns { number[] } | ||
| * @param { number } subscene - the id of the subscene | ||
| */ | ||
| rglwidgetClass.prototype.getChildSubscenes = function(subscene) { | ||
| return this.getObj(subscene).subscenes; | ||
| }; | ||
|
|
||
| /** | ||
| * Find a particular subscene by inheritance | ||
| * @returns { number } id of subscene to use | ||
| * @param { number } subsceneid - child subscene | ||
| * @param { string } type - type of inheritance: "projection" or "model" | ||
| */ | ||
| rglwidgetClass.prototype.useid = function(subsceneid, type) { | ||
| var sub = this.getObj(subsceneid); | ||
| if (sub.embeddings[type] === "inherit") | ||
| return(this.useid(sub.parent, type)); | ||
| else | ||
| return subsceneid; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
|
|
||
| /** | ||
| * Handle a texture after its image has been loaded | ||
| * @param { Object } texture - the gl texture object | ||
| * @param { Object } textureCanvas - the canvas holding the image | ||
| */ | ||
| rglwidgetClass.prototype.handleLoadedTexture = function(texture, textureCanvas) { | ||
| var gl = this.gl || this.initGL(); | ||
| gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); | ||
|
|
||
| gl.bindTexture(gl.TEXTURE_2D, texture); | ||
| gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureCanvas); | ||
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | ||
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST); | ||
| gl.generateMipmap(gl.TEXTURE_2D); | ||
|
|
||
| gl.bindTexture(gl.TEXTURE_2D, null); | ||
| }; | ||
|
|
||
| /** | ||
| * Get maximum dimension of texture in current browser. | ||
| * @returns {number} | ||
| */ | ||
| rglwidgetClass.prototype.getMaxTexSize = function() { | ||
| var gl = this.gl || this.initGL(); | ||
| return Math.min(4096, gl.getParameter(gl.MAX_TEXTURE_SIZE)); | ||
| }; | ||
|
|
||
| /** | ||
| * Load an image to a texture | ||
| * @param { string } uri - The image location | ||
| * @param { Object } texture - the gl texture object | ||
| */ | ||
| rglwidgetClass.prototype.loadImageToTexture = function(uri, texture) { | ||
| var canvas = this.textureCanvas, | ||
| ctx = canvas.getContext("2d"), | ||
| image = new Image(), | ||
| self = this; | ||
|
|
||
| image.onload = function() { | ||
| var w = image.width, | ||
| h = image.height, | ||
| canvasX = self.getPowerOfTwo(w), | ||
| canvasY = self.getPowerOfTwo(h), | ||
| maxTexSize = self.getMaxTexSize(); | ||
| while (canvasX > 1 && canvasY > 1 && (canvasX > maxTexSize || canvasY > maxTexSize)) { | ||
| canvasX /= 2; | ||
| canvasY /= 2; | ||
| } | ||
| canvas.width = canvasX; | ||
| canvas.height = canvasY; | ||
| ctx.imageSmoothingEnabled = true; | ||
| ctx.drawImage(image, 0, 0, canvasX, canvasY); | ||
| self.handleLoadedTexture(texture, canvas); | ||
| self.drawScene(); | ||
| }; | ||
| image.src = uri; | ||
| }; | ||
|
|
||
| /** | ||
| * Draw text to the texture canvas | ||
| * @returns { Object } object with text measurements | ||
| * @param { string } text - the text | ||
| * @param { number } cex - expansion | ||
| * @param { string } family - font family | ||
| * @param { number } font - font number | ||
| */ | ||
| rglwidgetClass.prototype.drawTextToCanvas = function(text, cex, family, font) { | ||
| var canvasX, canvasY, | ||
| scaling = 20, | ||
| textColour = "white", | ||
|
|
||
| backgroundColour = "rgba(0,0,0,0)", | ||
| canvas = this.textureCanvas, | ||
| ctx = canvas.getContext("2d"), | ||
| i, textHeight = 0, textHeights = [], width, widths = [], | ||
| offsetx, offsety = 0, line, lines = [], offsetsx = [], | ||
| offsetsy = [], lineoffsetsy = [], fontStrings = [], | ||
| maxTexSize = this.getMaxTexSize(), | ||
| getFontString = function(i) { | ||
| textHeights[i] = scaling*cex[i]; | ||
| var fontString = textHeights[i] + "px", | ||
| family0 = family[i], | ||
| font0 = font[i]; | ||
| if (family0 === "sans") | ||
| family0 = "sans-serif"; | ||
| else if (family0 === "mono") | ||
| family0 = "monospace"; | ||
| fontString = fontString + " " + family0; | ||
| if (font0 === 2 || font0 === 4) | ||
| fontString = "bold " + fontString; | ||
| if (font0 === 3 || font0 === 4) | ||
| fontString = "italic " + fontString; | ||
| return fontString; | ||
| }; | ||
| cex = this.repeatToLen(cex, text.length); | ||
| family = this.repeatToLen(family, text.length); | ||
| font = this.repeatToLen(font, text.length); | ||
|
|
||
| canvasX = 1; | ||
| line = -1; | ||
| offsetx = maxTexSize; | ||
| for (i = 0; i < text.length; i++) { | ||
| ctx.font = fontStrings[i] = getFontString(i); | ||
| width = widths[i] = ctx.measureText(text[i]).width; | ||
| if (offsetx + width > maxTexSize) { | ||
| offsety = offsety + 2*textHeight; | ||
| if (line >= 0) | ||
| lineoffsetsy[line] = offsety; | ||
| line += 1; | ||
| if (offsety > maxTexSize) | ||
| console.error("Too many strings for texture."); | ||
| textHeight = 0; | ||
| offsetx = 0; | ||
| } | ||
| textHeight = Math.max(textHeight, textHeights[i]); | ||
| offsetsx[i] = offsetx; | ||
| offsetx += width; | ||
| canvasX = Math.max(canvasX, offsetx); | ||
| lines[i] = line; | ||
| } | ||
| offsety = lineoffsetsy[line] = offsety + 2*textHeight; | ||
| for (i = 0; i < text.length; i++) { | ||
| offsetsy[i] = lineoffsetsy[lines[i]]; | ||
| } | ||
|
|
||
| canvasX = this.getPowerOfTwo(canvasX); | ||
| canvasY = this.getPowerOfTwo(offsety); | ||
|
|
||
| canvas.width = canvasX; | ||
| canvas.height = canvasY; | ||
|
|
||
| ctx.fillStyle = backgroundColour; | ||
| ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); | ||
|
|
||
| ctx.textBaseline = "alphabetic"; | ||
| for(i = 0; i < text.length; i++) { | ||
| ctx.font = fontStrings[i]; | ||
| ctx.fillStyle = textColour; | ||
| ctx.textAlign = "left"; | ||
| ctx.fillText(text[i], offsetsx[i], offsetsy[i]); | ||
| } | ||
| return {canvasX:canvasX, canvasY:canvasY, | ||
| widths:widths, textHeights:textHeights, | ||
| offsetsx:offsetsx, offsetsy:offsetsy}; | ||
| }; | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
|
|
||
| Copyright (c) 2015-2016, Speros Kokenes | ||
| All rights reserved. | ||
|
|
||
| Redistribution and use in source and binary forms, with or without | ||
| modification, are permitted provided that the following conditions are met: | ||
|
|
||
| 1. Redistributions of source code must retain the above copyright notice, this | ||
| list of conditions and the following disclaimer. | ||
|
|
||
| 2. Redistributions in binary form must reproduce the above copyright notice, | ||
| this list of conditions and the following disclaimer in the documentation | ||
| and/or other materials provided with the distribution. | ||
|
|
||
| 3. Neither the name of the copyright holder nor the names of its contributors | ||
| may be used to endorse or promote products derived from this software | ||
| without specific prior written permission. | ||
|
|
||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE | ||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| lasso | ||
| ========= | ||
|
|
||
| lasso.js is a D3 plugin that allows you to tag elements on a page by drawing a line over or around objects. Functions can be run based on the lasso action. This functionality can be useful for brushing or filtering. | ||
|
|
||
| An example of the lasso implemented in a scatterplot can be found here: [http://bl.ocks.org/skokenes/511c5b658c405ad68941](http://bl.ocks.org/skokenes/511c5b658c405ad68941) | ||
|
|
||
| This example is based off of Mike Bostock's scatterplot example here: [http://bl.ocks.org/mbostock/3887118](http://bl.ocks.org/mbostock/3887118) | ||
|
|
||
| Lassoing tags | ||
| -- | ||
| When the lasso is used, it tags elements by adding properties to their data. The properties are: | ||
|
|
||
| - possible: while drawing a lasso, if an element is part of the final selection that would be made if the lasso was completed at that instance, this value is true. Otherwise, it is false. | ||
| - selected: when a lasso is completed, all elements that were tagged as possible are given a selected value of true. Otherwise, the value is false. | ||
|
|
||
| The tags can be used in combination with functions to perform actions like styling the possible or selected values while the lasso is in use. | ||
|
|
||
| Note that the lasso only works with elements whose data is defined as an object. | ||
|
|
||
|
|
||
| Function Overview | ||
| -- | ||
| **d3.lasso**() | ||
|
|
||
| Creates a new lasso object. This object can then have parameters set before the lasso is drawn. | ||
| ``` | ||
| var lasso = d3.lasso(); // creates a new lasso | ||
| ``` | ||
|
|
||
| lasso.**items**(_[selection]_) | ||
|
|
||
| The items() parameter takes in a d3 selection. Each element in the selection will be tagged with lasso-specific properties when the lasso is used. If no input is specified, the function returns the lasso's current items. | ||
| ``` | ||
| lasso.items(d3.selectAll("circle")); // sets all circles on the page to be lasso-able | ||
| ``` | ||
|
|
||
| lasso.**hoverSelect**(_[bool]_) | ||
|
|
||
| The hoverSelect() parameter takes in a boolean that determines whether objects can be lassoed by hovering over an element during lassoing. The default value is set to true. If no input is specified, the function returns the lasso's current hover parameter. | ||
| ``` | ||
| lasso.hoverSelect(true); // allows hovering of elements for selection during lassoing | ||
| ``` | ||
|
|
||
| lasso.**closePathSelect**(_[bool]_) | ||
|
|
||
| The closePathSelect() parameter takes in a boolean that determines whether objects can be lassoed by drawing a loop around them. The default value is set to true. If no input is specified, the function returns the lasso's current parameter. | ||
| ``` | ||
| lasso.closePathSelect(true); // allows looping of elements for selection during lassoing | ||
| ``` | ||
|
|
||
| lasso.**closePathDistance**(_[num]_) | ||
|
|
||
| The closePathDistance() parameter takes in a number that specifies the maximum distance in pixels from the lasso origin that a lasso needs to be drawn in order to complete the loop and select elements. This parameter only works if closePathSelect is set to true; If no input is specified, the function returns the lasso's current parameter. | ||
| ``` | ||
| lasso.closePathDistance(75); // the lasso loop will complete itself whenever the lasso end is within 75 pixels of the origin | ||
| ``` | ||
|
|
||
| lasso.**area**(_[sel]_) | ||
|
|
||
| The area() parameter takes in a selection representing the element to be used as a target area for the lasso event. If no input is specified, the function returns the current area selection. | ||
| ``` | ||
| lasso.area(d3.select("#myLassoRect")); // the lasso will be trigger whenever a user clicks and drags on #myLassoRect | ||
| ``` | ||
|
|
||
| lasso.**on**(_type,[func]_) | ||
|
|
||
| The on() parameter takes in a type of event and a function for that event. There are 3 types of events that can be defined: | ||
| - start: this function will be executed whenever a lasso is started | ||
| - draw: this function will execute repeatedly as the lasso is drawn | ||
| - end: this function will be executed whenever a lasso is completed | ||
|
|
||
| If no function is specified, the function will return the current function defined for the type specified. | ||
| ``` | ||
| lasso.on("start",function() { alert("lasso started!"); }); // every time a lasso is started, an alert will trigger | ||
| ``` | ||
|
|
||
| Initiating a lasso | ||
| -- | ||
| Once a lasso object is defined, it can be added to a page by calling it on an element like an svg. | ||
| ``` | ||
| var lasso = d3.lasso() | ||
| .items(d3.selectAll("circle")) // Create a lasso and provide it some target elements | ||
| .area(de.select("#myLassoRect")); // Sets the drag area for the lasso on the rectangle #myLassoRect | ||
| d3.select("svg").call(lasso); // Initiate the lasso on an svg element | ||
| ``` | ||
|
|
||
| If a lasso is going to be used on graphical elements that have been translated via a g element acting as a container, which is a common practice for incorporating chart margins, then the lasso should be called on that g element so that it is in the same coordinate system as the graphical elements. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| .lasso path { | ||
| stroke: rgb(80,80,80); | ||
| stroke-width: 2px; | ||
| } | ||
|
|
||
| .lasso .drawn { | ||
| fill: #CCCCCC; | ||
| fill-opacity: .15 ; | ||
| } | ||
|
|
||
| .lasso .loop_close { | ||
| fill: none; | ||
| stroke-dasharray: 4,4; | ||
| } | ||
|
|
||
| .lasso .origin { | ||
| fill: #3399FF; | ||
| fill-opacity: .5; | ||
| } | ||
|
|
||
| .scatterD3 .not-possible-lasso { | ||
| fill: rgb(150,150,150); | ||
| opacity: 1; | ||
| } | ||
|
|
||
| .scatterD3 .arrow.not-possible-lasso { | ||
| stroke: rgb(200,200,200); | ||
| } | ||
|
|
||
| .scatterD3 .point-label-line.not-possible-lasso { | ||
| stroke: rgb(200,200,200); | ||
| } | ||
|
|
||
| .scatterD3 .possible-lasso { | ||
| fill: #EC888C; | ||
| opacity: 1; | ||
| } | ||
|
|
||
| .scatterD3 .arrow.possible-lasso { | ||
| stroke: #EC888C; | ||
| } | ||
|
|
||
| .scatterD3 .point-label-line.possible-lasso { | ||
| stroke: #EC888C; | ||
| } | ||
|
|
||
| /*.scatterD3 .selected-lasso { | ||
| opacity: 1 !important; | ||
| } | ||
| .scatterD3 .not-selected-lasso { | ||
| opacity: 0.1 !important; | ||
| }*/ |