c3_chart_fn.flow = function (args) {
    var $$ = this.internal,
        targets, data, notfoundIds = [], orgDataCount = $$.getMaxDataCount(),
        dataCount, domain, baseTarget, baseValue, length = 0, tail = 0, diff, to;

    if (args.json) {
        data = $$.convertJsonToData(args.json, args.keys);
    }
    else if (args.rows) {
        data = $$.convertRowsToData(args.rows);
    }
    else if (args.columns) {
        data = $$.convertColumnsToData(args.columns);
    }
    else {
        return;
    }
    targets = $$.convertDataToTargets(data, true);

    // Update/Add data
    $$.data.targets.forEach(function (t) {
        var found = false, i, j;
        for (i = 0; i < targets.length; i++) {
            if (t.id === targets[i].id) {
                found = true;

                if (t.values[t.values.length - 1]) {
                    tail = t.values[t.values.length - 1].index + 1;
                }
                length = targets[i].values.length;

                for (j = 0; j < length; j++) {
                    targets[i].values[j].index = tail + j;
                    if (!$$.isTimeSeries()) {
                        targets[i].values[j].x = tail + j;
                    }
                }
                t.values = t.values.concat(targets[i].values);

                targets.splice(i, 1);
                break;
            }
        }
        if (!found) { notfoundIds.push(t.id); }
    });

    // Append null for not found targets
    $$.data.targets.forEach(function (t) {
        var i, j;
        for (i = 0; i < notfoundIds.length; i++) {
            if (t.id === notfoundIds[i]) {
                tail = t.values[t.values.length - 1].index + 1;
                for (j = 0; j < length; j++) {
                    t.values.push({
                        id: t.id,
                        index: tail + j,
                        x: $$.isTimeSeries() ? $$.getOtherTargetX(tail + j) : tail + j,
                        value: null
                    });
                }
            }
        }
    });

    // Generate null values for new target
    if ($$.data.targets.length) {
        targets.forEach(function (t) {
            var i, missing = [];
            for (i = $$.data.targets[0].values[0].index; i < tail; i++) {
                missing.push({
                    id: t.id,
                    index: i,
                    x: $$.isTimeSeries() ? $$.getOtherTargetX(i) : i,
                    value: null
                });
            }
            t.values.forEach(function (v) {
                v.index += tail;
                if (!$$.isTimeSeries()) {
                    v.x += tail;
                }
            });
            t.values = missing.concat(t.values);
        });
    }
    $$.data.targets = $$.data.targets.concat(targets); // add remained

    // check data count because behavior needs to change when it's only one
    dataCount = $$.getMaxDataCount();
    baseTarget = $$.data.targets[0];
    baseValue = baseTarget.values[0];

    // Update length to flow if needed
    if (isDefined(args.to)) {
        length = 0;
        to = $$.isTimeSeries() ? $$.parseDate(args.to) : args.to;
        baseTarget.values.forEach(function (v) {
            if (v.x < to) { length++; }
        });
    } else if (isDefined(args.length)) {
        length = args.length;
    }

    // If only one data, update the domain to flow from left edge of the chart
    if (!orgDataCount) {
        if ($$.isTimeSeries()) {
            if (baseTarget.values.length > 1) {
                diff = baseTarget.values[baseTarget.values.length - 1].x - baseValue.x;
            } else {
                diff = baseValue.x - $$.getXDomain($$.data.targets)[0];
            }
        } else {
            diff = 1;
        }
        domain = [baseValue.x - diff, baseValue.x];
        $$.updateXDomain(null, true, true, domain);
    } else if (orgDataCount === 1) {
        if ($$.isTimeSeries()) {
            diff = (baseTarget.values[baseTarget.values.length - 1].x - baseValue.x) / 2;
            domain = [new Date(+baseValue.x - diff), new Date(+baseValue.x + diff)];
            $$.updateXDomain(null, true, true, domain);
        }
    }

    // Set targets
    $$.updateTargets($$.data.targets);

    // Redraw with new targets
    $$.redraw({
        flow: {
            index: baseValue.index,
            length: length,
            duration: isValue(args.duration) ? args.duration : $$.config.transition_duration,
            done: args.done,
            orgDataCount: orgDataCount,
        },
        withLegend: true,
        withTransition: orgDataCount > 1,
    });
};

