blacklight_range_limit 1.0.0pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. data/.gitignore +21 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +141 -0
  4. data/Rakefile +5 -0
  5. data/VERSION +1 -0
  6. data/app/helpers/range_limit_helper.rb +89 -0
  7. data/app/views/blacklight_range_limit/_range_limit_panel.html.erb +74 -0
  8. data/app/views/blacklight_range_limit/_range_segments.html.erb +14 -0
  9. data/blacklight_range_limit.gemspec +23 -0
  10. data/config/routes.rb +6 -0
  11. data/lib/blacklight_range_limit.rb +55 -0
  12. data/lib/blacklight_range_limit/controller_override.rb +139 -0
  13. data/lib/blacklight_range_limit/engine.rb +17 -0
  14. data/lib/blacklight_range_limit/route_sets.rb +12 -0
  15. data/lib/blacklight_range_limit/segment_calculation.rb +103 -0
  16. data/lib/blacklight_range_limit/version.rb +10 -0
  17. data/lib/blacklight_range_limit/view_helper_override.rb +82 -0
  18. data/lib/generators/blacklight_range_limit/assets_generator.rb +25 -0
  19. data/lib/generators/blacklight_range_limit/blacklight_range_limit_generator.rb +11 -0
  20. data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/excanvas.min.js +1 -0
  21. data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/jquery.flot.js +2513 -0
  22. data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/jquery.flot.selection.js +328 -0
  23. data/lib/generators/blacklight_range_limit/templates/public/javascripts/range_limit_distro_facets.js +211 -0
  24. data/lib/generators/blacklight_range_limit/templates/public/javascripts/range_limit_slider.js +83 -0
  25. data/lib/generators/blacklight_range_limit/templates/public/stylesheets/blacklight_range_limit.css +13 -0
  26. metadata +122 -0
