From e697fa989542d09fff471c2d6aa6294191ecf310 Mon Sep 17 00:00:00 2001 From: Gordon Woodhull Date: Fri, 5 Sep 2014 11:59:50 -0400 Subject: [PATCH 1/2] basic GitHub Pages just to use GitHub as a CDN --- dataframe.js | 73 ++++ dcplot.js | 1021 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1094 insertions(+) create mode 100644 dataframe.js create mode 100644 dcplot.js diff --git a/dataframe.js b/dataframe.js new file mode 100644 index 0000000..e675c52 --- /dev/null +++ b/dataframe.js @@ -0,0 +1,73 @@ +/* an attempt to wrap dataframe access in an abstract + interface that should work for other data too (?) + note: test on array-of-records data! + */ +(function() { + var dataframe = { + cols: function(data) { + var result = { + access: function(column) { + if(!(column in data)) + throw "dataframe doesn't have column " + column.toString(); + var columnv = data[column]; + var f = function(i) { + return columnv[i]; + }; + // access e.g. R attributes through access("col").attrs + f.attrs = columnv; + return f; + }, + index: function(i) { + return i; + }, + num_rows: function() { + for(var col in data) + return data[col].length; + }, + has: function(k) { + return k in data; + }, + records: function() { + return _.range(0, this.num_rows()); + } + }; + return result; + }, + rows: function(data) { + var result = { + access: function(column) { + var f = function(i) { + return data[i][column]; + }; + // to support attributes, add a field called 'attrs' + // to the row array! + if('attrs' in data && _.isObject(data.attrs)) + f.attrs = data.attrs[column]; + return f; + }, + // we could get rid of row indices and just use rows + // except here (?) + index: function(i) { + return i; + }, + num_rows: function() { + return data.length; + }, + has: function(k) { + return k in data[0]; + }, + records: function() { + return _.range(0, this.num_rows()); + } + }; + return result; + } + }; + if(typeof define === "function" && define.amd) { + define(dataframe); + } else if(typeof module === "object" && module.exports) { + module.exports = dataframe; + } else { + this.dataframe = dataframe; + } +})(); diff --git a/dcplot.js b/dcplot.js new file mode 100644 index 0000000..d669f40 --- /dev/null +++ b/dcplot.js @@ -0,0 +1,1021 @@ +/* + dcplot: a minimal interface to dc.js with ggplot-like defaulting + + takes a description in json+function format describing crossfilter dimensions and groups, + and charts. returns the resulting charts in a map. + */ + +(function() { + function _dcplot(dc, crossfilter) { + // todo? the groupvalue function could access subfields of the dimension value? + dcplot.group = { + identity: function(dim) { return dim.group(); }, + bin: function(binwidth) { + var f = function(dim) { + return dim.group( + function(x) { + return Math.floor(x/binwidth)*binwidth; + }); + }; + f.binwidth = binwidth; + return f; + } + }; + + // yes! these are fourth-order functions! + // the methods on this object take an access-thing and return an object for accessor() + // accessor() will bind access to a real accessor function + // that function is ready to take a group + // and pass it the functions it composes to call the true accessor + dcplot.reduce = { + count: function(group) { return group.reduceCount(); }, + countFilter: function(access, level) { + return dcplot.reduce.sum(function (a) { + return (access(a) === level) ? 1 : 0; + }); + }, + filter: function(reduce, access, level) { + function wrapper(acc) { + return function (a) { + return (access(a) === level) ? acc(a) : 0; + }; + } + return { + arg: reduce.arg, + fun: function(acc) { return reduce.fun(wrapper(acc)); } + }; + }, + sum: function(access, wacc) { + return { + arg: access, + fun: function(acc2) { + if(wacc === undefined) + return function(group) { + return group.reduceSum( + function(item) { + return acc2(item); + } + ); + }; + else return function(group) { + return group.reduce( + function(p, v) { + p.sum += (acc2(v)*wacc(v)); + return p; + }, + function(p, v) { + p.sum -= (acc2(v)*wacc(v)); + return p; + }, + function(p, v) { + return {sum: 0, valueOf: function() { return this.sum; }}; + }); + }; + } + }; + }, + any: function(access) { + return { + arg: access, + fun: function(acc2) { + return function(group) { + return group.reduce( + function(p, v) { + return acc2(v); + }, + function(p, v) { + return p; + }, + function(p, v) { + return 0; + }); + }; + } + }; + }, + avg: function(access, wacc) { + return { + arg: access, + fun: function(acc2) { + if(wacc === undefined) return function(group) { + return group.reduce( + function(p, v) { + ++p.count; + p.sum += acc2(v); + p.avg = p.sum / p.count; + return p; + }, + function(p, v) { + --p.count; + p.sum -= acc2(v); + p.avg = p.count ? p.sum / p.count : 0; + return p; + }, + function(p, v) { + return {count: 0, sum: 0, avg: 0, valueOf: function() { return this.avg; }}; + }); + }; + else return function(group) { + return group.reduce( + function(p, v) { + p.count += wacc(v); + p.sum += (acc2(v)*wacc(v)); + p.avg = p.sum / p.count; + return p; + }, + function(p, v) { + p.count -= wacc(v); + p.sum -= (acc2(v)*wacc(v)); + p.avg = p.count ? p.sum / p.count : 0; + return p; + }, + function(p, v) { + return {count: 0, sum: 0, avg: 0, valueOf: function() { return this.avg; }}; + }); + }; + } + }; + }, + value: function(field) { + return function(key, value) { + return value[field]; + }; + } + }; + + /* + many stages of filling in the blanks for dimensions, groups, and charts + + 1. fill in defaults for missing attributes + 2. infer other missing attributes from what's there + 3. check for required and unknown attributes + 4. check for logical errors + 5. finally, generate + + */ + + // a map of attr->required to check for at the end to make sure we have everything + // warning: don't put method calls for defaults which must be constructed each time! + var chart_attrs = { + base: { + supported: true, + div: {required: true}, // actually sent to parent selector for chart constructor + title: {required: false}, // title for html in the div, handled outside this lib + dimension: {required: true}, + group: {required: true}, + ordering: {required: false}, + width: {required: true, default: 300}, + height: {required: true, default: 300}, + 'transition.duration': {required: false}, + label: {required: false}, // or null for no labels + tips: {required: false}, // dc 'title', or null for no tips + more: {required: false} // executes arbitrary extra code on the dc.js chart object + // key, value are terrible names: handle as variables below + }, + color: { + supported: true, + color: {required: false}, // colorAccessor + 'color.scale': {required: false}, // the d3 way not the dc way + 'color.domain': {required: false}, + 'color.range': {required: false} + }, + stackable: { + supported: true, + stack: {required: false}, + 'stack.levels': {required: false} + }, + coordinateGrid: { + supported: true, + parents: ['base', 'color'], + margins: {required: false}, + x: {required: false}, // keyAccessor + y: {required: false}, // valueAccessor + // prob would be good to subgroup these? + 'x.ordinal': {required: false}, + 'x.scale': {required: true}, // scale component of x + 'x.domain': {required: false}, // domain component of x + 'x.units': {required: false}, // the most horrible thing EVER + 'x.round': {required: false}, + 'x.elastic': {required: false}, + 'x.padding': {required: false}, + // likewise + 'y.scale': {required: false}, + 'y.domain': {required: false}, + 'y.elastic': {required: false}, + 'y.padding': {required: false}, + gridLines: {required: false}, // horizontal and/or vertical + brush: {required: false} + // etc... + }, + pie: { + supported: true, + concrete: true, + parents: ['base', 'color'], + radius: {required: false}, + innerRadius: {required: false}, + wedge: {required: false}, // keyAccessor (okay these could just be x/y) + size: {required: false} // valueAccessor + // etc... + }, + row: { + supported: false, + parents: ['base', 'color'] + }, + bar: { + supported: true, + concrete: true, + parents: ['coordinateGrid', 'stackable'], + width: {default: 700}, + height: {default: 250}, + centerBar: {required: false}, + gap: {required: false}, + 'color.x': {default: true}, // color bars individually when not stacked + 'x.units': {required: true} // the most horrible thing EVER + }, + line: { + supported: true, + concrete: true, + parents: ['coordinateGrid', 'stackable'], + width: {default: 800}, + height: {default: 250}, + area: {required: false}, + dotRadius: {required: false} + }, + composite: { + parents: ['coordinateGrid'], + supported: false + }, + abstractBubble: { + supported: true, + parents: ['color'], + r: {default: 2}, // radiusValueAccessor + 'r.scale': {required: false}, // scale component of r + 'r.domain': {required: false} // domain component of r + }, + bubble: { + concrete: true, + parents: ['coordinateGrid', 'abstractBubble'], + width: {default: 400}, + label: {default: null}, // do not label by default; use ..key.. to label with keys + color: {default: 0}, // by default use first color in palette + supported: true, + 'r.elastic': {required: false} + }, + bubbleOverlay: { + supported: false, // this chart is a crime! + parents: ['base', 'abstractBubble'] + }, + geoCloropleth: { + supported: false + }, + dataCount: { + supported: false + }, + dataTable: { + supported: true, + concrete: true, + parents: ['base'], + columns: {required: true}, + size: {required: false}, + sortBy: {required: false} + } + }; + + function skip_attr(a) { + return a==='supported' || a==='concrete' || a==='parents'; + } + + function parents_first_traversal(map, iter, callbacks) { + if(!(iter in map)) + throw 'unknown chart type ' + defn.type; + var curr = map[iter]; + if('parents' in curr) + for(var i = 0; i < curr.parents.length; ++i) + parents_first_traversal(map, curr.parents[i], callbacks); + callbacks[iter](); + } + function parents_last_traversal(map, iter, callbacks) { + if(!(iter in map)) + throw 'unknown chart type ' + defn.type; + callbacks[iter](); + var curr = map[iter]; + if('parents' in curr) + for(var i = 0; i < curr.parents.length; ++i) + parents_last_traversal(map, curr.parents[i], callbacks); + } + + // dc.js formats all numbers as ints - override + var _psv = dc.utils.printSingleValue; + dc.utils.printSingleValue = function(filter) { + if(typeof(filter) === 'number') { + if(filter%1 === 0) + return filter; + else if(filter>10000 || filter < -10000) + return Math.round(filter); + else + return filter.toPrecision(4); + } + else return _psv(filter); + }; + + dcplot.format_error = function(e) { + var tab; + if(_.isArray(e)) { // expected exception: input error + tab = $(''); + $.each(e, function(i) { + var err = e[i], formatted_errors = $(''). + append($('
'); + if(_.isString(err.errors)) + formatted_errors.text(err.errors); + else if(_.isArray(err.errors)) + $.each(err.errors, function(e) { + formatted_errors.append($('

').text(err.errors[e])); + }); + else formatted_errors.text(err.errors.message.toString()); + var name = err.name.replace(/_\d*_\d*$/, ''); + tab.append($('

').text(err.type)). + append($('').text(name)). + append(formatted_errors) + ); + }); + } + else // unexpected exception: probably logic error + tab = $('

').text(e.toString()); + var error_report = $('

'). + append($('

').text('dcplot errors!')). + append(tab); + return error_report; + }; + + function dcplot(frame, groupname, definition) { + + // generalization of _.has + function mhas(obj) { + for(var i=1; i10) ? + d3.scale.category20() : d3.scale.category10(); + } + if(!defn['color.domain']) { + // this also should be abstracted out into a plugin (RCloud-specific) + if(mhas(defn, 'color', 'attrs', 'r_attributes', 'levels')) + defn['color.domain'] = defn.color.attrs.r_attributes.levels; + } + }, + stackable: function() { + if(_.has(defn,'stack')) { + if(!_.has(defn,'stack.levels')) + defn['stack.levels'] = get_levels(defn.stack); + var levels = defn['stack.levels']; + + // Change reduce functions to filter on stack levels + for(var s = 0; s key + * group.group functions are key -> key + * group.reduce functions are key -> value + in dc: + * accessor functions are {key,value} -> whatever + + so instead we make them (key,value) -> whatever and then they look like + crossfilter functions! + */ + function key_value(f) { return function(kv) { return f(kv.key, kv.value); }; } + + var callbacks = { + base: function() { + chart = ctor(defn.div, groupname); + chart.dimension(dimensions[defn.dimension]) + .group(groups[defn.group]) + .width(defn.width) + .height(defn.height); + if(_.has(defn, 'ordering')) + chart.ordering(defn.ordering); + if(_.has(defn, 'transition.duration')) + chart.transitionDuration(defn['transition.duration']); + if(_.has(defn, 'label')) { + if(defn.label) + chart.label(key_value(defn.label)); + else + chart.renderLabel(false); + } + if(_.has(defn, 'tips')) { + if(defn.tips) + chart.title(key_value(defn.tips)); + else + chart.renderTitle(false); + } + }, + color: function() { + // i am cool with dc.js's color accessor + if(_.has(defn, 'color')) + chart.colorAccessor(key_value(accessor(defn.color))); + // however i don't understand why dc chooses to use a + // "color calculator" when a d3 scale seems like it ought + // to serve the purpose. so just plug a d3 scale into colors + // and override the calculator to use it + // also default to category10 which seems better for discrete colors + + var scale = defn['color.scale']; + if(_.has(defn, 'color.domain')) + scale.domain(defn['color.domain']); + if(_.has(defn, 'color.range')) + scale.range(defn['color.range']); + chart.colors(scale); + chart.colorCalculator(function(x) { return chart.colors()(x); }); + }, + stackable: function() { + if(_.has(defn, 'stack') && _.has(defn, 'stack.levels')) { + for(var s = 0; s Date: Fri, 5 Sep 2014 12:03:34 -0400 Subject: [PATCH 2/2] add silly example --- silly/index.html | 20 ++++++ silly/test.dcplot.small.js | 135 +++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 silly/index.html create mode 100644 silly/test.dcplot.small.js diff --git a/silly/index.html b/silly/index.html new file mode 100644 index 0000000..4d6a843 --- /dev/null +++ b/silly/index.html @@ -0,0 +1,20 @@ + + + Codestin Search App + + + + + + + + + + + + + + + + + diff --git a/silly/test.dcplot.small.js b/silly/test.dcplot.small.js new file mode 100644 index 0000000..0862703 --- /dev/null +++ b/silly/test.dcplot.small.js @@ -0,0 +1,135 @@ +// simple illustration of how to use dcplot.js + +// nonsense data. the dataframe can be generated from column-major or row-major data +var data_rows = [ + {x:0, y:0.1}, + {x:0.1, y:0.2}, + {x:0.15, y:0.17}, + {x:0.3, y:0.23}, + {x:0.35, y:0.28}, + {x:0.37, y:0.3}, + {x:0.4, y:0.33}, + {x:0.5, y:0.29}, + {x:0.61, y:0.45}, + {x:0.8, y:0.51} +]; + +var data_cols = { + x: [0, 0.1, 0.15, 0.3, 0.35, 0.37, 0.4, 0.5, 0.61, 0.8], + y: [0.1, 0.2, 0.17, 0.23, 0.28, 0.3, 0.33, 0.29, 0.45, 0.51], + r: [2, 3, 4, 2, 3, 4, 2, 3, 4, 2], + c: ['a', 'b', 'c', 'b', 'b', 'a', 'a', 'a', 'b', 'c'] +}; + +// the dataframe is based on the concept from R, here we use R-like column-major data +var frame = dataframe.cols(data_cols); + +var cgname = 'chartgroup0'; + +// some annoying boilerplate to generate the filter display and reset links +// and put them in the chart divs - this could perhaps be automated better by dcplot.js +function make_chart_div(name, group_name) { + var props = {id: name, style: "float:left"}; + var reset = $('', + {class: 'reset', + href: '#', + style: "display: none;"}) + .append("reset") + .click(function(e) { + e.preventDefault(); + window.charts[name].filterAll(); + dc.redrawAll(group_name); + }); + + return $('

',props) + .append($('
') + .append($('').append(name)) + .append('  ') + .append($('', {class: 'reset', style: 'display: none;'}) + .append('Current filter: ') + .append($('', {class: 'filter'}))) + .append('  ') + .append(reset) + + ); +} + +// generate div for each chart +var divs = _.reduce(['bubs', 'lines', 'bars', 'colbars', 'series'], + function(memo, dname) { + memo[dname] = make_chart_div(dname, cgname); + return memo; + }, {}); + + +var body = $('body'); + +// okay now comes the cool part ;-) +// have you ever seen so little code to generate interactive charts? +try { + window.charts = dcplot(frame, cgname, { + dimensions: { + index: frame.index, + x: 'x', + x2: 'x', + col: 'c' + }, + groups: { + index: { dimension: 'index' }, + nongroup: { + dimension: 'x', + reduce: dcplot.reduce.any('y') + }, + bingroup: { + dimension: 'x2', + group: dcplot.group.bin(0.2) + }, + colgroup: { + dimension: 'col' + } + }, + charts: { + bubs: { + div: divs['bubs'][0], + type: 'bubble', + dimension: 'index', + width: 800, + x: 'x', + y: 'y', + color: 'c', + r: function(k,v) { return frame.access('r')(k) * v; }, + 'r.domain': [1,15], + label: null + }, + lines: { + div: divs['lines'][0], + type: 'line', + group: 'nongroup', + tips: function(x, y) { return x + ', ' + y; } + }, + bars: { + div: divs['bars'][0], + type: 'bar', + group: 'bingroup' + }, + colbars: { + div: divs['colbars'][0], + type: 'bar', + group: 'colgroup' + } + }}); + + $.each(divs, + function(d) { + body.append(divs[d]); + }); +} +catch(e) { + body.append(dcplot.format_error(e)); +} + +/* note the lines chart could also be expressed this way (more like bubble chart) + dimension: 'index', + x: 'x', + y: 'y', +*/