c3_chart_internal_fn.generateFlow = function (args) {
    var $$ = this, config = $$.config, d3 = $$.d3;

    return function () {
        var targets = args.targets,
            flow = args.flow,
            drawBar = args.drawBar,
            drawLine = args.drawLine,
            drawArea = args.drawArea,
            cx = args.cx,
            cy = args.cy,
            xv = args.xv,
            xForText = args.xForText,
            yForText = args.yForText,
            duration = args.duration;

        var translateX, scaleX = 1, transform,
            flowIndex = flow.index,
            flowLength = flow.length,
            flowStart = $$.getValueOnIndex($$.data.targets[0].values, flowIndex),
            flowEnd = $$.getValueOnIndex($$.data.targets[0].values, flowIndex + flowLength),
            orgDomain = $$.x.domain(), domain,
            durationForFlow = flow.duration || duration,
            done = flow.done || function () {},
            wait = $$.generateWait();

        var xgrid = $$.xgrid || d3.selectAll([]),
            xgridLines = $$.xgridLines || d3.selectAll([]),
            mainRegion = $$.mainRegion || d3.selectAll([]),
            mainText = $$.mainText || d3.selectAll([]),
            mainBar = $$.mainBar || d3.selectAll([]),
            mainLine = $$.mainLine || d3.selectAll([]),
            mainArea = $$.mainArea || d3.selectAll([]),
            mainCircle = $$.mainCircle || d3.selectAll([]);

        // remove head data after rendered
        $$.data.targets.forEach(function (d) {
            d.values.splice(0, flowLength);
        });

        // update x domain to generate axis elements for flow
        domain = $$.updateXDomain(targets, true, true);
        // update elements related to x scale
        if ($$.updateXGrid) { $$.updateXGrid(true); }

        // generate transform to flow
        if (!flow.orgDataCount) { // if empty
            if ($$.data.targets[0].values.length !== 1) {
                translateX = $$.x(orgDomain[0]) - $$.x(domain[0]);
            } else {
                if ($$.isTimeSeries()) {
                    flowStart = $$.getValueOnIndex($$.data.targets[0].values, 0);
                    flowEnd = $$.getValueOnIndex($$.data.targets[0].values, $$.data.targets[0].values.length - 1);
                    translateX = $$.x(flowStart.x) - $$.x(flowEnd.x);
                } else {
                    translateX = diffDomain(domain) / 2;
                }
            }
        } else if (flow.orgDataCount === 1 || flowStart.x === flowEnd.x) {
            translateX = $$.x(orgDomain[0]) - $$.x(domain[0]);
        } else {
            if ($$.isTimeSeries()) {
                translateX = ($$.x(orgDomain[0]) - $$.x(domain[0]));
            } else {
                translateX = ($$.x(flowStart.x) - $$.x(flowEnd.x));
            }
        }
        scaleX = (diffDomain(orgDomain) / diffDomain(domain));
        transform = 'translate(' + translateX + ',0) scale(' + scaleX + ',1)';

        d3.transition().ease('linear').duration(durationForFlow).each(function () {
            wait.add($$.axes.x.transition().call($$.xAxis));
            wait.add(mainBar.transition().attr('transform', transform));
            wait.add(mainLine.transition().attr('transform', transform));
            wait.add(mainArea.transition().attr('transform', transform));
            wait.add(mainCircle.transition().attr('transform', transform));
            wait.add(mainText.transition().attr('transform', transform));
            wait.add(mainRegion.filter($$.isRegionOnX).transition().attr('transform', transform));
            wait.add(xgrid.transition().attr('transform', transform));
            wait.add(xgridLines.transition().attr('transform', transform));
        })
        .call(wait, function () {
            var i, shapes = [], texts = [], eventRects = [];

            // remove flowed elements
            if (flowLength) {
                for (i = 0; i < flowLength; i++) {
                    shapes.push('.' + CLASS.shape + '-' + (flowIndex + i));
                    texts.push('.' + CLASS.text + '-' + (flowIndex + i));
                    eventRects.push('.' + CLASS.eventRect + '-' + (flowIndex + i));
                }
                $$.svg.selectAll('.' + CLASS.shapes).selectAll(shapes).remove();
                $$.svg.selectAll('.' + CLASS.texts).selectAll(texts).remove();
                $$.svg.selectAll('.' + CLASS.eventRects).selectAll(eventRects).remove();
                $$.svg.select('.' + CLASS.xgrid).remove();
            }

            // draw again for removing flowed elements and reverting attr
            xgrid
                .attr('transform', null)
                .attr($$.xgridAttr);
            xgridLines
                .attr('transform', null);
            xgridLines.select('line')
                .attr("x1", config.axis_rotated ? 0 : xv)
                .attr("x2", config.axis_rotated ? $$.width : xv);
            xgridLines.select('text')
                .attr("x", config.axis_rotated ? $$.width : 0)
                .attr("y", xv);
            mainBar
                .attr('transform', null)
                .attr("d", drawBar);
            mainLine
                .attr('transform', null)
                .attr("d", drawLine);
            mainArea
                .attr('transform', null)
                .attr("d", drawArea);
            mainCircle
                .attr('transform', null)
                .attr("cx", cx)
                .attr("cy", cy);
            mainText
                .attr('transform', null)
                .attr('x', xForText)
                .attr('y', yForText)
                .style('fill-opacity', $$.opacityForText.bind($$));
            mainRegion
                .attr('transform', null);
            mainRegion.select('rect').filter($$.isRegionOnX)
                .attr("x", $$.regionX.bind($$))
                .attr("width", $$.regionWidth.bind($$));
            $$.updateEventRect();

            // callback for end of flow
            done();
        });
    };
};
