"use strict";
/* Copyright 2015 Bloomberg Finance L.P.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Lines = void 0;
const _ = __importStar(require("underscore"));
const d3 = __importStar(require("d3"));
// var d3 =Object.assign({}, require("d3-array"), require("d3-selection"), require("d3-shape"), require("d3-transition"));
const Mark_1 = require("./Mark");
const markers = __importStar(require("./Markers"));
const bqSymbol = markers.symbol;
class Lines extends Mark_1.Mark {
    render() {
        const _super = Object.create(null, {
            render: { get: () => super.render }
        });
        return __awaiter(this, void 0, void 0, function* () {
            const baseRenderPromise = _super.render.call(this);
            this.dot = bqSymbol().size(this.model.get('marker_size'));
            if (this.model.get('marker')) {
                this.dot.type(this.model.get('marker'));
            }
            // TODO: create_listeners is put inside the promise success handler
            // because some of the functions depend on child scales being
            // created. Make sure none of the event handler functions make that
            // assumption.
            this.displayed.then(() => {
                this.parent.tooltip_div.node().appendChild(this.tooltip_div.node());
                this.create_tooltip();
            });
            this.display_el_classes = ['line', 'legendtext', 'dot'];
            yield baseRenderPromise;
            this.event_listeners = {};
            this.process_interactions();
            this.create_listeners();
            this.compute_view_padding();
            this.draw(false);
        });
    }
    set_ranges() {
        const xScale = this.scales.x;
        if (xScale) {
            xScale.set_range(this.parent.padded_range('x', xScale.model));
        }
        const yScale = this.scales.y;
        if (yScale) {
            yScale.set_range(this.parent.padded_range('y', yScale.model));
        }
    }
    set_positional_scales() {
        const xScale = this.scales.x, yScale = this.scales.y;
        this.listenTo(xScale, 'domain_changed', function () {
            if (!this.model.dirty) {
                this.update_line_xy();
            }
        });
        this.listenTo(yScale, 'domain_changed', function () {
            if (!this.model.dirty) {
                this.update_line_xy();
            }
        });
    }
    initialize_additional_scales() {
        const colorScale = this.scales.color;
        if (colorScale) {
            this.listenTo(colorScale, 'domain_changed', function () {
                this.update_style();
            });
            colorScale.on('color_scale_range_changed', this.update_style, this);
        }
    }
    create_listeners() {
        super.create_listeners();
        this.d3el
            .on('mouseover', _.bind(() => {
            this.event_dispatcher('mouse_over');
        }, this))
            .on('mousemove', _.bind(() => {
            this.event_dispatcher('mouse_move');
        }, this))
            .on('mouseout', _.bind(() => {
            this.event_dispatcher('mouse_out');
        }, this));
        this.listenTo(this.model, 'change:tooltip', this.create_tooltip);
        // FIXME: multiple calls to update_path_style. Use on_some_change.
        this.listenTo(this.model, 'change:interpolation', this.update_path_style);
        this.listenTo(this.model, 'change:close_path', this.update_path_style);
        // FIXME: multiple calls to update_style. Use on_some_change.
        this.listenTo(this.model, 'change:colors', this.update_style);
        this.listenTo(this.model, 'change:opacities', this.update_style);
        this.listenTo(this.model, 'change:fill_opacities', this.update_style);
        this.listenTo(this.model, 'change:fill_colors', this.update_style);
        this.listenTo(this.model, 'change:fill', this.update_fill);
        this.listenTo(this.model, 'data_updated', function () {
            const animate = true;
            this.draw(animate);
        });
        this.listenTo(this.model, 'labels_updated', this.update_labels);
        this.listenTo(this.model, 'change:stroke_width', this.update_stroke_width);
        this.listenTo(this.model, 'change:labels_visibility', this.update_legend_labels);
        this.listenTo(this.model, 'change:curves_subset', this.update_curves_subset);
        this.listenTo(this.model, 'change:line_style', this.update_line_style);
        this.listenTo(this.model, 'change:interactions', this.process_interactions);
        this.listenTo(this.parent, 'bg_clicked', function () {
            this.event_dispatcher('parent_clicked');
        });
        this.listenTo(this.model, 'change:marker', this.update_marker);
        this.listenTo(this.model, 'change:marker_size', this.update_marker_size);
    }
    update_legend_labels() {
        if (this.model.get('labels_visibility') === 'none') {
            this.d3el.selectAll('.legend').attr('display', 'none');
            this.d3el.selectAll('.curve_label').attr('display', 'none');
        }
        else if (this.model.get('labels_visibility') === 'label') {
            this.d3el.selectAll('.legend').attr('display', 'none');
            this.d3el.selectAll('.curve_label').attr('display', 'inline');
        }
        else {
            this.d3el.selectAll('.legend').attr('display', 'inline');
            this.d3el.selectAll('.curve_label').attr('display', 'none');
        }
    }
    update_labels() {
        this.d3el
            .selectAll('.curve')
            .data(this.model.mark_data)
            .select('.curve_label')
            .text((d) => {
            return d.name;
        });
    }
    get_line_style() {
        switch (this.model.get('line_style')) {
            case 'solid':
                return 'none';
            case 'dashed':
                return '10,10';
            case 'dotted':
                return '2,10';
            case 'dash_dotted':
                return '10,5,2,5';
        }
    }
    // Updating the style of the curve, stroke, colors, dashed etc...
    // Could be fused in a single function for increased readability
    // and to avoid code repetition
    update_line_style() {
        this.d3el
            .selectAll('.curve')
            .select('.line')
            .style('stroke-dasharray', _.bind(this.get_line_style, this));
        if (this.legendEl) {
            this.legendEl
                .select('path')
                .style('stroke-dasharray', _.bind(this.get_line_style, this));
        }
    }
    update_stroke_width(model, strokeWidth) {
        this.compute_view_padding();
        this.d3el
            .selectAll('.curve')
            .select('.line')
            .style('stroke-width', strokeWidth);
        if (this.legendEl) {
            this.legendEl.select('path').style('stroke-width', strokeWidth);
        }
    }
    update_style() {
        const fill = this.model.get('fill'), fillColor = this.model.get('fill_colors'), fillOpacities = this.model.get('fill_opacities');
        // update curve colors
        const curves = this.d3el.selectAll('.curve');
        curves
            .select('.line')
            .style('opacity', this.get_mark_opacity.bind(this))
            .style('stroke', (d, i) => {
            return this.get_mark_color(d, i) || fillColor[i];
        })
            .style('fill', (d, i) => {
            return fill === 'inside' ? this.get_fill_color(d, i) : '';
        })
            .style('fill-opacity', (d, i) => {
            return fill === 'inside' ? fillOpacities[i] : '';
        });
        curves
            .select('.area')
            .style('fill', (d, i) => {
            return this.get_fill_color(d, i);
        })
            .style('opacity', (d, i) => {
            return fillOpacities[i];
        });
        this.update_marker_style();
        // update legend style
        if (this.legendEl) {
            this.legendEl
                .select('.line')
                .style('stroke', (d, i) => {
                return this.get_mark_color(d, i) || fillColor[i];
            })
                .style('opacity', this.get_mark_opacity.bind(this))
                .style('fill', (d, i) => {
                return this.model.get('fill') === 'none'
                    ? ''
                    : this.get_fill_color(d, i);
            });
            this.legendEl
                .select('.dot')
                .style('stroke', (d, i) => {
                return this.get_mark_color(d, i) || fillColor[i];
            })
                .style('opacity', this.get_mark_opacity.bind(this))
                .style('fill', (d, i) => {
                return this.get_mark_color(d, i) || fillColor[i];
            });
            this.legendEl
                .select('text')
                .style('fill', (d, i) => {
                return this.get_mark_color(d, i) || fillColor[i];
            })
                .style('opacity', this.get_mark_opacity.bind(this));
        }
        this.update_stroke_width(this.model, this.model.get('stroke_width'));
        this.update_line_style();
    }
    path_closure() {
        return this.model.get('close_path') ? 'Z' : '';
    }
    update_path_style() {
        const interpolation = this.get_interpolation();
        this.line.curve(interpolation);
        this.area.curve(interpolation);
        this.d3el
            .selectAll('.curve')
            .select('.line')
            .attr('d', (d) => {
            return this.line(d.values) + this.path_closure();
        });
        this.d3el
            .selectAll('.curve')
            .select('.area')
            .transition('update_path_style')
            .duration(0) //FIXME
            .attr('d', (d) => {
            return this.area(d.values);
        });
        if (this.legendEl) {
            this.legendLine.curve(interpolation);
            this.legendEl
                .selectAll('path')
                .attr('d', this.legendLine(this.legendPathData) + this.path_closure());
        }
    }
    relayout() {
        this.set_ranges();
        this.update_line_xy(false);
    }
    selector_changed(pointSelector, rectSelector) {
        if (pointSelector === undefined) {
            this.model.set('selected', null);
            this.touch();
            return [];
        }
        const pixels = this.pixelCoords;
        const indices = new Uint32Array(_.range(pixels.length));
        const selected = indices.filter((index) => {
            return pointSelector(pixels[index]);
        });
        this.model.set('selected', selected);
        this.touch();
    }
    invert_point(pixel) {
        if (pixel === undefined) {
            this.model.set('selected', null);
            this.touch();
            return;
        }
        const index = Math.min(this.bisect(this.xPixels, pixel), Math.max(this.xPixels.length - 1, 0));
        this.model.set('selected', new Uint32Array([index]));
        this.touch();
    }
    update_multi_range(brushExtent) {
        const xStart = brushExtent[0];
        const xEnd = brushExtent[1];
        const data = this.model.x_data[0] instanceof Array
            ? this.model.x_data[0]
            : this.model.x_data;
        const idXStart = this.bisect(data, xStart);
        const idXEnd = Math.min(this.bisect(data, xEnd), Math.max(data.length - 1, 0));
        this.selectorModel.set('selected', [idXStart, idXEnd]);
        this.selector.touch();
    }
    draw_legend(elem, xDisp, yDisp, interXDisp, interYDisp) {
        const curveLabels = this.model.get_labels();
        const legendData = this.model.mark_data.map((d) => {
            return { index: d.index, name: d.name, color: d.color };
        });
        this.legendEl = elem.selectAll('.legend' + this.uuid).data(legendData);
        const rectDim = interYDisp * 0.8, fillColors = this.model.get('fill_colors');
        this.legendLine = d3
            .line()
            .curve(this.get_interpolation())
            .x((d) => {
            return d[0];
        })
            .y((d) => {
            return d[1];
        });
        this.legendPathData = [
            [0, rectDim],
            [rectDim / 2, 0],
            [rectDim, rectDim / 2],
        ];
        const legend = this.legendEl
            .enter()
            .append('g')
            .attr('class', 'legend' + this.uuid)
            .attr('transform', (d, i) => {
            return 'translate(0, ' + (i * interYDisp + yDisp) + ')';
        })
            .on('mouseover', _.bind(() => {
            this.event_dispatcher('legend_mouse_over');
        }, this))
            .on('mouseout', _.bind(() => {
            this.event_dispatcher('legend_mouse_out');
        }, this))
            .on('click', _.bind(() => {
            this.event_dispatcher('legend_clicked');
        }, this));
        legend
            .append('path')
            .attr('class', 'line')
            .attr('fill', 'none')
            .attr('d', this.legendLine(this.legendPathData) + this.path_closure())
            .style('stroke', (d, i) => {
            return this.get_mark_color(d, i) || fillColors[i];
        })
            .style('fill', (d, i) => {
            return this.model.get('fill') === 'none'
                ? ''
                : this.get_fill_color(d, i);
        })
            .style('opacity', this.get_mark_opacity.bind(this))
            .style('stroke-width', this.model.get('stroke_width'))
            .style('stroke-dasharray', _.bind(this.get_line_style, this));
        if (this.model.get('marker')) {
            legend
                .append('path')
                .attr('class', 'dot')
                .attr('transform', 'translate(' + rectDim / 2 + ',0)')
                .attr('d', this.dot.size(25))
                .style('fill', (d, i) => {
                return this.get_mark_color(d, i);
            });
        }
        legend
            .append('text')
            .attr('class', 'legendtext')
            .attr('x', rectDim * 1.2)
            .attr('y', rectDim / 2)
            .attr('dy', '0.35em')
            .text((d, i) => {
            return curveLabels[i];
        })
            .style('fill', (d, i) => {
            return this.get_mark_color(d, i) || fillColors[i];
        })
            .style('opacity', this.get_mark_opacity.bind(this));
        this.legendEl = legend.merge(this.legendEl);
        const maxLength = d3.max(curveLabels, (d) => {
            return d.length;
        });
        this.legendEl.exit().remove();
        return [this.model.mark_data.length, maxLength];
    }
    update_curves_subset() {
        const displayLabels = this.model.get('labels_visibility') === 'label';
        // Show a subset of the curves
        const curvesSubset = this.model.get('curves_subset');
        if (curvesSubset.length > 0) {
            this.d3el
                .selectAll('.curve')
                .attr('display', (d, i) => {
                return curvesSubset.indexOf(i) !== -1 ? 'inline' : 'none';
            })
                .select('.curve_label')
                .attr('display', (d, i) => {
                return curvesSubset.indexOf(i) !== -1 && displayLabels
                    ? 'inline'
                    : 'none';
            });
            if (this.legendEl) {
                this.legendEl.attr('display', (d, i) => {
                    return curvesSubset.indexOf(i) !== -1 ? 'inline' : 'none';
                });
            }
            this.d3el.selectAll('.curve');
        }
        else {
            //make all curves visible
            this.d3el
                .selectAll('.curve')
                .attr('display', 'inline')
                .select('.curve_label')
                .attr('display', (d) => {
                return displayLabels ? 'inline' : 'none';
            });
            if (this.legendEl) {
                this.legendEl.attr('display', 'inline');
            }
        }
    }
    update_fill() {
        const fill = this.model.get('fill'), area = fill === 'top' || fill === 'bottom' || fill === 'between';
        const yScale = this.scales.y;
        this.area.defined((d) => {
            return area && d.y !== null && isFinite(yScale.scale(d.y));
        });
        if (fill == 'bottom') {
            this.area.y0(this.parent.plotarea_height);
        }
        else if (fill == 'top') {
            this.area.y0(0);
        }
        else if (fill == 'between') {
            this.area.y0((d) => {
                return yScale.scale(d.y0) + yScale.offset;
            });
        }
        this.d3el
            .selectAll('.curve')
            .select('.area')
            .attr('d', (d) => {
            return this.area(d.values);
        });
        this.d3el
            .selectAll('.curve')
            .select('.line')
            .style('fill', (d, i) => {
            return fill === 'inside' ? this.get_fill_color(d, i) : '';
        });
        // update legend fill
        if (this.legendEl) {
            this.legendEl.select('path').style('fill', (d, i) => {
                return fill === 'none' ? '' : this.get_fill_color(d, i);
            });
        }
    }
    get_fill_color(data, index) {
        const fillColors = this.model.get('fill_colors');
        return fillColors.length === 0
            ? this.get_mark_color(data, index)
            : fillColors[index];
    }
    update_line_xy(animate) {
        const xScale = this.scales.x, yScale = this.scales.y;
        const animationDuration = animate === true ? this.parent.model.get('animation_duration') : 0;
        this.line
            .x((d) => {
            return xScale.scale(d.x) + xScale.offset;
        })
            .y((d) => {
            return yScale.scale(d.y) + yScale.offset;
        });
        const fill = this.model.get('fill');
        this.area
            .x((d) => {
            return xScale.scale(d.x) + xScale.offset;
        })
            .y1((d) => {
            return yScale.scale(d.y) + yScale.offset;
        });
        if (fill == 'bottom') {
            this.area.y0(this.parent.plotarea_height);
        }
        else if (fill == 'top') {
            this.area.y0(0);
        }
        else if (fill == 'between') {
            this.area.y0((d) => {
                return yScale.scale(d.y0) + yScale.offset;
            });
        }
        const curvesSel = this.d3el.selectAll('.curve');
        curvesSel
            .select('.line')
            .transition('update_line_xy')
            .attr('d', (d) => {
            return this.line(d.values) + this.path_closure();
        })
            .duration(animationDuration);
        curvesSel
            .select('.area')
            .transition('update_line_xy')
            .attr('d', (d, i) => {
            return this.area(d.values);
        })
            .duration(animationDuration);
        curvesSel
            .select('.curve_label')
            .transition('update_line_xy')
            .attr('transform', (d) => {
            const lastXy = d.values[d.values.length - 1];
            return ('translate(' +
                xScale.scale(lastXy.x) +
                ',' +
                yScale.scale(lastXy.y) +
                ')');
        })
            .duration(animationDuration);
        this.update_dots_xy(animate);
        this.xPixels =
            this.model.mark_data.length > 0
                ? this.model.mark_data[0].values.map((el) => {
                    return xScale.scale(el.x) + xScale.offset;
                })
                : [];
        this.yPixels =
            this.model.mark_data.length > 0
                ? this.model.mark_data[0].values.map((el) => {
                    return yScale.scale(el.y) + yScale.offset;
                })
                : [];
        this.pixelCoords =
            this.model.mark_data.length > 0
                ? this.model.mark_data[0].values.map((el) => {
                    return [
                        xScale.scale(el.x) + xScale.offset,
                        yScale.scale(el.y) + yScale.offset,
                    ];
                })
                : [];
    }
    get_interpolation() {
        const curveTypes = {
            linear: d3.curveLinear,
            basis: d3.curveBasis,
            'basis-open': d3.curveBasisOpen,
            'basis-closed': d3.curveBasisClosed,
            bundle: d3.curveBundle,
            cardinal: d3.curveCardinal,
            'cardinal-open': d3.curveCardinalOpen,
            'cardinal-closed': d3.curveCardinalClosed,
            monotone: d3.curveMonotoneY,
            'step-before': d3.curveStepBefore,
            'step-after': d3.curveStepAfter,
        };
        return curveTypes[this.model.get('interpolation')];
    }
    draw(animate) {
        this.set_ranges();
        const curvesSel = this.d3el.selectAll('.curve').data(this.model.mark_data);
        const yScale = this.scales.y;
        const newCurves = curvesSel.enter().append('g').attr('class', 'curve');
        newCurves
            .append('path')
            .attr('class', 'line')
            .attr('fill', 'none')
            .attr('id', (d, i) => 'curve' + (i + 1))
            .on('click', () => {
            this.event_dispatcher('element_clicked');
        });
        newCurves.append('path').attr('class', 'area');
        newCurves
            .append('text')
            .attr('class', 'curve_label')
            .attr('x', 3)
            .attr('dy', '.35em')
            .attr('display', this.model.get('labels_visibility') !== 'label' ? 'none' : 'inline')
            .text((d) => d.name);
        const fill = this.model.get('fill'), area = fill === 'top' || fill === 'bottom' || fill === 'between';
        this.draw_dots();
        this.line = d3
            .line()
            .curve(this.get_interpolation())
            .defined((d) => {
            return d.y !== null && isFinite(yScale.scale(d.y));
        });
        this.area = d3
            .area()
            .curve(this.get_interpolation())
            .defined((d) => {
            return area && d.y !== null && isFinite(yScale.scale(d.y));
        });
        // Having a transition on exit is complicated. Please refer to
        // Scatter.js for detailed explanation.
        curvesSel.exit().remove();
        this.update_line_xy(animate);
        this.update_style();
        // alter the display only if a few of the curves are visible
        this.update_curves_subset();
    }
    draw_dots() {
        if (this.model.get('marker')) {
            const dots = this.d3el
                .selectAll('.curve')
                .selectAll('.dot')
                .data((d, i) => {
                return d.values.map((e) => {
                    return { x: e.x, y: e.y, sub_index: e.sub_index };
                });
            });
            dots.enter().append('path').attr('class', 'dot');
            dots.exit().remove();
        }
    }
    update_dots_xy(animate) {
        if (this.model.get('marker')) {
            const xScale = this.scales.x, yScale = this.scales.y;
            const animationDuration = animate === true ? this.parent.model.get('animation_duration') : 0;
            const dots = this.d3el.selectAll('.curve').selectAll('.dot');
            dots
                .transition('update_dots_xy')
                .duration(animationDuration)
                .attr('transform', (d) => {
                return ('translate(' +
                    (xScale.scale(d.x) + xScale.offset) +
                    ',' +
                    (yScale.scale(d.y) + yScale.offset) +
                    ')');
            })
                .attr('d', this.dot
                .size(this.model.get('marker_size'))
                .type(this.model.get('marker')));
        }
    }
    compute_view_padding() {
        //This function sets the padding for the view through the variables
        //xPadding and yPadding which are view specific paddings in pixel
        let xPadding;
        if (this.model.get('marker')) {
            const markerPadding = Math.sqrt(this.model.get('marker_size')) / 2 + 1.0;
            const linePadding = this.model.get('stroke_width') / 2.0;
            xPadding = Math.max(markerPadding, linePadding);
        }
        else {
            xPadding = this.model.get('stroke_width') / 2.0;
        }
        const yPadding = xPadding;
        if (xPadding !== this.xPadding || yPadding !== this.yPadding) {
            this.xPadding = xPadding;
            this.yPadding = yPadding;
            this.trigger('mark_padding_updated');
        }
    }
    update_marker_style() {
        const that = this;
        const fillColor = this.model.get('fill_colors');
        const opacities = this.model.get('opacities');
        this.d3el.selectAll('.curve').each(function (d, i) {
            const curve = d3.select(this);
            curve
                .selectAll('.dot')
                .style('opacity', opacities[i])
                .style('fill', that.get_mark_color(d, i) || fillColor[i]);
        });
    }
    update_marker(model, marker) {
        if (marker) {
            this.draw_dots();
            this.update_dots_xy(false);
            this.update_marker_style();
            if (this.legendEl) {
                this.legendEl.select('.dot').attr('d', this.dot.type(marker).size(25));
            }
        }
        else {
            this.d3el.selectAll('.dot').remove();
            if (this.legendEl) {
                this.legendEl.select('.dot').attr('d', this.dot.size(0));
            }
        }
    }
    update_marker_size(model, markerSize) {
        this.compute_view_padding();
        this.d3el.selectAll('.dot').attr('d', this.dot.size(markerSize));
    }
    clear_style(style_dict, indices) { }
    set_default_style(indices) { }
    set_style_on_elements(style, indices) { }
}
exports.Lines = Lines;
//# sourceMappingURL=Lines.js.map