"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.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.OHLC = void 0;
const d3 = __importStar(require("d3"));
const _ = __importStar(require("underscore"));
const Mark_1 = require("./Mark");
const utils_1 = require("./utils");
class OHLC extends Mark_1.Mark {
    render() {
        const _super = Object.create(null, {
            render: { get: () => super.render }
        });
        return __awaiter(this, void 0, void 0, function* () {
            this.display_el_classes = ['stick_body'];
            const base_creation_promise = _super.render.call(this);
            this.selected_indices = this.model.get('selected');
            this.selected_style = this.model.get('selected_style');
            this.unselected_style = this.model.get('unselected_style');
            this.display_el_classes = ['stick_body', 'stick_tail', 'stick_head'];
            this.displayed.then(() => {
                this.parent.tooltip_div.node().appendChild(this.tooltip_div.node());
                this.create_tooltip();
            });
            yield base_creation_promise;
            this.event_listeners = {};
            this.process_interactions();
            this.create_listeners();
            this.draw();
        });
    }
    set_ranges() {
        if (this.scales.x) {
            this.scales.x.set_range(this.parent.padded_range('x', this.scales.x.model));
        }
        if (this.scales.y) {
            this.scales.y.set_range(this.parent.padded_range('y', this.scales.y.model));
        }
    }
    set_positional_scales() {
        this.listenTo(this.scales.x, 'domain_changed', () => {
            if (!this.model.dirty) {
                this.draw();
            }
        });
        this.listenTo(this.scales.y, 'domain_changed', () => {
            if (!this.model.dirty) {
                this.draw();
            }
        });
    }
    /**
     * Creates event listeners and binds them to rendered d3 elements
     */
    create_listeners() {
        super.create_listeners.apply(this);
        this.d3el
            .on('mouseover', () => {
            this.event_dispatcher('mouse_over');
        })
            .on('mousemove', () => {
            this.event_dispatcher('mouse_move');
        })
            .on('mouseout', () => {
            this.event_dispatcher('mouse_out');
        });
        this.listenTo(this.model, 'change:tooltip', this.create_tooltip);
        this.listenTo(this.model, 'change:interactions', this.process_interactions);
        this.listenTo(this.model, 'change:selected', this.update_selected);
        this.listenTo(this.model, 'change:stroke', this.updateStroke);
        this.listenTo(this.model, 'change:stroke_width', this.updateStrokeWidth);
        this.listenTo(this.model, 'change:colors', this.updateColors);
        this.listenTo(this.model, 'change:opacities', this.updateOpacities);
        this.listenTo(this.model, 'change:marker', this.updateMarker);
        this.listenTo(this.model, 'format_updated', this.draw);
        this.listenTo(this.model, 'data_updated', this.draw);
        this.listenTo(this.parent, 'bg_clicked', () => {
            this.event_dispatcher('parent_clicked');
        });
    }
    /**
     * Sets event listeners depending on the interaction typr
     * @param interaction - string representing the interaction
     */
    process_click(interaction) {
        super.process_click(interaction);
        if (interaction === 'select') {
            this.event_listeners.parent_clicked = this.reset_selection;
            this.event_listeners.element_clicked = this.ohlc_click_handler;
        }
    }
    /**
     * Resets model state for selected indices
     */
    reset_selection() {
        this.model.set('selected', null);
        this.selected_indices = null;
        this.touch();
    }
    /**
     * Updates model state with selected indices
     * @param model - model object
     * @param value - array of indices
     */
    update_selected(model, value) {
        this.selected_indices = value;
        this.apply_styles();
    }
    /**
     * Updates model state with selected indices (adapted from Bars.ts)
     * @param args - data object of {x, y, index}
     */
    ohlc_click_handler(args) {
        const index = args.index;
        const idx = this.model.get('selected') || [];
        let selected = Array.from(idx);
        // index of candle i. Checking if it is already present in the list.
        const elem_index = selected.indexOf(index);
        // Replacement for "Accel" modifier.
        const accelKey = utils_1.d3GetEvent().ctrlKey || utils_1.d3GetEvent().metaKey;
        if (elem_index > -1 && accelKey) {
            // if the index is already selected and if accel key is
            // pressed, remove the element from the list
            selected.splice(elem_index, 1);
        }
        else {
            if (utils_1.d3GetEvent().shiftKey) {
                //If shift is pressed and the element is already
                //selected, do not do anything
                if (elem_index > -1) {
                    return;
                }
                //Add elements before or after the index of the current
                //candle which has been clicked
                const min_index = selected.length !== 0 ? d3.min(selected) : -1;
                const max_index = selected.length !== 0
                    ? d3.max(selected)
                    : this.model.mark_data.length;
                if (index > max_index) {
                    _.range(max_index + 1, index + 1).forEach((i) => {
                        selected.push(i);
                    });
                }
                else if (index < min_index) {
                    _.range(index, min_index).forEach((i) => {
                        selected.push(i);
                    });
                }
            }
            else if (accelKey) {
                //If accel is pressed and the candle is not already selcted
                //add the candle to the list of selected candles.
                selected.push(index);
            }
            // updating the array containing the candle indexes selected
            // and updating the style
            else {
                //if accel is not pressed, then clear the selected ones
                //and set the current element to the selected
                selected = [];
                selected.push(index);
            }
        }
        this.model.set('selected', selected.length === 0 ? null : selected, {
            updated_view: this,
        });
        this.touch();
        const e = utils_1.d3GetEvent();
        if (e.cancelBubble !== undefined) {
            // IE
            e.cancelBubble = true;
        }
        if (e.stopPropagation) {
            e.stopPropagation();
        }
        e.preventDefault();
    }
    updateStroke() {
        const stroke = this.model.get('stroke');
        this.d3el.selectAll('.stick').style('stroke', stroke);
        if (this.legendEl) {
            this.legendEl.selectAll('path').style('stroke', stroke);
            this.legendEl.selectAll('text').style('fill', stroke);
        }
    }
    updateStrokeWidth() {
        const stroke_width = this.model.get('stroke_width');
        this.d3el.selectAll('.stick').attr('stroke-width', stroke_width);
    }
    updateColors() {
        const colors = this.model.get('colors');
        const up_color = colors[0] ? colors[0] : 'none';
        const down_color = colors[1] ? colors[1] : 'none';
        // Fill candles based on the opening and closing values
        this.d3el.selectAll('.stick').style('fill', (d) => {
            return d.y[this.model.px.o] > d.y[this.model.px.c]
                ? down_color
                : up_color;
        });
        if (this.legendEl) {
            this.legendEl.selectAll('path').style('fill', up_color);
        }
    }
    updateOpacities() {
        const opacities = this.model.get('opacities');
        this.d3el.selectAll('.stick').style('opacity', (d, i) => {
            return opacities[i];
        });
        if (this.legendEl) {
            this.legendEl.selectAll('path').attr('opacity', (d, i) => {
                return opacities[i];
            });
        }
    }
    updateMarker() {
        const marker = this.model.get('marker');
        if (this.legendEl && this.rectDim) {
            this.draw_legend_icon(this.rectDim, this.legendEl);
        }
        // Redraw existing marks
        this.drawMarkPaths(marker, this.d3el, this.model.mark_data.map((d, index) => {
            return d[1];
        }));
    }
    updateSelectedColors(idx_start, idx_end) {
        let current_range = _.range(idx_start, idx_end + 1);
        if (current_range.length == this.model.mark_data.length) {
            current_range = [];
        }
        const stroke = this.model.get('stroke');
        const colors = this.model.get('colors');
        const up_color = colors[0] ? colors[0] : stroke;
        const down_color = colors[1] ? colors[1] : stroke;
        const px = this.model.px;
        _.range(0, this.model.mark_data.length).forEach((d) => {
            this.d3el.selectAll('#stick' + d).style('stroke', stroke);
        });
        current_range.forEach((d) => {
            this.d3el.selectAll('#stick' + d).style('stroke', (d) => {
                return d[px.o] > d[px.c] ? down_color : up_color;
            });
        });
    }
    invert_range(start_pxl, end_pxl) {
        if (start_pxl === undefined ||
            end_pxl === undefined ||
            this.model.mark_data.length === 0) {
            this.updateSelectedColors(-1, -1);
            return [];
        }
        const indices = new Uint32Array(_.range(this.model.mark_data.length));
        const selected = indices.filter((index) => {
            const elem = this.xPixels[index];
            return elem >= start_pxl && elem <= end_pxl;
        });
        let idx_start = -1;
        let idx_end = -1;
        if (selected.length > 0 &&
            (start_pxl !== this.scales.x.scale.range()[0] ||
                end_pxl !== this.scales.x.scale.range()[1])) {
            idx_start = selected[0];
            idx_end = selected[selected.length - 1];
        }
        this.updateSelectedColors(idx_start, idx_end);
        this.model.set('selected', selected);
        this.touch();
        return super.invert_range(start_pxl, end_pxl);
    }
    invert_point(pixel) {
        const abs_diff = this.xPixels.map((elem) => {
            return Math.abs(elem - pixel);
        });
        const sel_index = abs_diff.indexOf(d3.min(abs_diff));
        this.updateSelectedColors(sel_index, sel_index);
        this.model.set('selected', [sel_index]);
        this.touch();
        return;
    }
    draw() {
        const x_scale = this.scales.x, y_scale = this.scales.y;
        this.set_ranges();
        const colors = this.model.get('colors');
        const opacities = this.model.get('opacities');
        const up_color = colors[0] ? colors[0] : 'none';
        const down_color = colors[1] ? colors[1] : 'none';
        const px = this.model.px;
        const stick = this.d3el.selectAll('.stick').data(this.model.mark_data.map((data, index) => {
            return {
                x: data[0],
                y: data[1],
                index: index,
            };
        }));
        // Create new
        const new_sticks = stick
            .enter()
            .append('g')
            .attr('class', 'stick')
            .attr('id', (d, i) => {
            return 'stick' + i;
        })
            .style('stroke', this.model.get('stroke'))
            .style('opacity', (d, i) => {
            return opacities[i];
        });
        new_sticks.append('path').attr('class', 'stick_head');
        new_sticks.append('path').attr('class', 'stick_tail');
        new_sticks.append('path').attr('class', 'stick_body');
        stick.exit().remove();
        // Determine offset to use for translation
        let y_index = px.h;
        if (px.h === -1) {
            y_index = px.o;
        }
        // Update all of the marks
        this.d3el
            .selectAll('.stick')
            .style('fill', (d, i) => {
            return d.y[px.o] > d.y[px.c] ? down_color : up_color;
        })
            .attr('stroke-width', this.model.get('stroke_width'));
        if (x_scale.model.type === 'ordinal') {
            // If we are out of range, we just set the mark in the final
            // bucket's range band. FIXME?
            const x_max = d3.max(this.parent.range('x'));
            this.d3el.selectAll('.stick').attr('transform', (d, i) => {
                return ('translate(' +
                    ((x_scale.scale(this.model.mark_data[i][0]) !== undefined
                        ? x_scale.scale(this.model.mark_data[i][0])
                        : x_max) +
                        x_scale.scale.bandwidth() / 2) +
                    ',' +
                    (y_scale.scale(d.y[y_index]) + y_scale.offset) +
                    ')');
            });
        }
        else {
            this.d3el.selectAll('.stick').attr('transform', (d, i) => {
                return ('translate(' +
                    (x_scale.scale(this.model.mark_data[i][0]) + x_scale.offset) +
                    ',' +
                    (y_scale.scale(d.y[y_index]) + y_scale.offset) +
                    ')');
            });
        }
        this.d3el.selectAll('.stick').on('click', (d, i) => {
            return this.event_dispatcher('element_clicked', { data: d, index: i });
        });
        // Draw the mark paths
        this.drawMarkPaths(this.model.get('marker'), this.d3el, this.model.mark_data.map((d) => {
            return d[1];
        }));
        this.xPixels = this.model.mark_data.map((el) => {
            return x_scale.scale(el[0]) + x_scale.offset;
        });
    }
    drawMarkPaths(type, selector, dat) {
        /* Calculate some values so that we can draw the marks
         *      | <----- high (0,0)
         *      |
         *  --------- <- headline_top (open or close)
         *  |       |
         *  |       |
         *  |       |
         *  --------- <- headline_bottom (open or close)
         *      |
         *      | <----- low
         *
         *
         *      | <----- high (0,0)
         *  ____| <----- open
         *      |
         *      |
         *      |
         *      |____ <- close
         *      | <----- low
         */
        const px = this.model.px;
        const open = [];
        const high = [];
        const low = [];
        const close = [];
        const headline_top = [];
        const headline_bottom = [];
        const to_left_side = [];
        const scaled_mark_widths = [];
        let min_x_difference = this.calculateMarkWidth();
        const x_scale = this.scales.x;
        const y_scale = this.scales.y;
        let offset_in_x_units;
        let data_point;
        for (let i = 0; i < dat.length; i++) {
            if (px.o === -1) {
                open[i] = undefined;
            }
            else {
                open[i] = y_scale.scale(dat[i][px.o]);
            }
            if (px.c === -1) {
                close[i] = undefined;
            }
            else {
                close[i] = y_scale.scale(dat[i][px.c]);
            }
            // We can only compute these (and only need to compute these)
            // when we have both the open and the close values
            if (px.o !== -1 && px.c !== -1) {
                headline_top[i] = dat[i][px.o] > dat[i][px.c] ? open[i] : close[i];
                headline_bottom[i] = dat[i][px.o] < dat[i][px.c] ? open[i] : close[i];
            }
            // We never have high without low and vice versa, so we can
            // check everything at once
            if (px.h === -1 || px.l === -1) {
                high[i] = open[i];
                low[i] = close[i];
            }
            else {
                high[i] = y_scale.scale(dat[i][px.h]);
                low[i] = y_scale.scale(dat[i][px.l]);
            }
            data_point = this.model.mark_data[i][0];
            // Check for dates so that we don't concatenate
            if (min_x_difference instanceof Date) {
                min_x_difference = min_x_difference.getTime();
            }
            if (data_point instanceof Date) {
                data_point = data_point.getTime();
            }
            offset_in_x_units = data_point + min_x_difference;
            if (x_scale.model.type === 'ordinal') {
                scaled_mark_widths[i] = x_scale.scale.bandwidth() * 0.75;
            }
            else {
                scaled_mark_widths[i] =
                    (x_scale.scale(offset_in_x_units) - x_scale.scale(data_point)) * 0.75;
            }
            to_left_side[i] = (-1 * scaled_mark_widths[i]) / 2;
        }
        // Determine OHLC marker type
        // Note: if we do not have open or close data then we have to draw
        // a bar.
        if (type == 'candle' && px.o !== -1 && px.c !== -1) {
            /*
             *      | <-------- head
             *  ---------
             *  |       |
             *  |       | <---- body
             *  |       |
             *  ---------
             *      | <-------- tail
             */
            if (px.h !== -1 || px.l !== -1) {
                selector.selectAll('.stick_head').attr('d', (d, i) => {
                    return this.headPathCandle(headline_top[i] - high[i]);
                });
                selector.selectAll('.stick_tail').attr('d', (d, i) => {
                    return this.tailPathCandle(headline_bottom[i] - high[i], low[i] - headline_bottom[i]);
                });
            }
            else {
                selector.selectAll('.stick_head').attr('d', '');
                selector.selectAll('.stick_tail').attr('d', '');
            }
            selector.selectAll('.stick_body').attr('d', (d, i) => {
                return this.bodyPathCandle(to_left_side[i], open[i] - high[i], scaled_mark_widths[i], close[i] - open[i]);
            });
        }
        else {
            // bar
            /*
             *      |
             *  ____| <-------- head (horizontal piece)
             *      |
             *      | <-------- body (vertical piece)
             *      |
             *      |____ <---- tail (horizontal piece)
             *      |
             */
            if (px.o !== -1) {
                selector.selectAll('.stick_head').attr('d', (d, i) => {
                    return this.headPathBar(to_left_side[i], open[i] - high[i], to_left_side[i] * -1);
                });
            }
            else {
                selector.selectAll('.stick_head').attr('d', '');
            }
            if (px.c !== -1) {
                selector.selectAll('.stick_tail').attr('d', (d, i) => {
                    return this.tailPathBar(close[i] - high[i], to_left_side[i] * -1);
                });
            }
            else {
                selector.selectAll('.stick_tail').attr('d', '');
            }
            selector.selectAll('.stick_body').attr('d', (d, i) => {
                return this.bodyPathBar(low[i] - high[i]);
            });
        }
    }
    /* SVG path for head of candle */
    headPathCandle(height) {
        return 'm0,0 l0,' + height;
    }
    /* SVG path for tail of candle */
    tailPathCandle(yOffset, height) {
        return 'm0,' + yOffset + ' l0,' + height;
    }
    /* SVG path for body of candle */
    bodyPathCandle(xOffset, yOffset, width, height) {
        return ('m' +
            xOffset +
            ',' +
            yOffset +
            ' l' +
            width +
            ',0' +
            ' l0,' +
            height +
            ' l' +
            -1 * width +
            ',0' +
            ' z');
    }
    /* SVG path for head of bar */
    headPathBar(xOffset, yOffset, width) {
        return 'm' + xOffset + ',' + yOffset + ' l' + width + ',0';
    }
    /* SVG path for tail of bar */
    tailPathBar(yOffset, width) {
        return 'm0,' + yOffset + ' l' + width + ',0';
    }
    /* SVG path for body of bar */
    bodyPathBar(height) {
        return 'm0,0 l0,' + height;
    }
    calculateMarkWidth() {
        /*
         * Calculate the mark width for this data set based on the minimum
         * distance between consecutive points.
         */
        let min_distance = Number.POSITIVE_INFINITY;
        const scales = this.model.get('scales');
        for (let i = 1; i < this.model.mark_data.length; i++) {
            const dist = this.model.mark_data[i][0] - this.model.mark_data[i - 1][0];
            if (dist < min_distance) {
                min_distance = dist;
            }
        }
        // Check if there are less than two data points
        if (min_distance === Number.POSITIVE_INFINITY) {
            min_distance = (scales.x.domain[1] - scales.x.domain[0]) / 2;
        }
        if (min_distance < 0) {
            min_distance = -1 * min_distance;
        }
        return min_distance;
    }
    relayout() {
        this.set_ranges();
        this.d3el
            .select('.intselmouse')
            .attr('width', this.width)
            .attr('height', this.height);
        // We have to redraw every time that we relayout
        this.draw();
    }
    draw_legend(elem, x_disp, y_disp, inter_x_disp, inter_y_disp) {
        const stroke = this.model.get('stroke');
        const colors = this.model.get('colors');
        const up_color = colors[0] ? colors[0] : 'none';
        this.rectDim = inter_y_disp * 0.8;
        this.legendEl = elem
            .selectAll('#legend' + this.uuid)
            .data([this.model.mark_data]);
        const leg = this.legendEl
            .enter()
            .append('g')
            .attr('transform', (d, i) => {
            return 'translate(0, ' + (i * inter_y_disp + y_disp) + ')';
        })
            .attr('class', 'legend')
            .attr('id', 'legend' + this.uuid)
            .style('fill', up_color)
            .on('mouseover', _.bind(this.highlight_axes, this))
            .on('mouseout', _.bind(this.unhighlight_axes, this));
        leg.append('path').attr('class', 'stick_head stick');
        leg.append('path').attr('class', 'stick_tail stick');
        leg
            .append('path')
            .attr('class', 'stick_body stick')
            .style('fill', up_color);
        // Add stroke color and set position
        leg
            .selectAll('path')
            .style('stroke', stroke)
            .attr('transform', 'translate(' + this.rectDim / 2 + ',0)');
        // Draw icon and text
        this.draw_legend_icon(this.rectDim, leg);
        this.legendEl
            .append('text')
            .attr('class', 'legendtext sticktext')
            .attr('x', this.rectDim * 1.2)
            .attr('y', this.rectDim / 2)
            .attr('dy', '0.35em')
            .text((d, i) => {
            return this.model.get('labels')[i];
        })
            .style('fill', stroke);
        const max_length = d3.max(this.model.get('labels'), (d) => {
            return Number(d.length);
        });
        this.legendEl.exit().remove();
        return [1, max_length];
    }
    draw_legend_icon(size, selector) {
        /*
         * Draw OHLC icon next to legend text
         * Drawing the icon like this means we can avoid scaling when we
         * already know what the size of the mark is in pixels
         */
        const height = size;
        const width = size / 2;
        const bottom_y_offset = (size * 3) / 4;
        const top_y_offset = size / 4;
        if (this.model.get('marker') === 'candle') {
            selector
                .selectAll('.stick_head')
                .attr('d', this.headPathCandle(width / 2));
            selector
                .selectAll('.stick_tail')
                .attr('d', this.tailPathCandle(bottom_y_offset, width / 2));
            selector
                .selectAll('.stick_body')
                .attr('d', this.bodyPathCandle((width * -1) / 2, top_y_offset, width, height / 2));
        }
        else {
            // bar
            selector
                .selectAll('.stick_head')
                .attr('d', this.headPathBar((width * -1) / 2, bottom_y_offset, width / 2));
            selector
                .selectAll('.stick_tail')
                .attr('d', this.tailPathBar(top_y_offset, width / 2));
            selector.selectAll('.stick_body').attr('d', this.bodyPathBar(height));
        }
    }
    clear_style() { }
    compute_view_padding() { }
    set_default_style() { }
    set_style_on_elements() { }
}
exports.OHLC = OHLC;
//# sourceMappingURL=OHLC.js.map