@@ -0,0 +1,328 @@
1
+ /*
2
+ Flot plugin for selecting regions.
3
+
4
+ The plugin defines the following options:
5
+
6
+ selection: {
7
+ mode: null or "x" or "y" or "xy",
8
+ color: color
9
+ }
10
+
11
+ Selection support is enabled by setting the mode to one of "x", "y" or
12
+ "xy". In "x" mode, the user will only be able to specify the x range,
13
+ similarly for "y" mode. For "xy", the selection becomes a rectangle
14
+ where both ranges can be specified. "color" is color of the selection.
15
+
16
+ When selection support is enabled, a "plotselected" event will be
17
+ emitted on the DOM element you passed into the plot function. The
18
+ event handler gets a parameter with the ranges selected on the axes,
19
+ like this:
20
+
21
+ placeholder.bind("plotselected", function(event, ranges) {
22
+ alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
23
+ // similar for yaxis - with multiple axes, the extra ones are in
24
+ // x2axis, x3axis, ...
25
+ });
26
+
27
+ The "plotselected" event is only fired when the user has finished
28
+ making the selection. A "plotselecting" event is fired during the
29
+ process with the same parameters as the "plotselected" event, in case
30
+ you want to know what's happening while it's happening,
31
+
32
+ A "plotunselected" event with no arguments is emitted when the user
33
+ clicks the mouse to remove the selection.
34
+
35
+ The plugin allso adds the following methods to the plot object:
36
+
37
+ - setSelection(ranges, preventEvent)
38
+
39
+ Set the selection rectangle. The passed in ranges is on the same
40
+ form as returned in the "plotselected" event. If the selection mode
41
+ is "x", you should put in either an xaxis range, if the mode is "y"
42
+ you need to put in an yaxis range and both xaxis and yaxis if the
43
+ selection mode is "xy", like this:
44
+
45
+ setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
46
+
47
+ setSelection will trigger the "plotselected" event when called. If
48
+ you don't want that to happen, e.g. if you're inside a
49
+ "plotselected" handler, pass true as the second parameter. If you
50
+ are using multiple axes, you can specify the ranges on any of those,
51
+ e.g. as x2axis/x3axis/... instead of xaxis, the plugin picks the
52
+ first one it sees.
53
+
54
+ - clearSelection(preventEvent)
55
+
56
+ Clear the selection rectangle. Pass in true to avoid getting a
57
+ "plotunselected" event.
58
+
59
+ - getSelection()
60
+
61
+ Returns the current selection in the same format as the
62
+ "plotselected" event. If there's currently no selection, the
63
+ function returns null.
64
+
65
+ */
66
+
67
+ (function ($) {
68
+ function init(plot) {
69
+ var selection = {
70
+ first: { x: -1, y: -1}, second: { x: -1, y: -1},
71
+ show: false,
72
+ active: false
73
+ };
74
+
75
+ // FIXME: The drag handling implemented here should be
76
+ // abstracted out, there's some similar code from a library in
77
+ // the navigation plugin, this should be massaged a bit to fit
78
+ // the Flot cases here better and reused. Doing this would
79
+ // make this plugin much slimmer.
80
+ var savedhandlers = {};
81
+
82
+ function onMouseMove(e) {
83
+ if (selection.active) {
84
+ plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
85
+
86
+ updateSelection(e);
87
+ }
88
+ }
89
+
90
+ function onMouseDown(e) {
91
+ if (e.which != 1) // only accept left-click
92
+ return;
93
+
94
+ // cancel out any text selections
95
+ document.body.focus();
96
+
97
+ // prevent text selection and drag in old-school browsers
98
+ if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
99
+ savedhandlers.onselectstart = document.onselectstart;
100
+ document.onselectstart = function () { return false; };
101
+ }
102
+ if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
103
+ savedhandlers.ondrag = document.ondrag;
104
+ document.ondrag = function () { return false; };
105
+ }
106
+
107
+ setSelectionPos(selection.first, e);
108
+
109
+ selection.active = true;
110
+
111
+ $(document).one("mouseup", onMouseUp);
112
+ }
113
+
114
+ function onMouseUp(e) {
115
+ // revert drag stuff for old-school browsers
116
+ if (document.onselectstart !== undefined)
117
+ document.onselectstart = savedhandlers.onselectstart;
118
+ if (document.ondrag !== undefined)
119
+ document.ondrag = savedhandlers.ondrag;
120
+
121
+ // no more draggy-dee-drag
122
+ selection.active = false;
123
+ updateSelection(e);
124
+
125
+ if (selectionIsSane())
126
+ triggerSelectedEvent();
127
+ else {
128
+ // this counts as a clear
129
+ plot.getPlaceholder().trigger("plotunselected", [ ]);
130
+ plot.getPlaceholder().trigger("plotselecting", [ null ]);
131
+ }
132
+
133
+ return false;
134
+ }
135
+
136
+ function getSelection() {
137
+ if (!selectionIsSane())
138
+ return null;
139
+
140
+ var r = {}, c1 = selection.first, c2 = selection.second;
141
+ $.each(plot.getAxes(), function (name, axis) {
142
+ if (axis.used) {
143
+ var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]);
144
+ r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
145
+ }
146
+ });
147
+ return r;
148
+ }
149
+
150
+ function triggerSelectedEvent() {
151
+ var r = getSelection();
152
+
153
+ plot.getPlaceholder().trigger("plotselected", [ r ]);
154
+
155
+ // backwards-compat stuff, to be removed in future
156
+ if (r.xaxis && r.yaxis)
157
+ plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
158
+ }
159
+
160
+ function clamp(min, value, max) {
161
+ return value < min ? min: (value > max ? max: value);
162
+ }
163
+
164
+ function setSelectionPos(pos, e) {
165
+ var o = plot.getOptions();
166
+ var offset = plot.getPlaceholder().offset();
167
+ var plotOffset = plot.getPlotOffset();
168
+ pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
169
+ pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
170
+
171
+ if (o.selection.mode == "y")
172
+ pos.x = pos == selection.first ? 0 : plot.width();
173
+
174
+ if (o.selection.mode == "x")
175
+ pos.y = pos == selection.first ? 0 : plot.height();
176
+ }
177
+
178
+ function updateSelection(pos) {
179
+ if (pos.pageX == null)
180
+ return;
181
+
182
+ setSelectionPos(selection.second, pos);
183
+ if (selectionIsSane()) {
184
+ selection.show = true;
185
+ plot.triggerRedrawOverlay();
186
+ }
187
+ else
188
+ clearSelection(true);
189
+ }
190
+
191
+ function clearSelection(preventEvent) {
192
+ if (selection.show) {
193
+ selection.show = false;
194
+ plot.triggerRedrawOverlay();
195
+ if (!preventEvent)
196
+ plot.getPlaceholder().trigger("plotunselected", [ ]);
197
+ }
198
+ }
199
+
200
+ // taken from markings support
201
+ function extractRange(ranges, coord) {
202
+ var axis, from, to, axes, key;
203
+
204
+ axes = plot.getUsedAxes();
205
+ for (i = 0; i < axes.length; ++i) {
206
+ axis = axes[i];
207
+ if (axis.direction == coord) {
208
+ key = coord + axis.n + "axis";
209
+ if (!ranges[key] && axis.n == 1)
210
+ key = coord + "axis"; // support x1axis as xaxis
211
+ if (ranges[key]) {
212
+ from = ranges[key].from;
213
+ to = ranges[key].to;
214
+ break;
215
+ }
216
+ }
217
+ }
218
+
219
+ // backwards-compat stuff - to be removed in future
220
+ if (!ranges[key]) {
221
+ axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0];
222
+ from = ranges[coord + "1"];
223
+ to = ranges[coord + "2"];
224
+ }
225
+
226
+ // auto-reverse as an added bonus
227
+ if (from != null && to != null && from > to) {
228
+ var tmp = from;
229
+ from = to;
230
+ to = tmp;
231
+ }
232
+
233
+ return { from: from, to: to, axis: axis };
234
+ }
235
+
236
+
237
+ function setSelection(ranges, preventEvent) {
238
+ var axis, range, o = plot.getOptions();
239
+
240
+ if (o.selection.mode == "y") {
241
+ selection.first.x = 0;
242
+ selection.second.x = plot.width();
243
+ }
244
+ else {
245
+ range = extractRange(ranges, "x");
246
+
247
+ selection.first.x = range.axis.p2c(range.from);
248
+ selection.second.x = range.axis.p2c(range.to);
249
+ }
250
+
251
+ if (o.selection.mode == "x") {
252
+ selection.first.y = 0;
253
+ selection.second.y = plot.height();
254
+ }
255
+ else {
256
+ range = extractRange(ranges, "y");
257
+
258
+ selection.first.y = range.axis.p2c(range.from);
259
+ selection.second.y = range.axis.p2c(range.to);
260
+ }
261
+
262
+ selection.show = true;
263
+ plot.triggerRedrawOverlay();
264
+ if (!preventEvent && selectionIsSane())
265
+ triggerSelectedEvent();
266
+ }
267
+
268
+ function selectionIsSane() {
269
+ var minSize = 5;
270
+ return Math.abs(selection.second.x - selection.first.x) >= minSize &&
271
+ Math.abs(selection.second.y - selection.first.y) >= minSize;
272
+ }
273
+
274
+ plot.clearSelection = clearSelection;
275
+ plot.setSelection = setSelection;
276
+ plot.getSelection = getSelection;
277
+
278
+ plot.hooks.bindEvents.push(function(plot, eventHolder) {
279
+ var o = plot.getOptions();
280
+ if (o.selection.mode != null)
281
+ eventHolder.mousemove(onMouseMove);
282
+
283
+ if (o.selection.mode != null)
284
+ eventHolder.mousedown(onMouseDown);
285
+ });
286
+
287
+
288
+ plot.hooks.drawOverlay.push(function (plot, ctx) {
289
+ // draw selection
290
+ if (selection.show && selectionIsSane()) {
291
+ var plotOffset = plot.getPlotOffset();
292
+ var o = plot.getOptions();
293
+
294
+ ctx.save();
295
+ ctx.translate(plotOffset.left, plotOffset.top);
296
+
297
+ var c = $.color.parse(o.selection.color);
298
+
299
+ ctx.strokeStyle = c.scale('a', 0.8).toString();
300
+ ctx.lineWidth = 1;
301
+ ctx.lineJoin = "round";
302
+ ctx.fillStyle = c.scale('a', 0.4).toString();
303
+
304
+ var x = Math.min(selection.first.x, selection.second.x),
305
+ y = Math.min(selection.first.y, selection.second.y),
306
+ w = Math.abs(selection.second.x - selection.first.x),
307
+ h = Math.abs(selection.second.y - selection.first.y);
308
+
309
+ ctx.fillRect(x, y, w, h);
310
+ ctx.strokeRect(x, y, w, h);
311
+
312
+ ctx.restore();
313
+ }
314
+ });
315
+ }
316
+
317
+ $.plot.plugins.push({
318
+ init: init,
319
+ options: {
320
+ selection: {
321
+ mode: null, // one of null, "x", "y" or "xy"
322
+ color: "#e8cfac"
323
+ }
324
+ },
325
+ name: 'selection',
326
+ version: '1.0'
327
+ });
328
+ })(jQuery);
@@ -0,0 +1,211 @@
1
+ jQuery(document).ready(function($) {
2
+ // Facets already on the page? Turn em into a chart.
3
+ $(".range_limit .profile .distribution.chart_js ul").each(function() {
4
+ turnIntoPlot($(this).parent());
5
+ });
6
+
7
+
8
+ // Add AJAX fetched range facets if needed, and add a chart to em
9
+ $(".range_limit .profile .distribution a.load_distribution").each(function() {
10
+ var container = $(this).parent('div.distribution');
11
+
12
+ $(container).load($(this).attr('href'), function(response, status) {
13
+ if ($(container).hasClass("chart_js") && status == "success" ) {
14
+ turnIntoPlot(container);
15
+ }
16
+ });
17
+ });
18
+
19
+ function turnIntoPlot(container) {
20
+ wrapPrepareForFlot($(container),
21
+ $(container).closest(".range_limit.limit_content"),
22
+ 1/(1.618 * 2), // half a golden rectangle, why not.
23
+ function(container) {
24
+ areaChart($(container));
25
+ });
26
+ }
27
+
28
+ // Takes a div holding a ul of distribution segments produced by
29
+ // blacklight_range_limit/_range_facets and makes it into
30
+ // a flot area chart.
31
+ function areaChart(container) {
32
+ //flot loaded? And canvas element supported.
33
+ if ( domDependenciesMet() ) {
34
+
35
+ // Grab the data from the ul div
36
+ var series_data = new Array();
37
+ var pointer_lookup = new Array();
38
+ var x_ticks = new Array();
39
+ var min = parseInt($(container).find("ul li:first-child span.from").text());
40
+ var max = parseInt($(container).find("ul li:last-child span.to").text());
41
+
42
+ $(container).find("ul li").each(function() {
43
+ var from = parseInt($(this).find("span.from").text());
44
+ var to = parseInt($(this).find("span.to").text());
45
+ var count = parseInt($(this).find("span.count").text());
46
+ var avg = (count / (to - from + 1));
47
+
48
+
49
+ //We use the avg as the y-coord, to make the area of each
50
+ //segment proportional to how many documents it holds.
51
+ series_data.push( [from, avg ] );
52
+ series_data.push( [to+1, avg] );
53
+
54
+ x_ticks.push(from);
55
+
56
+ pointer_lookup.push({'from': from, 'to': to, 'count': count, 'label': $(this).find(".facet_select").text() });
57
+ });
58
+ var max_plus_one = parseInt($(container).find("ul li:last-child span.to").text())+1;
59
+ x_ticks.push( max_plus_one );
60
+
61
+
62
+
63
+ var plot;
64
+ var config = $(container).closest('.facet_limit').data('plot-config') || {};
65
+
66
+ try {
67
+ plot = $.plot($(container), [series_data],
68
+ $.extend(true, config, {
69
+ yaxis: { ticks: [], min: 0, autoscaleMargin: 0.1},
70
+ //xaxis: { ticks: x_ticks },
71
+ xaxis: { tickDecimals: 0 }, // force integer ticks
72
+ series: { lines: { fill: true, steps: true }},
73
+ grid: {clickable: true, hoverable: true, autoHighlight: false},
74
+ selection: {mode: "x"}
75
+ }));
76
+ }
77
+ catch(err) {
78
+ alert(err);
79
+ }
80
+
81
+ // Div initially hidden to show hover mouseover legend for
82
+ // each segment.
83
+ $('<div class="subsection hover_legend ui-corner-all"></div>').css('display', 'none').insertAfter(container);
84
+
85
+ find_segment_for = function_for_find_segment(pointer_lookup);
86
+ $(container).bind("plothover", function (event, pos, item) {
87
+ segment = find_segment_for(pos.x);
88
+ showHoverLegend(container, '<span class="label">' + segment.label + '</span> <span class="count">(' + segment.count + ')</span>');
89
+ });
90
+ $(container).bind("mouseout", function() {
91
+ $(container).next(".hover_legend").hide();
92
+ });
93
+ $(container).bind("plotclick", function (event, pos, item) {
94
+ if ( plot.getSelection() == null) {
95
+ segment = find_segment_for(pos.x);
96
+ plot.setSelection( normalized_selection(segment.from, segment.to));
97
+ }
98
+ });
99
+ $(container).bind("plotselected plotselecting", function(event, ranges) {
100
+ if (ranges != null ) {
101
+ var from = Math.floor(ranges.xaxis.from);
102
+ var to = Math.floor(ranges.xaxis.to);
103
+
104
+ var form = $(container).closest(".limit_content").find("form.range_limit");
105
+ form.find("input.range_begin").val(from);
106
+ form.find("input.range_end").val(to);
107
+
108
+ var slider_container = $(container).closest(".limit_content").find(".profile .range");
109
+ slider_container.slider("values", 0, from);
110
+ slider_container.slider("values", 1, to+1);
111
+ }
112
+ });
113
+
114
+ var form = $(container).closest(".limit_content").find("form.range_limit");
115
+ form.find("input.range_begin, input.range_end").change(function () {
116
+ plot.setSelection( form_selection(form, min, max) , true );
117
+ });
118
+ $(container).closest(".limit_content").find(".profile .range").bind("slide", function(event, ui) {
119
+ plot.setSelection( normalized_selection(ui.values[0], Math.max(ui.values[0], ui.values[1]-1)), true);
120
+ });
121
+
122
+ // initially entirely selected, to match slider
123
+ plot.setSelection( {xaxis: { from:min, to:max+0.9999}} );
124
+
125
+ // try to make slider width/orientation match chart's
126
+ var slider_container = $(container).closest(".limit_content").find(".profile .range");
127
+ slider_container.width(plot.width());
128
+ slider_container.css('margin-right', 'auto');
129
+ slider_container.css('margin-left', 'auto');
130
+ // And set slider min/max to match charts, for sure
131
+ slider_container.slider("option", "min", min);
132
+ slider_container.slider("option", "max", max+1);
133
+ }
134
+ }
135
+
136
+
137
+ // Send endpoint to endpoint+0.99999 to have display
138
+ // more closely approximate limiting behavior esp
139
+ // at small resolutions. (Since we search on whole numbers,
140
+ // inclusive, but flot chart is decimal.)
141
+ function normalized_selection(min, max) {
142
+ max += 0.99999;
143
+
144
+ return {xaxis: { 'from':min, 'to':max}}
145
+ }
146
+
147
+ function form_selection(form, min, max) {
148
+ var begin_val = parseInt($(form).find("input.range_begin").val());
149
+ if (isNaN(begin_val) || begin_val < min) {
150
+ begin_val = min;
151
+ }
152
+ var end_val = parseInt($(form).find("input.range_end").val());
153
+ if (isNaN(end_val) || end_val > max) {
154
+ end_val = max;
155
+ }
156
+
157
+ return normalized_selection(begin_val, end_val);
158
+ }
159
+
160
+ function function_for_find_segment(pointer_lookup_arr) {
161
+ return function(x_coord) {
162
+ for (var i = pointer_lookup_arr.length-1 ; i >= 0 ; i--) {
163
+ var hash = pointer_lookup_arr[i];
164
+ if (x_coord >= hash.from)
165
+ return hash;
166
+ }
167
+ return pointer_lookup_arr[0];
168
+ };
169
+ }
170
+
171
+ function showHoverLegend(container, contents) {
172
+ var el = $(container).next(".hover_legend");
173
+
174
+ el.html(contents);
175
+ el.show();
176
+ }
177
+
178
+ // Check if Flot is loaded, and if browser has support for
179
+ // canvas object, either natively or via IE excanvas.
180
+ function domDependenciesMet() {
181
+ var flotLoaded = (typeof $.plot != "undefined");
182
+ var canvasAvailable = ((typeof(document.createElement('canvas').getContext) != "undefined") || (typeof window.CanvasRenderingContext2D != 'undefined' || typeof G_vmlCanvasManager != 'undefined'));
183
+
184
+ return (flotLoaded && canvasAvailable);
185
+ }
186
+
187
+ /* Set up dom for flot rendering: flot needs to render in a non-hidden
188
+ div with explicitly set width and height. The non-hidden thing
189
+ is annoying to us, since it might be in a hidden facet limit.
190
+ Can we get away with moving it off-screen? Not JUST the flot
191
+ container, or it will render weird. But the whole parent
192
+ limit content, testing reveals we can. */
193
+ function wrapPrepareForFlot(container, parent_section, widthToHeight, call_block) {
194
+ var parent_originally_hidden = $(parent_section).css("display") == "none";
195
+ if (parent_originally_hidden) {
196
+ $(parent_section).show();
197
+ }
198
+ $(container).width( $(parent_section).width() );
199
+ $(container).height( $(parent_section).width() * widthToHeight );
200
+ if (parent_originally_hidden) {
201
+ parent_section.addClass("ui-helper-hidden-accessible");
202
+ }
203
+
204
+ call_block(container);
205
+
206
+ if (parent_originally_hidden) {
207
+ $(parent_section).removeClass("ui-helper-hidden-accessible");
208
+ $(parent_section).hide();
209
+ }
210
+ }
211
+ });