diff --git a/doc/Architecture.md b/doc/Architecture.md index c416fdc..1dc3969 100644 --- a/doc/Architecture.md +++ b/doc/Architecture.md @@ -4,6 +4,10 @@ DataV组件库的设计 ## DataV结构 +- Chart +- Widget +- Component + ## API设计 ## 数据映射 diff --git a/example/stream/stream.html b/example/stream/stream.html index 51873fd..b8b917f 100644 --- a/example/stream/stream.html +++ b/example/stream/stream.html @@ -6,15 +6,25 @@ -
+
+
+ diff --git a/lib/charts/cover.js b/lib/charts/cover.js new file mode 100644 index 0000000..1cf9496 --- /dev/null +++ b/lib/charts/cover.js @@ -0,0 +1,36 @@ +/*global Raphael, d3, $, define, _ */ +/*! + * StreamLegend的兼容定义 + */ +;(function (name, definition) { + if (typeof define === 'function') { // Module + define(definition); + } else { // Assign to common namespaces or simply the global object (window) + this[name] = definition(function (id) { return this[id];}); + } +})('Cover', function (require) { + var DataV = require('DataV'); + //cover + var Cover = DataV.extend(DataV.Chart, { + initialize: function (container) { + var conf = this.defaults; + this.node = $(container); + this.node.css({ + "position": "absolute", + "left": 0, + "top": 0, + "width": conf.chartWidth, + "height": conf.chartHeight, + "zIndex": 100, + "visibility": "hidden" + }).bind("mousemove", $.proxy(function (e) { + this.mouse = {x: e.pageX, y: e.pageY}; + e.stopPropagation(); + }, this)).bind("mouseleave", $.proxy(function () { + this.mouse = undefined; + }, this)); + } + }); + + return Cover; +}); \ No newline at end of file diff --git a/lib/charts/hover_line.js b/lib/charts/hover_line.js new file mode 100644 index 0000000..d4b32f6 --- /dev/null +++ b/lib/charts/hover_line.js @@ -0,0 +1,69 @@ +/*global Raphael, d3, $, define, _ */ +/*! + * StreamLegend的兼容定义 + */ +;(function (name, definition) { + if (typeof define === 'function') { // Module + define(definition); + } else { // Assign to common namespaces or simply the global object (window) + this[name] = definition(function (id) { return this[id];}); + } +})('HoverLine', function (require) { + var DataV = require('DataV'); + var HoverLine = DataV.extend(DataV.Widget, { + initialize: function () { + } + }); + + HoverLine.prototype.render = function () { + this.clear(); + var paper = this.owner.paper; + var conf = this.owner.defaults; + this.indicatorLine = paper.path("M0 0V" + conf.chartHeight).attr({ + stroke: "none", + "stroke-width": 1, + "stroke-dasharray": "- " + }); + this.highlightLine = paper.path("M0 0V" + conf.chartHeight).attr({ + stroke: "none", + "stroke-width": 2 + }); + }; + HoverLine.prototype.hidden = function () { + this.indicatorLine.attr({"stroke": "none"}); + this.highlightLine.attr({"stroke": "none"}); + }; + HoverLine.prototype.show = function () { + this.indicatorLine.attr({"stroke": "#000"}); + this.highlightLine.attr({"stroke": "white"}); + }; + + HoverLine.prototype.refresh = function (xIdx, pathIndex) { + //refresh lines' position + var owner = this.owner; + var pathSource = owner.pathSource; + var lineX = owner.defaults.chartWidth * xIdx / (owner.source[0].length - 1); + var pathSourceCell = pathSource[pathSource.length - 1][xIdx]; + this.indicatorLine.attr({ + path: "M" + lineX + " " + (pathSourceCell.y0 - pathSourceCell.y) + "V" + pathSource[0][xIdx].y0 + }); + + pathSourceCell = pathSource[pathIndex][xIdx]; + this.highlightLine.attr({ + path: "M" + lineX + " " + (pathSourceCell.y0 - pathSourceCell.y) + "V" + pathSourceCell.y0 + }); + + if (pathIndex === 0 && owner.getDisplayRowInfo(pathIndex).rowIndex === -1) { + this.highlightLine.attr({"cursor": "pointer"}); + } else { + this.highlightLine.attr({"cursor": "auto"}); + } + }; + + HoverLine.prototype.clear = function () { + this.indicatorLine && this.indicatorLine.remove(); + this.highlightLine && this.highlightLine.remove(); + }; + + return HoverLine; +}); diff --git a/lib/charts/legend.js b/lib/charts/legend.js index edf3468..2d588ae 100644 --- a/lib/charts/legend.js +++ b/lib/charts/legend.js @@ -11,12 +11,37 @@ })('Legend', function (require) { var DataV = require('DataV'); - var Legend = DataV.extend(DataV.Widget, { + var Legend = DataV.extend(DataV.Chart, { initialize: function (container) { - this.legendIndent = 21; + this.legendIndent = 20; this.node = $(container); + /** + * 类型纬度 + */ + this.dimension.type = { + type: "string", + required: true, + index: 1 + }; + /** + * 时间纬度 + */ + this.dimension.x = { + type: "string", + required: true, + index: 0 + }; + /** + * 值纬度 + */ + this.dimension.value = { + type: "number", + required: true, + index: 2 + }; } }); + Legend.prototype.init = function () { var conf = this.owner.defaults; this.legend = $("
"); @@ -28,14 +53,18 @@ this.node.append(this.legend); }; + Legend.prototype.setSource = function (source, map) { + map = this.map(map); + this.list = _.keys(_.groupBy(source, map.type)); + }; + Legend.prototype.render = function () { var that = this; this.init(); this.clear(); this.legends = []; var owner = this.owner; - var colorFunc = owner.getColor(); - var colorArray = [], + var colorFunc = this.defaults.colorFunc, hoverIn = function (e) { var index = e.data.index; owner.fire('hoverIn', index); @@ -54,19 +83,14 @@ }); var ul = $("").css({ - "margin": "0px 0 0px 10px", - "padding-left": "0" + "margin": "0 0 0 10px", + "paddingLeft": 0 }); - var i, l; - for (i = 0, l = owner.displayData.allInfos.length; i < l; i++) { - colorArray.push(colorFunc(i)); - } - for (i = 0, l = owner.displayData.allInfos.length; i < l; i++) { - var color = colorArray[owner.displayData.rowIndex[i].slicedData]; - var li = $('
  • ' + owner.getDisplayRowInfo(i).rowName + '
  • '); + for (var i = 0, l = this.list.length; i < l && i < this.legendIndent; i++) { + var color = colorFunc(i); + var li = $('
  • ' + this.list[i] + '
  • '); + li.mouseenter({"index": i}, $.proxy(hoverIn, this)).mouseleave({"index": i}, $.proxy(hoverOut, this)); ul.append(li); - li.mouseenter({"index": i}, $.proxy(hoverIn, this)); - li.mouseleave({"index": i}, $.proxy(hoverOut, this)); this.legends.push(li); } diff --git a/lib/charts/navi.js b/lib/charts/navi.js index 54fcc39..841e1b0 100644 --- a/lib/charts/navi.js +++ b/lib/charts/navi.js @@ -9,11 +9,17 @@ this[name] = definition(function (id) { return this[id];}); } })('Navi', function (require) { - var Navi = function (owner, container) { - this.node = $(container); - this.owner = owner; + var DataV = require('DataV'); + + var Navi = DataV.extend(DataV.Chart, { + initialize: function (container) { + this.node = $(container); + } + }); + + Navi.prototype.init = function () { this.naviBackWidth = 80; - var conf = this.owner.defaults; + var conf = this.defaults; this.node.css({ "border-top": "1px solid #ddd", "border-bottom": "1px solid #ddd", @@ -52,9 +58,10 @@ that.owner.fire('changeLevel'); }); }; + Navi.prototype.render = function () { - var owner = this.owner; - var level = owner.defaults.moreConfig.level; + this.init(); + var level = this.defaults.moreConfig.level; this.clear(); for (var i = 0; i <= level; i++) { this.naviTrace.append($(" > ")); diff --git a/lib/charts/path_label.js b/lib/charts/path_label.js new file mode 100644 index 0000000..33d8c25 --- /dev/null +++ b/lib/charts/path_label.js @@ -0,0 +1,141 @@ +/*global Raphael, d3, $, define, _ */ +/*! + * PathLabel的兼容定义 + */ +;(function (name, definition) { + if (typeof define === 'function') { // Module + define(definition); + } else { // Assign to common namespaces or simply the global object (window) + this[name] = definition(function (id) { return this[id];}); + } +})('PathLabel', function (require) { + var DataV = require('DataV'); + //pathLabel + var PathLabel = DataV.extend(DataV.Chart, { + initialize: function (stream) { + this.stream = stream; + } + }); + + PathLabel.prototype.render = function () { + this.clear(); + var stream = this.stream; + var paths = stream.chart.paths; + var conf = stream.defaults; + var pathSource = stream.chart.pathSource; + var labels = []; + var getLabelLocation = function (locArray, el) { + var x = 0, + y = 0, + i; + var ratioMargin = 0.15; + var index = 0; + var max = 0; + var box = el.getBBox(); + var xInterval; + var minTop, maxBottom; + var showLabel = true; + var loc; + var height; + + xInterval = Math.ceil(box.width / (locArray[1].x - locArray[0].x) / 2); + if (xInterval === 0) { + xInterval = 1; + } + + locArray.forEach(function (d, i, array) { + var m = Math.max(ratioMargin * array.length, xInterval); + if (i >= m && i <= array.length - m) { + if (d.y > max) { + minTop = d.y0 - d.y; + maxBottom = d.y0; + max = d.y; + index = i; + } + } + }); + for (i = index - xInterval; i <= index + xInterval; i++) { + if (i < 0 || i >= locArray.length) { + height = 0; + showLabel = false; + break; + } + loc = locArray[i]; + //top's y is small + if (loc.y0 - loc.y > minTop) { + minTop = loc.y0 - loc.y; + } + if (loc.y0 < maxBottom) { + maxBottom = loc.y0; + } + } + + if (showLabel && maxBottom - minTop >= box.height * 0.8) { + x = locArray[index].x; + y = (minTop + maxBottom) / 2; + } else { + showLabel = false; + } + + return { + x: x, + y: y, + showLabel: showLabel + }; + }; + + stream.labels = labels; + var i, l, label, path; + for (i = 0, l = paths.length; i < l; i++) { + path = paths[i]; + label = stream.chart.paper.text(0, 0, + conf.pathLabel ? + stream.getDisplayRowInfo(i).rowName + " " + (Math.round(stream.getDisplayRowInfo(i).rowSum * 10000) / 100) + "%" : "") + .attr({ + "text-anchor": "middle", + "fill": "white", + "font-size": conf.fontSize, + "font-family": "微软雅黑" + }); + label.labelLoc = getLabelLocation(pathSource[i], label); + + if (label.labelLoc.showLabel) { + label.attr({ + "x": label.labelLoc.x, + "y": label.labelLoc.y + }); + } else { + label.attr({"opacity": 0}); + } + if (i === 0 && stream.getDisplayRowInfo(i).rowIndex === -1) { + path.attr({"cursor": "pointer"}); + label.attr({"cursor": "pointer"}); + } else { + path.attr({"cursor": "auto"}); + label.attr({"cursor": "auto"}); + } + labels.push(label); + } + }; + PathLabel.prototype.hidden = function () { + this.stream.labels.forEach(function (d) { + d.hide(); + }); + }; + PathLabel.prototype.show = function () { + this.stream.labels.forEach(function (d) { + if (d.labelLoc.showLabel) { + d.show(); + } + }); + }; + PathLabel.prototype.clear = function () { + var stream = this.stream; + if (stream.labels) { + stream.labels.forEach(function (d) { + d.remove(); + }); + } + }; + return PathLabel; +}); diff --git a/lib/charts/percentage.js b/lib/charts/percentage.js index 1b5be9a..acb47cc 100644 --- a/lib/charts/percentage.js +++ b/lib/charts/percentage.js @@ -11,52 +11,71 @@ })('Percentage', function (require) { var DataV = require('DataV'); - var Percentage = DataV.extend(DataV.Widget, { + var Percentage = DataV.extend(DataV.Chart, { initialize: function (container) { this.node = $(container); + this.limit = 20; + this.from = 0; + this.to = 0; + /** + * 类型纬度 + */ + this.dimension.type = { + type: "string", + required: true, + index: 1 + }; + /** + * 值纬度 + */ + this.dimension.value = { + type: "number", + required: true, + index: 2 + }; } }); Percentage.prototype.init = function () { - var owner = this.owner; - var conf = owner.defaults; + var conf = this.defaults; this.paper = new Raphael(this.node[0], conf.percentageWidth, conf.chartHeight); this.node.css({ - "width": conf.percentageWidth + "px", - "height": conf.chartHeight + "px", + "width": conf.percentageWidth, + "height": conf.chartHeight, "float": "left", "margin-bottom": "0px", "border-bottom": "0px", "padding-bottom": "0px" }); - this.statDataMaxY = d3.max(owner.statisticData.columnSum); + }; + + Percentage.prototype.setSource = function (source, map) { + map = this.map(map); + this.grouped = _.groupBy(source, map.type); + this.types = _.keys(this.grouped); + if (this.types.length > this.limit) { + this.to = this.limit; + } }; Percentage.prototype.render = function () { this.init(); - var owner = this.owner; - if (!owner.defaults.moreConfig.more) { - return; - } - var conf = owner.defaults; - var maxY = owner.chart.getMaxY() / this.statDataMaxY; - var y = maxY > 0.1 ? (1 - maxY) * conf.chartHeight + conf.fontSize * 2 / 3 - : (1 - maxY) * conf.chartHeight - conf.fontSize * 2 / 3; - + var conf = this.defaults; + var y = conf.fontSize * 2 / 3; if (!this.rect) {//init - this.rect = this.paper.rect(0, (1 - maxY) * conf.chartHeight, conf.percentageWidth, maxY * conf.chartHeight) + this.rect = this.paper.rect(0, 0, conf.percentageWidth, conf.chartHeight) .attr({ "fill": "#f4f4f4", "stroke": "#aaa", "stroke-width": 0.5 }); - this.text = this.paper.text(conf.percentageWidth / 2, y, Math.round(maxY * 100) + "%") + this.text = this.paper.text(conf.percentageWidth / 2, y, Math.round(100) + "%") .attr({"text-anchor": "middle"}); } - this.rect.animate({"y": (1 - maxY) * conf.chartHeight, "height": maxY * conf.chartHeight}, 750); - this.text.attr({ - "text": Math.round(maxY * 100) + "%" - }).animate({"y": y}, 750); + // this.rect.animate({"y": (1 - maxY) * conf.chartHeight, "height": maxY * conf.chartHeight}, 750); + // this.text.attr({ + // "text": Math.round(maxY * 100) + "%" + // }).animate({"y": y}, 300); }; return Percentage; diff --git a/lib/charts/stream.js.bak b/lib/charts/stream.js.bak new file mode 100755 index 0000000..bdb37e8 --- /dev/null +++ b/lib/charts/stream.js.bak @@ -0,0 +1,2113 @@ +/*global Raphael, d3, $, define, _ */ +/*! + * Stream的兼容定义 + */ +;(function (name, definition) { + if (typeof define === 'function') { // Module + define(definition); + } else { // Assign to common namespaces or simply the global object (window) + this[name] = definition(function (id) { return this[id];}); + } +})('Stream', function (require) { + var DataV = require('DataV'); + DataV.Axis = require('Axis'); + + var Widget = function () {}; + Widget.prototype.show = function () { + this.node.css('visibility', 'visible'); + return this; + }; + Widget.prototype.hidden = function () { + this.node.css('visibility', 'hidden'); + return this; + }; + + //streamChart + var StreamChart = DataV.extend(DataV.Chart, { + initialize: function (node, options) { + this.node = this.checkContainer(node); + this.defaults = {}; + this.defaults.width = 500; + this.defaults.height = 300; + this.defaults.offset = "expand";//zero, expand, silhou-ette, wiggle; + this.defaults.order = "default";//default, reverse, inside-out //in this Stream application, it will always be default, the real order is adjusted in Stream's data-process. + + this.defaults.animateDuration = 750; + this.defaults.animateOrder = undefined; + this.paths = undefined; + this.source = undefined; + this.layoutData = undefined; + this.pathSource = undefined; + this.setOptions(options); + this.createPaper(); + } + }); + + StreamChart.prototype.createPaper = function () { + var conf = this.defaults; + this.paper = new Raphael(this.node, conf.width, conf.height); + }; + + StreamChart.prototype.setSource = function (source) { + this.source = source; + this.layoutData = this.remapSource(source.slice()); + }; + + StreamChart.prototype.remapSource = function (data) { + var row = data.length; + var column = data[0].length; + var remap = []; + for (var i = 0; i < row; i++) { + remap[i] = []; + for (var j = 0; j < column; j++) { + remap[i][j] = {}; + remap[i][j].x = j; + remap[i][j].y = data[i][j]; + } + } + return remap; + }; + + StreamChart.prototype.layout = function () { + var conf = this.defaults; + d3.layout.stack().offset(conf.offset).order(conf.order)(this.layoutData); + }; + + StreamChart.prototype.generateChartElements = function () { + var conf = this.defaults; + var paper = this.paper, + paths = [], + area = this.generateArea(), + areaString, + colorFunc = this.getColor(), + color, + path; + + // set div's background instread; + paper.rect(0, 0, conf.width, conf.height).attr({ + "stroke": "none", + "fill": "#e0e0e0" + }); + + for (var i = 0, l = this.layoutData.length; i < l; i++) { + areaString = area(this.pathSource[i]); + color = colorFunc(i); + path = paper.path(areaString).attr({ + fill: color, + stroke: color, + "stroke-width": 1 + }); + paths[i] = path; + } + this.paths = paths; + }; + + StreamChart.prototype.render = function (animate) { + if (animate !== "animate") { + this.clear(); + this.layout(); + this.generateChartElements(); + } else { + this.layout(); + this.animate(); + } + }; + + StreamChart.prototype.animate = function () { + var time = 0, + area, + colorFunc, + color, + i, l, + _area, + paths = [], + order, + anim, + count = this.paths.length; + var that = this; + var animateCallback = function () { + count -= 1; + if (count > 0) { + return; + } + that.animateCallback(); + }; + if (typeof this.defaults.animateDuration !== 'undefined') { + time = this.defaults.animateDuration; + } + + // if paths have not been created + if (typeof this.paths === 'undefined') { + this.generateChartElements(); + } + + area = this.generateArea(); + colorFunc = this.getColor(); + if (typeof this.defaults.animateOrder !== 'undefined') { + order = this.defaults.animateOrder; + } else { + order = d3.range(this.pathSource.length); + } + for (i = 0, l = this.pathSource.length; i < l; i++) { + _area = area(this.pathSource[i]); + paths.push(_area); + } + for (i = 0, l = this.pathSource.length; i < l; i++) { + color = colorFunc(i); + anim = Raphael.animation({"path": paths[i]}, time, animateCallback); + this.paths[order[i]].animate(anim); + } + }; + + StreamChart.prototype.animateCallback = function () { + var newOrderPaths = []; + var that = this; + if (typeof this.defaults.animateOrder !== 'undefined') { + this.defaults.animateOrder.forEach(function (d, i) { + newOrderPaths[i] = that.paths[d]; + }); + this.paths = newOrderPaths; + } + }; + + StreamChart.prototype.clear = function () { + this.paper.clear(); + }; + + StreamChart.prototype.getColor = function () { + return d3.scale.category10(); + }; + + StreamChart.prototype.getMaxY = function () { + return d3.max(this.layoutData, function (d) { + return d3.max(d, function (d) { + return d.y0 + d.y; + }); + }); + }; + + StreamChart.prototype.mapPathSource = function () { + var conf = this.defaults, + maxX = this.layoutData[0].length - 1,//this.digitData[0].length - 1, + maxY = this.getMaxY(), + width = conf.width, + height = conf.height; + var i, j, l, l2, s, ps; + this.pathSource = []; + for (i = 0, l = this.layoutData.length; i < l; i++) { + this.pathSource[i] = []; + for (j = 0, l2 = this.layoutData[0].length; j < l2; j++) { + s = this.layoutData[i][j]; + ps = this.pathSource[i][j] = {}; + ps.x = s.x * width / maxX; + ps.y0 = height - s.y0 * height / maxY; + ps.y = s.y * height / maxY; + } + } + }; + + StreamChart.prototype.generateArea = function () { + this.mapPathSource(); + var area = d3.svg.area().x(function (d) { + return d.x; + }).y0(function (d) { + return d.y0; + }).y1(function (d) { + return d.y0 - d.y; + }); + return area; + }; + + var Legend = function (stream, container) { + this.stream = stream; + this.container = container; + this.legendIndent = 21; + var conf = stream.defaults; + this.legend = document.createElement("div"); + $(this.legend).css({ + "overflow": "hidden", + "padding": "10px 0 10px 0", + "width": conf.leftLegendWidth - this.legendIndent + "px" + }); + if (!conf.showLegend) { + $(this.legend).css({ + "visibility": "hidden", + "position": "absolute" + }); + } + this.container.appendChild(this.legend); + }; + + Legend.prototype.render = function () { + this.clear(); + var stream = this.stream, + legends = [], + li; + var colorFunc = stream.getColor(); + var colorArray = [], + color, + i, + l, + leftHeight, + legendHeight, + hoverIn = function (e) { + var index = e.data.index; + var stream = e.data.stream; + var path = stream.paths[index]; + stream.preIndex = index; + this.highlight(index); + path.attr({"opacity": 0.5}); + }, + hoverOut = function (e) { + var index = e.data.index; + var stream = e.data.stream; + var path = stream.paths[index]; + stream.preIndex = index; + this.lowlight(index); + path.attr({"opacity": 1.0}); + }; + + var ul = $("