From 0e32fc0d90fc1d843a06b8214784f00a7cda19f4 Mon Sep 17 00:00:00 2001 From: Masayuki Tanaka Date: Mon, 3 Feb 2014 18:21:14 +0900 Subject: [PATCH] Support multiple xs --- c3.js | 663 ++++++++++++++++++++++++++++++++++-------------------- c3.min.js | 141 ++++++------ 2 files changed, 487 insertions(+), 317 deletions(-) diff --git a/c3.js b/c3.js index 6955ab9..2cc64a3 100644 --- a/c3.js +++ b/c3.js @@ -48,7 +48,8 @@ // data - data configuration checkConfig('data', 'data is required in config'); - var __data_x = getConfig(['data', 'x'], undefined), + var __data_x = getConfig(['data', 'x'], null), + __data_xs = getConfig(['data', 'xs'], null), __data_x_format = getConfig(['data', 'x_format'], '%Y-%m-%d'), __data_id_converter = getConfig(['data', 'id_converter'], function (id) { return id; }), __data_names = getConfig(['data', 'names'], {}), @@ -153,14 +154,12 @@ var isTimeSeries = (__axis_x_type === 'timeseries'), isCategorized = (__axis_x_type === 'categorized'), - isCustomX = !isTimeSeries && __data_x; + isCustomX = !isTimeSeries && (__data_x || __data_xs); var dragStart = null, dragging = false, cancelClick = false; var legendHeight = __legend_show ? 40 : 0; - var parseDate = d3.time.format(__data_x_format).parse; - var color = generateColor(__data_colors, __color_pattern); var defaultTimeFormat = (function () { @@ -458,6 +457,16 @@ //-- Data --// + function isX(key) { + return (__data_x && key === __data_x) || (__data_xs && hasValue(__data_xs, key)); + } + function isNotX(key) { + return !isX(key); + } + function getXKey(id) { + return __data_x ? __data_x : __data_xs ? __data_xs[id] : null; + } + function addName(data) { var name = __data_names[data.id]; data.name = isDefined(name) ? name : data.id; @@ -489,45 +498,49 @@ return new_rows; } function convertDataToTargets(data) { - var ids = d3.keys(data[0]).filter(function (key) { return key !== __data_x; }); - var targets, index, parsedDate; + var ids = d3.keys(data[0]).filter(isNotX), xs = d3.keys(data[0]).filter(isX), targets; - // check __data_x is defined if timeseries - if (isTimeSeries && ! __data_x) { - window.alert('data.x must be specified when axis.x.type == "timeseries"'); + // check "x" is defined if timeseries + if (isTimeSeries && xs.length === 0) { + window.alert('data.x or data.xs must be specified when axis.x.type == "timeseries"'); return []; } - if (isCustomX && isUndefined(c3.data.x)) { - c3.data.x = data.map(function (d) { return d[__data_x]; }); + // save x for update data by load + if (isCustomX) { + ids.forEach(function (id) { + var xKey = getXKey(id); + if (xs.indexOf(xKey) >= 0) { + c3.data.x[id] = data.map(function (d) { return d[xKey]; }); + } + }); } - index = 0; - data.forEach(function (d) { - if (isTimeSeries) { - if (!(__data_x in d)) { throw Error("'" + __data_x + "' must be included in data"); } - parsedDate = parseDate(d[__data_x]); - if (parsedDate === null) { throw Error("Failed to parse timeseries date in data"); } - d.x = parsedDate; - } - else if (isCustomX) { - d.x = isDefined(d[__data_x]) ? d[__data_x] : c3.data.x[index]; - } - else { - d.x = index; - } - if (firstDate === null) { firstDate = new Date(d.x); } - lastDate = new Date(d.x); - index++; - }); - + // convert to target targets = ids.map(function (id) { var convertedId = __data_id_converter(id); return { id: convertedId, id_org: id, - values: data.map(function (d) { - return {x: d.x, value: d[id] !== null && !isNaN(d[id]) ? +d[id] : null, id: convertedId}; + values: data.map(function (d, i) { + var x, xKey = getXKey(id); + + if (isTimeSeries) { + x = parseDate(d[xKey]); + } + else if (isCustomX) { + x = d[xKey] ? d[xKey] : c3.data.x[id][i]; + } + else { + x = i; + } + + if (x < firstX || firstX === null) { firstX = x; } + if (lastX < x) { lastX = x; } + + d.x = x; // used by event-rect + + return {x: x, value: d[id] !== null && !isNaN(d[id]) ? +d[id] : null, id: convertedId, index: i}; }) }; }); @@ -595,10 +608,84 @@ return y(d.value); } - //-- Circle --/ + function findClosestOfValues(values, pos, _min, _max) { // MEMO: values must be sorted by x + var min = _min ? _min : 0, + max = _max ? _max : values.length - 1, + med = Math.floor((max - min) / 2) + min, + value = values[med], + diff = x(value.x) - pos[0], + minDist, maxDist; + + // Update rage for search + diff > 0 ? max = med : min = med; + + // if candidates are two closest min and max, stop recursive call + if ((max - min) === 1) { + if (! values[min].x) { return values[max]; } + if (! values[max].x) { return values[min]; } + minDist = Math.pow(pos[0] - x(values[min].x), 2) + Math.pow(pos[1] - y(values[min].value), 2); + maxDist = Math.pow(pos[0] - x(values[max].x), 2) + Math.pow(pos[1] - y(values[max].value), 2); + return minDist < maxDist ? values[min] : values[max]; + } + + return findClosestOfValues(values, pos, min, max); + } + function findClosest(targets, mouse) { + var closest, closests, minDist; + + // map to array of closest points of each target + closests = targets.map(function (target) { + return findClosestOfValues(target.values, mouse); + }); + + // decide closest point + closests.forEach(function (c) { + var dist = Math.pow(x(c.x) - mouse[0], 2) + Math.pow(y(c.value) - mouse[1], 2); + if (dist < minDist || ! minDist) { + minDist = dist; + closest = c; + } + }); + + // TODO: multiple closests when each is very close + + return closest; + } + + //-- Tooltip --// + + function showTooltip(selectedData, mouse) { + // Construct tooltip + tooltip.html(__tooltip_contents(selectedData)) + .style("visibility", "hidden") + .style("display", "block"); + // Get tooltip dimensions + var tWidth = tooltip.property('offsetWidth'), + tHeight = tooltip.property('offsetHeight'); + // Set tooltip + // todo get rid of magic numbers + tooltip + .style("top", (mouse[1] + 15 + tHeight < getCurrentHeight() ? mouse[1] + 15 : mouse[1] - tHeight) + "px") + .style("left", ((__axis_rotated ? + mouse[0] : + (x(selectedData[0].x) + 60 + tWidth < getCurrentWidth()) ? + (x(selectedData[0].x) + 60) + "px" : (x(selectedData[0].x) - tWidth + 30) + "px" + ))) + .style("visibility", "visible"); + } + + function showXGridFocus(data) { + main.selectAll('line.xgrid-focus') + .style("visibility", "visible") + .data([data]) + .attr(__axis_rotated ? 'y1' : 'x1', xx) + .attr(__axis_rotated ? 'y2' : 'x2', xx); + } + + //-- Circle --// function circleX(d) { - return x(d.x); + return d.x || d.x === 0 ? x(d.x) : null; } function circleY(d) { return getYScale(d.id)(d.value); @@ -724,6 +811,16 @@ }; } + //-- Date --// + + function parseDate(date) { + var parsedDate; + if (!date) { throw Error(date + " can not be parsed as d3.time with format " + __data_x_format + ". Maybe 'x' of this data is not defined. See data.x or data.xs option."); } + parsedDate = d3.time.format(__data_x_format).parse(date); + if (!parsedDate) { throw Error("Failed to parse '" + date + "' with format " + __data_x_format); } + return parsedDate; + } + //-- Util --// function isWithinCircle(_this, _r) { @@ -745,6 +842,18 @@ return false; } + function hasValue(dict, value) { + var found = false; + Object.keys(dict).forEach(function (key) { + if (dict[key] === value) { found = true; } + }); + return found; + } + + function dist(data, mouse) { + return Math.pow(x(data.x) - mouse[0], 2) + Math.pow(y(data.value) - mouse[1], 2); + } + //-- Selection --// function selectPoint(target, d, i) { @@ -910,10 +1019,10 @@ var svg, defs, main, context, legend, tooltip, selectChart; // for brush area culculation - var firstDate = null, lastDate = null, orgXDomain; + var firstX = null, lastX = null, orgXDomain; function init(data) { - var grid, xgridLine; + var eventRect, grid, xgridLine; var i; selectChart = d3.select(__bindto); @@ -924,7 +1033,7 @@ selectChart.html(""); } - c3.data.x = undefined; + c3.data.x = {}; c3.data.targets = convertDataToTargets(data); // TODO: set names if names not specified @@ -934,7 +1043,7 @@ updateScales(); // Set domains for each scale - x.domain(d3.extent(data.map(function (d) { return d.x; }))); + x.domain(d3.extent([firstX, lastX])); y.domain(getYDomain('y')); y2.domain(getYDomain('y2')); subX.domain(x.domain()); @@ -1091,10 +1200,105 @@ .attr('class', 'chart'); // Cover whole with rects for events - main.select('.chart').append("g") + eventRect = main.select('.chart').append("g") .attr("class", "event-rects") .style('fill-opacity', 0) - .style('cursor', __zoom_enabled ? 'ew-resize' : null) + .style('cursor', __zoom_enabled ? 'ew-resize' : null); + + // Generate rect for event handling + __data_xs ? generateEventRectsForMultipleXs(eventRect) : generateEventRectsForSingleX(eventRect, data); + + // Define g for bar chart area + main.select(".chart").append("g") + .attr("class", "chart-bars"); + + // Define g for line chart area + main.select(".chart").append("g") + .attr("class", "chart-lines"); + + if (__zoom_enabled) { // TODO: __zoom_privileged here? + // if zoom privileged, insert rect to forefront + main.insert('rect', __zoom_privileged ? null : 'g.grid') + .attr('class', 'zoom-rect') + .attr('width', width) + .attr('height', height) + .style('opacity', 0) + .style('cursor', 'ew-resize') + .call(zoom).on("dblclick.zoom", null); + } + + // Set default extent if defined + if (__axis_x_default !== null) { + brush.extent(typeof __axis_x_default !== 'function' ? __axis_x_default : __axis_x_default(firstX, lastX)); + } + + /*-- Context Region --*/ + + if (__subchart_show) { + // Define g for chart area + context.append('g') + .attr("clip-path", clipPath) + .attr('class', 'chart'); + + // Define g for bar chart area + context.select(".chart").append("g") + .attr("class", "chart-bars"); + + // Define g for line chart area + context.select(".chart").append("g") + .attr("class", "chart-lines"); + + // Add extent rect for Brush + context.append("g") + .attr("clip-path", clipPath) + .attr("class", "x brush") + .call(brush) + .selectAll("rect") + .attr("height", height2); + + // ATTENTION: This must be called AFTER chart added + // Add Axis + context.append("g") + .attr("class", "x axis") + .attr("transform", translate.subx) + .call(subXAxis); + } + + /*-- Legend Region --*/ + + if (__legend_show) { updateLegend(c3.data.targets); } + + // Set targets + updateTargets(c3.data.targets); + + // Draw with targets + redraw({withTransition: false, withUpdateXDomain: true}); + + // Show tooltip if needed + if (__tooltip_init_show) { + if (isTimeSeries && typeof __tooltip_init_x === 'string') { + __tooltip_init_x = parseDate(__tooltip_init_x); + for (i = 0; i < c3.data.targets[0].values.length; i++) { + if ((c3.data.targets[0].values[i].x - __tooltip_init_x) === 0) { break; } + } + __tooltip_init_x = i; + } + tooltip.html(__tooltip_contents(c3.data.targets.map(function (d) { + return addName(d.values[__tooltip_init_x]); + }))); + tooltip.style("top", __tooltip_init_position.top) + .style("left", __tooltip_init_position.left) + .style("display", "block"); + } + + // Bind resize event + if (window.onresize == null) { + window.onresize = resize; + } + } + + function generateEventRectsForSingleX(eventRect, data) { + eventRect .selectAll(".event-rects") .data(data) .enter().append("rect") @@ -1133,11 +1337,7 @@ .classed(EXPANDED, true); // Show xgrid focus line - main.selectAll('line.xgrid-focus') - .style("visibility", "visible") - .data([selectedData[0]]) - .attr(__axis_rotated ? 'y1' : 'x1', xx) - .attr(__axis_rotated ? 'y2' : 'x2', xx); + showXGridFocus(selectedData[0]); }) .on('mouseout', function (d, i) { main.select('line.xgrid-focus').style("visibility", "hidden"); @@ -1152,29 +1352,17 @@ .classed(EXPANDED, false); }) .on('mousemove', function (d, i) { - var selectedData = c3.data.targets.map(function (d) { + var selectedData; + + if (dragging) { return; } // do nothing when dragging + + // Show tooltip + selectedData = c3.data.targets.map(function (d) { return addName(d.values[i]); }); + showTooltip(selectedData, d3.mouse(this)); - // Construct tooltip - tooltip.html(__tooltip_contents(selectedData)) - .style("visibility", "hidden") - .style("display", "block"); - // Get tooltip dimensions - var tWidth = tooltip.property('offsetWidth'), - tHeight = tooltip.property('offsetHeight'); - // Set tooltip - // todo get rid of magic numbers - tooltip - .style("top", (d3.mouse(this)[1] + 15 + tHeight < getCurrentHeight() ? d3.mouse(this)[1] + 15 : d3.mouse(this)[1] - tHeight) + "px") - .style("left", ((__axis_rotated ? - d3.mouse(this)[0] : - (x(selectedData[0].x) + 60 + tWidth < getCurrentWidth()) ? - (x(selectedData[0].x) + 60) + "px" : (x(selectedData[0].x) - tWidth + 30) + "px" - ))) - .style("visibility", "visible"); - - if (! __data_selection_enabled || dragging) { return; } + if (! __data_selection_enabled) { return; } if (__data_selection_grouped) { return; } // nothing to do when grouped main.selectAll('.-shape-' + i) @@ -1207,183 +1395,160 @@ cancelClick = false; return; } - main.selectAll('.-shape-' + i).each(function (d) { - var _this = d3.select(this), - isSelected = _this.classed(SELECTED); - var isWithin = false, toggle; - if (this.nodeName === 'circle') { - isWithin = isWithinCircle(this, __point_select_r * 1.5); - toggle = togglePoint; - } - else if (this.nodeName === 'rect') { - isWithin = isWithinBar(this); - toggle = toggleBar; - } - if (__data_selection_grouped || isWithin) { - if (__data_selection_enabled && __data_selection_isselectable(d)) { - _this.classed(SELECTED, !isSelected); - toggle(!isSelected, _this, d, i); - } - __point_onclick(d, _this); // TODO: should be __data_onclick - } - }); + main.selectAll('.-shape-' + i).each(function (d) { selectShape(this, d, i); }); }) .call( - d3.behavior.drag().origin(Object).on('drag', function () { - if (! __data_selection_enabled) { return; } // do nothing if not selectable - if (__zoom_enabled && ! zoom.altDomain) { return; } // skip if zoomable because of conflict drag dehavior - - var sx = dragStart[0], sy = dragStart[1], - mouse = d3.mouse(this), - mx = mouse[0], - my = mouse[1], - minX = Math.min(sx, mx), - maxX = Math.max(sx, mx), - minY = (__data_selection_grouped) ? margin.top : Math.min(sy, my), - maxY = (__data_selection_grouped) ? height : Math.max(sy, my); - main.select('.dragarea') - .attr('x', minX) - .attr('y', minY) - .attr('width', maxX - minX) - .attr('height', maxY - minY); - main.selectAll('.-shapes').selectAll('.-shape') - .filter(function (d) { return __data_selection_isselectable(d); }) - .each(function (d, i) { - var _this = d3.select(this), - isSelected = _this.classed(SELECTED), - isIncluded = _this.classed(INCLUDED), - _x, _y, _w, toggle, isWithin = false; - if (this.nodeName === 'circle') { - _x = _this.attr("cx") * 1; - _y = _this.attr("cy") * 1; - toggle = togglePoint; - isWithin = minX < _x && _x < maxX && minY < _y && _y < maxY; - } - else if (this.nodeName === 'rect') { - _x = _this.attr("x") * 1; - _y = _this.attr("y") * 1; - _w = _this.attr('width') * 1; - toggle = toggleBar; - isWithin = minX < _x + _w && _x < maxX && _y < maxY; - } - if (isWithin ^ isIncluded) { - _this.classed(INCLUDED, !isIncluded); - // TODO: included/unincluded callback here - _this.classed(SELECTED, !isSelected); - toggle(!isSelected, _this, d, i); - } - }); - }) - .on('dragstart', function () { - if (! __data_selection_enabled) { return; } // do nothing if not selectable - dragStart = d3.mouse(this); - main.select('.chart').append('rect') - .attr('class', 'dragarea') - .style('opacity', 0.1); - dragging = true; - // TODO: add callback here - }) - .on('dragend', function () { - if (! __data_selection_enabled) { return; } // do nothing if not selectable - main.select('.dragarea') - .transition().duration(100) - .style('opacity', 0) - .remove(); - main.selectAll('.-shape') - .classed(INCLUDED, false); - dragging = false; - // TODO: add callback here - }) + d3.behavior.drag().origin(Object) + .on('drag', function () { drag(d3.mouse(this)); }) + .on('dragstart', function () { dragstart(d3.mouse(this)); }) + .on('dragend', function () { dragend(); }) ) .call(zoom).on("dblclick.zoom", null); + } - // Define g for bar chart area - main.select(".chart").append("g") - .attr("class", "chart-bars"); - - // Define g for line chart area - main.select(".chart").append("g") - .attr("class", "chart-lines"); - - if (__zoom_enabled) { - // if zoom privileged, insert rect to forefront - main.insert('rect', __zoom_privileged ? null : 'g.grid') - .attr('class', 'zoom-rect') - .attr('width', width) - .attr('height', height) - .style('opacity', 0) - .style('cursor', 'ew-resize') - .call(zoom).on("dblclick.zoom", null); - } - - // Set default extent if defined - if (__axis_x_default !== null) { - brush.extent(typeof __axis_x_default !== 'function' ? __axis_x_default : (isTimeSeries ? __axis_x_default(firstDate, lastDate) : __axis_x_default(0, maxDataCount() - 1))); - } - - /*-- Context Region --*/ + function generateEventRectsForMultipleXs(eventRect) { + eventRect.append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', width) + .attr('height', height) + .attr('class', "event-rect") + .on('mousemove', function () { + var mouse = d3.mouse(this), + closest = findClosest(c3.data.targets, mouse); - if (__subchart_show) { - // Define g for chart area - context.append('g') - .attr("clip-path", clipPath) - .attr('class', 'chart'); + // show tooltip when cursor is close to some point + var selectedData = [addName(closest)]; + showTooltip(selectedData, mouse); - // Define g for bar chart area - context.select(".chart").append("g") - .attr("class", "chart-bars"); + // expand points + if (__point_focus_expand_enabled) { + main.selectAll('.-circle') + .filter(function () { return d3.select(this).classed(EXPANDED); }) + .classed(EXPANDED, false) + .attr('r', __point_r); + main.select('.-circles-' + closest.id).select('.-circle-' + closest.index) + .classed(EXPANDED, true) + .attr('r', __point_focus_expand_r); + } - // Define g for line chart area - context.select(".chart").append("g") - .attr("class", "chart-lines"); + // Show xgrid focus line + showXGridFocus(selectedData[0]); - // Add extent rect for Brush - context.append("g") - .attr("clip-path", clipPath) - .attr("class", "x brush") - .call(brush) - .selectAll("rect") - .attr("height", height2); + // Show cursor as pointer if point is close to mouse position + if (dist(closest, mouse) < 100) { + d3.select('.event-rect').style('cursor', 'pointer'); + } else { + d3.select('.event-rect').style('cursor', null); + } + }) + .on('click', function () { + var mouse = d3.mouse(this), + closest = findClosest(c3.data.targets, mouse); + + // select if selection enabled + if (dist(closest, mouse) < 100) { + main.select('.-circles-' + closest.id).select('.-circle-' + closest.index).each(function () { + selectShape(this, closest, closest.index); + }); + } + }) + .call( + d3.behavior.drag().origin(Object) + .on('drag', function () { drag(d3.mouse(this)); }) + .on('dragstart', function () { dragstart(d3.mouse(this)); }) + .on('dragend', function () { dragend(); }) + ) + .call(zoom).on("dblclick.zoom", null); + } - // ATTENTION: This must be called AFTER chart added - // Add Axis - context.append("g") - .attr("class", "x axis") - .attr("transform", translate.subx) - .call(subXAxis); + function selectShape(target, d, i) { + var _this = d3.select(target), + isSelected = _this.classed(SELECTED); + var isWithin = false, toggle; + if (target.nodeName === 'circle') { + isWithin = isWithinCircle(target, __point_select_r * 1.5); + toggle = togglePoint; } - - /*-- Legend Region --*/ - - if (__legend_show) { updateLegend(c3.data.targets); } - - // Set targets - updateTargets(c3.data.targets); - - // Draw with targets - redraw({withTransition: false, withUpdateXDomain: true}); - - // Show tooltip if needed - if (__tooltip_init_show) { - if (isTimeSeries && typeof __tooltip_init_x === 'string') { - __tooltip_init_x = parseDate(__tooltip_init_x); - for (i = 0; i < c3.data.targets[0].values.length; i++) { - if ((c3.data.targets[0].values[i].x - __tooltip_init_x) === 0) { break; } - } - __tooltip_init_x = i; + else if (target.nodeName === 'rect') { + isWithin = isWithinBar(target); + toggle = toggleBar; + } + if (__data_selection_grouped || isWithin) { + if (__data_selection_enabled && __data_selection_isselectable(d)) { + _this.classed(SELECTED, !isSelected); + toggle(!isSelected, _this, d, i); } - tooltip.html(__tooltip_contents(c3.data.targets.map(function (d) { - return addName(d.values[__tooltip_init_x]); - }))); - tooltip.style("top", __tooltip_init_position.top) - .style("left", __tooltip_init_position.left) - .style("display", "block"); + __point_onclick(d, _this); // TODO: should be __data_onclick } + } - // Bind resize event - if (window.onresize == null) { - window.onresize = resize; - } + function drag(mouse) { + if (! __data_selection_enabled) { return; } // do nothing if not selectable + if (__zoom_enabled && ! zoom.altDomain) { return; } // skip if zoomable because of conflict drag dehavior + + var sx = dragStart[0], sy = dragStart[1], + mx = mouse[0], + my = mouse[1], + minX = Math.min(sx, mx), + maxX = Math.max(sx, mx), + minY = (__data_selection_grouped) ? margin.top : Math.min(sy, my), + maxY = (__data_selection_grouped) ? height : Math.max(sy, my); + main.select('.dragarea') + .attr('x', minX) + .attr('y', minY) + .attr('width', maxX - minX) + .attr('height', maxY - minY); + main.selectAll('.-shapes').selectAll('.-shape') + .filter(function (d) { return __data_selection_isselectable(d); }) + .each(function (d, i) { + var _this = d3.select(this), + isSelected = _this.classed(SELECTED), + isIncluded = _this.classed(INCLUDED), + _x, _y, _w, toggle, isWithin = false; + if (this.nodeName === 'circle') { + _x = _this.attr("cx") * 1; + _y = _this.attr("cy") * 1; + toggle = togglePoint; + isWithin = minX < _x && _x < maxX && minY < _y && _y < maxY; + } + else if (this.nodeName === 'rect') { + _x = _this.attr("x") * 1; + _y = _this.attr("y") * 1; + _w = _this.attr('width') * 1; + toggle = toggleBar; + isWithin = minX < _x + _w && _x < maxX && _y < maxY; + } + if (isWithin ^ isIncluded) { + _this.classed(INCLUDED, !isIncluded); + // TODO: included/unincluded callback here + _this.classed(SELECTED, !isSelected); + toggle(!isSelected, _this, d, i); + } + }); + } + + function dragstart(mouse) { + if (! __data_selection_enabled) { return; } // do nothing if not selectable + dragStart = mouse; + main.select('.chart').append('rect') + .attr('class', 'dragarea') + .style('opacity', 0.1); + dragging = true; + // TODO: add callback here + } + + function dragend() { + if (! __data_selection_enabled) { return; } // do nothing if not selectable + main.select('.dragarea') + .transition().duration(100) + .style('opacity', 0) + .remove(); + main.selectAll('.-shape') + .classed(INCLUDED, false); + dragging = false; + // TODO: add callback here + } function redraw(options) { @@ -1432,8 +1597,8 @@ if (__grid_x_show) { if (__grid_x_type === 'year') { xgridData = []; - var firstYear = firstDate.getFullYear(); - var lastYear = lastDate.getFullYear(); + var firstYear = firstX.getFullYear(); + var lastYear = lastX.getFullYear(); for (var year = firstYear; year <= lastYear; year++) { xgridData.push(new Date(year + '-01-01 00:00:00')); } @@ -1574,24 +1739,26 @@ .attr("cy", __axis_rotated ? circleX : circleY); // rect for mouseover - if (isCustomX) { - rectW = function (d, i) { - var prevX = getPrevX(i), nextX = getNextX(i); - return (x(nextX ? nextX : d.x + 50) - x(prevX ? prevX : d.x - 50)) / 2; - }; - rectX = function (d, i) { - var prevX = getPrevX(i); - return (x(d.x) + x(prevX ? prevX : d.x - 50)) / 2; - }; - } else { - rectW = (((__axis_rotated ? height : width) * getXDomainRatio()) / (maxDataCount() - 1)); - rectX = function (d) { return x(d.x) - (rectW / 2); }; + if (! __data_xs) { + if (isCustomX) { + rectW = function (d, i) { + var prevX = getPrevX(i), nextX = getNextX(i); + return (x(nextX ? nextX : d.x + 50) - x(prevX ? prevX : d.x - 50)) / 2; + }; + rectX = function (d, i) { + var prevX = getPrevX(i); + return (x(d.x) + x(prevX ? prevX : d.x - 50)) / 2; + }; + } else { + rectW = (((__axis_rotated ? height : width) * getXDomainRatio()) / (maxDataCount() - 1)); + rectX = function (d) { return x(d.x) - (rectW / 2); }; + } + main.selectAll('.event-rect') + .attr("x", __axis_rotated ? 0 : rectX) + .attr("y", __axis_rotated ? rectX : 0) + .attr("width", __axis_rotated ? width : rectW) + .attr("height", __axis_rotated ? rectW : height); } - main.selectAll('.event-rect') - .attr("x", __axis_rotated ? 0 : rectX) - .attr("y", __axis_rotated ? rectX : 0) - .attr("width", __axis_rotated ? width : rectW) - .attr("height", __axis_rotated ? rectW : height); // rect for regions var mainRegion = main.select('.regions').selectAll('rect.region') @@ -2042,7 +2209,7 @@ c3.destroy = function () { c3.data.targets = undefined; - c3.data.x = undefined; + c3.data.x = {}; selectChart.html(""); window.onresize = null; }; diff --git a/c3.min.js b/c3.min.js index 1a08aaf..f5d943d 100644 --- a/c3.min.js +++ b/c3.min.js @@ -1,69 +1,72 @@ -(function(O){function xc(){function n(e,c){e.attr("transform",function(e){return"translate("+(c(e)+Z)+", 0)"})}function r(e,c){e.attr("transform",function(e){return"translate(0,"+c(e)+")"})}function O(e){var c=e[0];e=e[e.length-1];return ca?0:a}function nb(a){var b=Ia[a.id];a.name=n(b)?b:a.id;return a}function Wb(a){var b=a[0],d={},k=[],e,c;for(e=1;ew[d].indexOf(e.id)))for(k=0;k=g;g+=s)u+=e(a[c-1],a[c],g,v)}return u} -function wb(a){var b,d;db=e.select(ua);if(db.empty())O.alert('No bind element found. Check the selector specified by "bindto" and existance of that element. Default "bindto" is "#chart".');else{db.html("");g.data.x=void 0;g.data.targets=Ja(a);sa();Eb();l.domain(e.extent(a.map(function(a){return a.x})));y.domain(ka("y"));T.domain(ka("y2"));ba.domain(l.domain());Wa.domain(y.domain());Xa.domain(T.domain());B.ticks(10>a.length?a.length:10);na.ticks(Zc).outerTickSize(0).tickFormat($c);Ya.ticks(ad).outerTickSize(0).tickFormat(bd); -U=l.domain();G.x(ba);pa&&H.x(l);Ma=e.select(ua).append("svg").attr("width",p+A+aa).attr("height",t+s+F).on("mouseenter",cd).on("mouseleave",dd);eb=Ma.append("defs");eb.append("clipPath").attr("id",xb).append("rect").attr("y",s).attr("width",p).attr("height",t-s);eb.append("clipPath").attr("id","xaxis-clip").append("rect").attr("x",-1-A).attr("y",-20).attr("width",Q).attr("height",P);eb.append("clipPath").attr("id","yaxis-clip").append("rect").attr("x",-A+1).attr("y",s-1).attr("width",ja).attr("height", -hb);h=Ma.append("g").attr("transform",K.main);x=wa?Ma.append("g").attr("transform",K.context):null;xa=ya?Ma.append("g").attr("transform",K.legend):null;X=e.select(ua).style("position","relative").append("div").style("position","absolute").style("z-index","10").style("display","none");h.append("g").attr("class","x axis").attr("clip-path",f?"":"url(#xaxis-clip)").attr("transform",K.x).call(f?na:B).append("text").attr("class","-axis-x-label").attr("x",p).attr("dy","-.5em").style("text-anchor","end").text(ed); -h.append("g").attr("class","y axis").attr("clip-path",f?"url(#yaxis-clip)":"").call(f?B:na).append("text").attr("transform","rotate(-90)").attr("dy","1.2em").attr("dx","-.5em").style("text-anchor","end").text(fd);Mb&&h.append("g").attr("class","y2 axis").attr("transform",K.y2).call(Ya);b=h.append("g").attr("clip-path",Na).attr("class","grid");nc&&b.append("g").attr("class","xgrids");yb&&(d=b.append("g").attr("class","xgrid-lines").selectAll(".xgrid-line").data(yb).enter().append("g").attr("class", -"xgrid-line"),d.append("line").attr("class",function(a){return""+a["class"]}),d.append("text").attr("class",function(a){return""+a["class"]}).attr("text-anchor","end").attr("transform",f?"":"rotate(-90)").attr("dx",f?0:-s).attr("dy",-6).text(function(a){return a.text}));gd&&b.append("g").attr("class","xgrid-focus").append("line").attr("class","xgrid-focus").attr("x1",f?0:-10).attr("x2",f?p:-10).attr("y1",f?-10:s).attr("y2",f?-10:t);oc&&b.append("g").attr("class","ygrids");zb&&b.append("g").attr("class", -"ygrid-lines").selectAll("ygrid-line").data(zb).enter().append("line").attr("class",function(a){return"ygrid-line "+a["class"]});h.append("g").attr("clip-path",Na).attr("class","regions");h.append("g").attr("clip-path",Na).attr("class","chart");h.select(".chart").append("g").attr("class","event-rects").style("fill-opacity",0).style("cursor",pa?"ew-resize":null).selectAll(".event-rects").data(a).enter().append("rect").attr("class",function(a,b){return"event-rect event-rect-"+b}).style("cursor",ga&& -qa?"pointer":null).on("mouseover",function(a,b){if(!fb){var d=g.data.targets.map(function(a){return nb(a.values[b])}),e,c;if(0",d,c,e;for(d=0;d"+e+""+c+"";return b+""}),jd=c(["tooltip","init","show"],!1),za=c(["tooltip","init","x"],0),tc=c(["tooltip","init","position"],{top:"0px",left:"50px"}),xb=ua.replace("#","")+"-clip",Na="url(#"+xb+")",z="timeseries"===wc,ca="categorized"===wc,ob=!z&&V,Cb=null,fb=!1,Bb=!1,la=ya?40: -0,oa=e.time.format(qd).parse,W=function(a,b){var d=[],c=null!==b?b:"#1f77b4 #ff7f0e #2ca02c #d62728 #9467bd #8c564b #e377c2 #7f7f7f #bcbd22 #17becf".split(" ");return function(b){if(b in a)return a[b];-1===d.indexOf(b)&&d.push(b);return c[d.indexOf(b)%c.length]}}(rd,sd),Fc=function(){var a=[[e.time.format("%Y/%-m/%-d"),function(){return!0}],[e.time.format("%-m/%-d"),function(a){return a.getMonth()}],[e.time.format("%-m/%-d"),function(a){return 1!==a.getDate()}],[e.time.format("%-m/%-d"),function(a){return a.getDay()&& -1!==a.getDate()}],[e.time.format("%I %p"),function(a){return a.getHours()}],[e.time.format("%I:%M"),function(a){return a.getMinutes()}],[e.time.format(":%S"),function(a){return a.getSeconds()}],[e.time.format(".%L"),function(a){return a.getMilliseconds()}]];return function(b){for(var d=a.length-1,c=a[d];!c[1](b);)c=a[--d];return c[0](b)}}(),Ib,Pb,Kb,Va,Ob,Qb,p,t,ma,ib,ta,Sb,Tb,lb,mb,l,y,T,ba,Wa,Xa,B,na,Ya,Da,Ac=f?"left":"bottom",Bc=f?kb?"top":"bottom":kb?"right":"left",Cc=f?jb?"bottom":"top":jb?"left": -"right",Dc="bottom",K={main:function(){return"translate("+A+","+s+")"},context:function(){return"translate("+m+","+Aa+")"},legend:function(){return"translate("+Gb+","+Fb+")"},y2:function(){return"translate("+(f?0:p)+","+(f?10:0)+")"},x:function(){return"translate(0,"+t+")"},subx:function(){return"translate(0,"+ma+")"}},md=function(){var a=e.svg.line().x(f?function(a){return E(a.id)(a.value)}:cb).y(f?cb:function(a){return E(a.id)(a.value)});return function(b){var d=lc(b.values),c;if(ub(b))return"spline"=== -fa["string"===typeof b?b:b.id]?a.interpolate("cardinal"):a.interpolate("linear"),vc[b.id]?Yc(d,l,E(b.id),vc[b.id]):a(d);c=l(d[0].x);b=E(b.id)(d[0].value);return f?"M "+b+" "+c:"M "+c+" "+b}}(),nd=function(){var a=e.svg.line().x(function(a){return ba(a.x)}).y(function(a){return R(a.id)(a.value)});return function(b){var d=lc(b.values);return ub(b)?a(d):"M "+ba(d[0].x)+" "+R(b.id)(d[0].value)}}(),G=e.svg.brush().on("brush",function(){C({withTransition:!1,withY:!1,withSubchart:!1,withUpdateXDomain:!0})}), -H=e.behavior.zoom().on("zoomstart",function(){H.altDomain=e.event.sourceEvent.altKey?l.orgDomain():null}).on("zoom",pa?od:null);G.update=function(){x&&x.select(".x.brush").call(this);return this};H.orgScaleExtent=function(){var a=uc?uc:[1,10];return[a[0],Math.max(va()/a[1],a[1])]};H.updateScaleExtent=function(){var a=l.orgDomain(),a=(a[1]-a[0])/(U[1]-U[0]),b=this.orgScaleExtent();this.scaleExtent([b[0]*a,b[1]*a]);return this};var Ma,eb,h,x,xa,X,db,bb=null,pb=null,U;g.focus=function(a){g.defocus(); -e.selectAll(Ra(a)).filter(function(a){return rb(a.id)}).classed("focused",!0).transition().duration(100).style("opacity",1)};g.defocus=function(a){e.selectAll(Ra(a)).filter(function(a){return rb(a.id)}).classed("focused",!1).transition().duration(100).style("opacity",0.3)};g.revert=function(a){e.selectAll(Ra(a)).filter(function(a){return rb(a.id)}).classed("focused",!1).transition().duration(100).style("opacity",1)};g.show=function(a){e.selectAll(Ra(a)).transition().style("opacity",1)};g.hide=function(a){e.selectAll(Ra(a)).transition().style("opacity", -0)};g.unzoom=function(){G.clear().update();C({withUpdateXDomain:!0})};g.load=function(a){r(a.done)&&(a.done=function(){});"categories"in a&&ca&&(Ea=a.categories,B.categories(Ea));if("cacheIds"in a&&Sa(a.cacheIds))Pa(Ta(a.cacheIds),a.done);else if("data"in a)Pa(Ja(a.data),a.done);else if("url"in a)e.csv(a.url,function(b,d){Pa(Ja(d),a.done)});else if("rows"in a)Pa(Ja(Wb(a.rows)),a.done);else if("columns"in a)Pa(Ja(Xb(a.columns)),a.done);else throw Error("url or rows or columns is required.");};g.unload= -function(a){g.data.targets=g.data.targets.filter(function(b){return b.id!==a});e.selectAll(".target-"+a).transition().style("opacity",0).remove();ya&&(e.selectAll(".legend-item-"+a).remove(),gb(g.data.targets));0b.classes.indexOf(a)})});return L}; -g.data.get=function(a){a=g.data.getAsTarget(a);return n(a)?a.values.map(function(a){return a.value}):void 0};g.data.getAsTarget=function(a){var b=$a(function(b){return b.id===a});return 0a?0:a}function ac(a){return qa&&a===qa||X&&ad(X,a)}function bd(a){return!ac(a)}function eb(a){var b=Ka[a.id];a.name=n(b)?b:a.id;return a}function bc(a){var b=a[0],d={},f=[],c,e;for(c=1;ct[d].indexOf(c.id)))for(f=0;f=e;e+=r)h+=f(a[c-1],a[c],e,l)}return h}function Ab(a){var b,d;gb=c.select(wa);if(gb.empty())Q.alert('No bind element found. Check the selector specified by "bindto" and existance of that element. Default "bindto" is "#chart".'); +else{gb.html("");h.data.x={};h.data.targets=La(a);ua();Jb();m.domain(c.extent([xa,Ma]));u.domain(la("y"));V.domain(la("y2"));da.domain(m.domain());Za.domain(u.domain());$a.domain(V.domain());z.ticks(10>a.length?a.length:10);oa.ticks(nd).outerTickSize(0).tickFormat(od);ab.ticks(pd).outerTickSize(0).tickFormat(qd);W=m.domain();G.x(da);ra&&B.x(m);Qa=c.select(wa).append("svg").attr("width",q+y+ca).attr("height",p+r+F).on("mouseenter",rd).on("mouseleave",sd);hb=Qa.append("defs");hb.append("clipPath").attr("id", +Bb).append("rect").attr("y",r).attr("width",q).attr("height",p-r);hb.append("clipPath").attr("id","xaxis-clip").append("rect").attr("x",-1-y).attr("y",-20).attr("width",S).attr("height",R);hb.append("clipPath").attr("id","yaxis-clip").append("rect").attr("x",-y+1).attr("y",r-1).attr("width",ka).attr("height",mb);k=Qa.append("g").attr("transform",J.main);v=ya?Qa.append("g").attr("transform",J.context):null;za=Aa?Qa.append("g").attr("transform",J.legend):null;Y=c.select(wa).style("position","relative").append("div").style("position", +"absolute").style("z-index","10").style("display","none");k.append("g").attr("class","x axis").attr("clip-path",g?"":"url(#xaxis-clip)").attr("transform",J.x).call(g?oa:z).append("text").attr("class","-axis-x-label").attr("x",q).attr("dy","-.5em").style("text-anchor","end").text(td);k.append("g").attr("class","y axis").attr("clip-path",g?"url(#yaxis-clip)":"").call(g?z:oa).append("text").attr("transform","rotate(-90)").attr("dy","1.2em").attr("dx","-.5em").style("text-anchor","end").text(ud);Rb&& +k.append("g").attr("class","y2 axis").attr("transform",J.y2).call(ab);b=k.append("g").attr("clip-path",Ra).attr("class","grid");zc&&b.append("g").attr("class","xgrids");Cb&&(d=b.append("g").attr("class","xgrid-lines").selectAll(".xgrid-line").data(Cb).enter().append("g").attr("class","xgrid-line"),d.append("line").attr("class",function(a){return""+a["class"]}),d.append("text").attr("class",function(a){return""+a["class"]}).attr("text-anchor","end").attr("transform",g?"":"rotate(-90)").attr("dx",g? +0:-r).attr("dy",-6).text(function(a){return a.text}));vd&&b.append("g").attr("class","xgrid-focus").append("line").attr("class","xgrid-focus").attr("x1",g?0:-10).attr("x2",g?q:-10).attr("y1",g?-10:r).attr("y2",g?-10:p);Ac&&b.append("g").attr("class","ygrids");Db&&b.append("g").attr("class","ygrid-lines").selectAll("ygrid-line").data(Db).enter().append("line").attr("class",function(a){return"ygrid-line "+a["class"]});k.append("g").attr("clip-path",Ra).attr("class","regions");k.append("g").attr("clip-path", +Ra).attr("class","chart");b=k.select(".chart").append("g").attr("class","event-rects").style("fill-opacity",0).style("cursor",ra?"ew-resize":null);X?wd(b):xd(b,a);k.select(".chart").append("g").attr("class","chart-bars");k.select(".chart").append("g").attr("class","chart-lines");if(ra)k.insert("rect",yd?null:"g.grid").attr("class","zoom-rect").attr("width",q).attr("height",p).style("opacity",0).style("cursor","ew-resize").call(B).on("dblclick.zoom",null);null!==ib&&G.extent("function"!==typeof ib? +ib:ib(xa,Ma));ya&&(v.append("g").attr("clip-path",Ra).attr("class","chart"),v.select(".chart").append("g").attr("class","chart-bars"),v.select(".chart").append("g").attr("class","chart-lines"),v.append("g").attr("clip-path",Ra).attr("class","x brush").call(G).selectAll("rect").attr("height",na),v.append("g").attr("class","x axis").attr("transform",J.subx).call(Fa));Aa&&jb(h.data.targets);Bc(h.data.targets);C({withTransition:!1,withUpdateXDomain:!0});if(zd){if(A&&"string"===typeof Ba){Ba=pa(Ba);for(a= +0;asc(d,a)?c.select(".event-rect").style("cursor","pointer"):c.select(".event-rect").style("cursor",null)}).on("click",function(){var a=c.mouse(this),d=ic(h.data.targets,a);100>sc(d, +a)&&k.select(".-circles-"+d.id).select(".-circle-"+d.index).each(function(){Dc(this,d,d.index)})}).call(c.behavior.drag().origin(Object).on("drag",function(){Ec(c.mouse(this))}).on("dragstart",function(){Fc(c.mouse(this))}).on("dragend",function(){Gc()})).call(B).on("dblclick.zoom",null)}function Dc(a,b,d){var f=c.select(a),e=f.classed(ja),g=!1,h;"circle"===a.nodeName?(g=qc(a,1.5*Pa),h=uc):"rect"===a.nodeName&&(g=rc(a),h=wc);if(sa||g)ia&&ta(b)&&(f.classed(ja,!e),h(!e,f,b,d)),Bd(b,f)}function Ec(a){if(ia&& +(!ra||B.altDomain)){var b=Hb[0],d=Hb[1],f=a[0];a=a[1];var e=Math.min(b,f),g=Math.max(b,f),h=sa?r:Math.min(d,a),m=sa?p:Math.max(d,a);k.select(".dragarea").attr("x",e).attr("y",h).attr("width",g-e).attr("height",m-h);k.selectAll(".-shapes").selectAll(".-shape").filter(function(a){return ta(a)}).each(function(a,b){var d=c.select(this),f=d.classed(ja),k=d.classed(Ib),l,n,p,q;l=!1;"circle"===this.nodeName?(l=1*d.attr("cx"),n=1*d.attr("cy"),q=uc,l=e",c,e,g;for(c=0;c"+g+""+e+"";return b+""}),zd=e(["tooltip","init","show"],!1),Ba=e(["tooltip","init","x"],0),Cc=e(["tooltip","init","position"],{top:"0px",left:"50px"}),Bb=wa.replace("#","")+"-clip",Ra="url(#"+Bb+")",A="timeseries"===Jc,ea="categorized"===Jc,sb=!A&&(qa||X),Hb=null,kb=!1,Gb=!1,ma=Aa?40:0,Z=function(a,b){var c=[],e=null!==b?b:"#1f77b4 #ff7f0e #2ca02c #d62728 #9467bd #8c564b #e377c2 #7f7f7f #bcbd22 #17becf".split(" ");return function(b){if(b in +a)return a[b];-1===c.indexOf(b)&&c.push(b);return e[c.indexOf(b)%e.length]}}(Hd,Id),Sc=function(){var a=[[c.time.format("%Y/%-m/%-d"),function(){return!0}],[c.time.format("%-m/%-d"),function(a){return a.getMonth()}],[c.time.format("%-m/%-d"),function(a){return 1!==a.getDate()}],[c.time.format("%-m/%-d"),function(a){return a.getDay()&&1!==a.getDate()}],[c.time.format("%I %p"),function(a){return a.getHours()}],[c.time.format("%I:%M"),function(a){return a.getMinutes()}],[c.time.format(":%S"),function(a){return a.getSeconds()}], +[c.time.format(".%L"),function(a){return a.getMilliseconds()}]];return function(b){for(var c=a.length-1,e=a[c];!e[1](b);)e=a[--c];return e[0](b)}}(),Nb,Ub,Pb,Ya,Tb,Vb,q,p,na,nb,va,Xb,Yb,qb,rb,m,u,V,da,Za,$a,z,oa,ab,Fa,Nc=g?"left":"bottom",Oc=g?pb?"top":"bottom":pb?"right":"left",Pc=g?ob?"bottom":"top":ob?"left":"right",Qc="bottom",J={main:function(){return"translate("+y+","+r+")"},context:function(){return"translate("+l+","+Ca+")"},legend:function(){return"translate("+Lb+","+Kb+")"},y2:function(){return"translate("+ +(g?0:q)+","+(g?10:0)+")"},x:function(){return"translate(0,"+p+")"},subx:function(){return"translate(0,"+na+")"}},Dd=function(){var a=c.svg.line().x(g?function(a){return E(a.id)(a.value)}:fb).y(g?fb:function(a){return E(a.id)(a.value)});return function(b){var c=xc(b.values),e;if(xb(b))return"spline"===ha["string"===typeof b?b:b.id]?a.interpolate("cardinal"):a.interpolate("linear"),Ic[b.id]?md(c,m,E(b.id),Ic[b.id]):a(c);e=m(c[0].x);b=E(b.id)(c[0].value);return g?"M "+b+" "+e:"M "+e+" "+b}}(),Ed=function(){var a= +c.svg.line().x(function(a){return da(a.x)}).y(function(a){return T(a.id)(a.value)});return function(b){var c=xc(b.values);return xb(b)?a(c):"M "+da(c[0].x)+" "+T(b.id)(c[0].value)}}(),G=c.svg.brush().on("brush",function(){C({withTransition:!1,withY:!1,withSubchart:!1,withUpdateXDomain:!0})}),B=c.behavior.zoom().on("zoomstart",function(){B.altDomain=c.event.sourceEvent.altKey?m.orgDomain():null}).on("zoom",ra?Fd:null);G.update=function(){v&&v.select(".x.brush").call(this);return this};B.orgScaleExtent= +function(){var a=Hc?Hc:[1,10];return[a[0],Math.max(Na()/a[1],a[1])]};B.updateScaleExtent=function(){var a=m.orgDomain(),a=(a[1]-a[0])/(W[1]-W[0]),b=this.orgScaleExtent();this.scaleExtent([b[0]*a,b[1]*a]);return this};var Qa,hb,k,v,za,Y,gb,xa=null,Ma=null,W;h.focus=function(a){h.defocus();c.selectAll(Ua(a)).filter(function(a){return ub(a.id)}).classed("focused",!0).transition().duration(100).style("opacity",1)};h.defocus=function(a){c.selectAll(Ua(a)).filter(function(a){return ub(a.id)}).classed("focused", +!1).transition().duration(100).style("opacity",0.3)};h.revert=function(a){c.selectAll(Ua(a)).filter(function(a){return ub(a.id)}).classed("focused",!1).transition().duration(100).style("opacity",1)};h.show=function(a){c.selectAll(Ua(a)).transition().style("opacity",1)};h.hide=function(a){c.selectAll(Ua(a)).transition().style("opacity",0)};h.unzoom=function(){G.clear().update();C({withUpdateXDomain:!0})};h.load=function(a){s(a.done)&&(a.done=function(){});"categories"in a&&ea&&(Ga=a.categories,z.categories(Ga)); +if("cacheIds"in a&&Va(a.cacheIds))Sa(Wa(a.cacheIds),a.done);else if("data"in a)Sa(La(a.data),a.done);else if("url"in a)c.csv(a.url,function(b,c){Sa(La(c),a.done)});else if("rows"in a)Sa(La(bc(a.rows)),a.done);else if("columns"in a)Sa(La(cc(a.columns)),a.done);else throw Error("url or rows or columns is required.");};h.unload=function(a){h.data.targets=h.data.targets.filter(function(b){return b.id!==a});c.selectAll(".target-"+a).transition().style("opacity",0).remove();Aa&&(c.selectAll(".legend-item-"+ +a).remove(),jb(h.data.targets));0b.classes.indexOf(a)})});return L};h.data.get=function(a){a=h.data.getAsTarget(a);return n(a)?a.values.map(function(a){return a.value}):void 0};h.data.getAsTarget=function(a){var b= +cb(function(b){return b.id===a});return 0