"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MarketMap = void 0;
const widgets = __importStar(require("@jupyter-widgets/base"));
const _ = __importStar(require("underscore"));
const messaging_1 = require("@lumino/messaging");
const widgets_1 = require("@lumino/widgets");
const d3 = __importStar(require("d3"));
// var d3 =Object.assign({}, require("d3-array"), require("d3-format"), require("d3-selection"), require("d3-selection-multi"), require("d3-shape"));
const Figure_1 = require("./Figure");
const popperreference = __importStar(require("./PopperReference"));
const popper_js_1 = __importDefault(require("popper.js"));
const utils_1 = require("./utils");
class MarketMap extends Figure_1.Figure {
    renderImpl() {
        const figureSize = this.getFigureSize();
        this.width = figureSize.width;
        this.height = figureSize.height;
        this.id = widgets.uuid();
        this.scales = {};
        this.set_top_el_style();
        this.margin = this.model.get('map_margin');
        this.num_rows = this.model.get('rows');
        this.num_cols = this.model.get('cols');
        this.row_groups = this.model.get('row_groups');
        this.enable_select = this.model.get('enable_select');
        this.update_data();
        // set the number of rows and columns in the map
        this.set_area_dimensions(this.data.length);
        if (this.model.get('theme')) {
            this.svg.classed(this.model.get('theme'), true);
        }
        this.fig = this.svg
            .append('g')
            .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
        this.fig_map = this.fig.append('g');
        this.fig_axes = this.fig.append('g');
        this.fig_click = this.fig.append('g');
        this.fig_hover = this.fig.append('g');
        this.fig_names = this.fig
            .append('g')
            .style('display', this.model.get('show_groups') ? 'inline' : 'none');
        this.fig_map.classed('g-map', true);
        this.fig_axes.classed('g-axes', true);
        this.fig_click.classed('g-click', true);
        this.fig_hover.classed('g-hover', true);
        this.fig_names.classed('g-names', true);
        // code for tool tip to be displayed
        this.tooltip_div = d3
            .select(document.createElement('div'))
            .attr('class', 'mark_tooltip');
        utils_1.applyStyles(this.tooltip_div, { opacity: 0, 'pointer-events': 'none' });
        const freeze_tooltip_loc = this.model.get('freeze_tooltip_location');
        if (freeze_tooltip_loc) {
            this.popper_reference = new popperreference.ElementReference(this.svg.node());
        }
        else {
            this.popper_reference = new popperreference.PositionReference({
                x: 0,
                y: 0,
                width: 20,
                height: 20,
            });
        }
        this.popper = new popper_js_1.default(this.popper_reference, this.tooltip_div.node(), {
            placement: 'auto',
        });
        this.update_default_tooltip();
        this.selected_stroke = this.model.get('selected_stroke');
        this.hovered_stroke = this.model.get('hovered_stroke');
        this.update_plotarea_dimensions();
        this.title = this.fig
            .append('text')
            .attr('class', 'mainheading')
            .text(this.model.get('title'));
        utils_1.applyAttrs(this.title, {
            x: 0.5 * this.plotarea_width,
            y: -(this.margin.top / 2.0),
            dy: '1em',
        });
        utils_1.applyStyles(this.title, this.model.get('title_style'));
        return this.create_scale_views().then(() => {
            this.create_listeners();
            this.axis_views = new widgets.ViewList(this.add_axis, null, this);
            const axis_views_updated = this.axis_views.update(this.model.get('axes'));
            this.model.on('change:axes', (model, value, options) => {
                this.axis_views.update(value);
            });
            this.draw_map();
            document.body.appendChild(this.tooltip_div.node());
            this.create_tooltip_widget();
            // In the classic notebook, we should relayout the figure on
            // resize of the main window.
            window.addEventListener('resize', this.debouncedRelayout);
            this.once('remove', () => {
                window.removeEventListener('resize', this.debouncedRelayout);
            });
            return Promise.all([Promise.resolve(), axis_views_updated]);
        });
    }
    set_top_el_style() {
        this.el.style['user-select'] = 'none';
        this.el.style['ms-user-select'] = 'none';
        this.el.style['moz-user-select'] = 'none';
        this.el.style['khtml-user-select'] = 'none';
        this.el.style['webkit-user-select'] = 'none';
    }
    update_plotarea_dimensions() {
        this.plotarea_width = this.width - this.margin.left - this.margin.right;
        this.plotarea_height = this.height - this.margin.top - this.margin.bottom;
        this.column_width = parseFloat((this.plotarea_width / this.num_cols).toFixed(2));
        this.row_height = parseFloat((this.plotarea_height / this.num_rows).toFixed(2));
    }
    reset_drawing_controls() {
        // Properties useful in drawing the map
        this.prev_x = 0;
        this.prev_y = -1;
        this.y_direction = 1; // for y direction 1 means going to the right
        this.x_direction = 1; // for x direction 1 means going down
        this.group_iter = 1;
    }
    create_listeners() {
        this.listenTo(this.model, 'change:scales', this.create_scale_views);
        this.listenTo(this.model, 'change:color', this.recolor_chart);
        this.listenTo(this.model, 'change:colors', this.colors_updated);
        this.listenTo(this.model, 'change:show_groups', this.show_groups);
        this.listenTo(this.model, 'change:selected_stroke', this.update_selected_stroke);
        this.listenTo(this.model, 'change:hovered_stroke', this.update_hovered_stroke);
        this.listenTo(this.model, 'change:font_style', this.update_font_style);
        this.model.on_some_change(['title', 'title_style'], this.update_title, this);
        this.listenTo(this.model, 'change:selected', function () {
            this.clear_selected();
            this.apply_selected();
        });
        this.model.on_some_change(['names', 'groups', 'ref_data'], function () {
            this.update_data();
            this.compute_dimensions_and_draw();
        }, this);
        this.listenTo(this.model, 'change:rows', function (model, value) {
            this.num_rows = value;
            this.compute_dimensions_and_draw();
        });
        this.listenTo(this.model, 'change:cols', function (model, value) {
            this.num_cols = value;
            this.compute_dimensions_and_draw();
        });
        this.listenTo(this.model, 'change:row_groups', function (model, value) {
            this.row_groups = value;
            this.compute_dimensions_and_draw();
        });
        this.listenTo(this.model, 'change:tooltip_widget', this.create_tooltip_widget);
        this.listenTo(this.model, 'change:tooltip_fields', this.update_default_tooltip);
        this.listenTo(this.model, 'change:tooltip_formats', this.update_default_tooltip);
    }
    update_title(model, value) {
        this.title.text(this.model.get('title'));
        utils_1.applyStyles(this.title, this.model.get('title_style'));
    }
    relayout() {
        const that = this;
        const figureSize = this.getFigureSize();
        this.width = figureSize.width;
        this.height = figureSize.height;
        window.requestAnimationFrame(() => {
            // update ranges
            that.margin = that.model.get('map_margin');
            that.update_plotarea_dimensions();
            // transform figure
            that.fig.attr('transform', 'translate(' + that.margin.left + ',' + that.margin.top + ')');
            utils_1.applyAttrs(that.title, {
                x: 0.5 * that.plotarea_width,
                y: -(that.margin.top / 2.0),
                dy: '1em',
            });
            that.draw_map();
            // Drawing the selected cells
            that.clear_selected();
            that.apply_selected();
            // When map is expanded or contracted, there should not be any
            // accidental hovers. To prevent this, the following call is made.
            that.fig_hover.selectAll('rect').remove();
            that.hide_tooltip();
            that.trigger('margin_updated');
        });
    }
    update_data() {
        const that = this;
        this.model.set('selected', []);
        this.data = this.model.get('names') || [];
        this.ref_data = this.model.get('ref_data');
        this.group_data = this.model.get('groups');
        this.groups = _.uniq(this.group_data, true);
        let display_text = this.model.get('display_text');
        display_text =
            display_text === null || display_text.length === 0
                ? this.data
                : display_text;
        this.colors = this.model.get('colors');
        const num_colors = this.colors.length;
        this.colors_map = function (d) {
            return that.get_color(d, num_colors);
        };
        const color_data = this.model.get('color');
        const mapped_data = _.map(this.data, (d, i) => {
            return {
                display: display_text[i],
                name: d,
                color: Number.isNaN(color_data[i]) ? undefined : color_data[i],
                group: this.group_data[i],
                ref_data: this.ref_data === null || this.ref_data === undefined
                    ? null
                    : this.ref_data[i],
            };
        });
        this.update_domains();
        this.grouped_data = _.groupBy(mapped_data, (d, i) => {
            return that.group_data[i];
        });
        this.groups = [];
        this.running_sums = [];
        this.running_sums[0] = 0;
        let count = 0;
        for (const key in this.grouped_data) {
            this.groups.push(key);
            count += this.grouped_data[key].length;
            this.running_sums.push(count);
        }
        this.running_sums.pop();
    }
    update_domains() {
        const color_scale_model = this.model.get('scales').color;
        const color_data = this.model.get('color');
        if (color_scale_model && color_data.length > 0) {
            color_scale_model.compute_and_set_domain(color_data, this.model.model_id);
        }
    }
    set_area_dimensions(num_items) {
        this.num_rows = this.model.get('rows');
        this.num_cols = this.model.get('cols');
        this.row_groups = this.model.get('row_groups');
        if (this.num_cols !== undefined &&
            this.num_cols !== null &&
            this.num_cols !== 0) {
            // When the number of row groups is greater than 1, the number
            // of columns has to be an odd number. This is to
            // ensure the continuity of the waffles when groups are spread
            // across multiple row groups
            if (this.row_groups > 1 && this.num_cols % 2 === 0) {
                this.num_cols++;
            }
            this.num_rows = Math.floor(num_items / this.num_cols);
            this.num_rows =
                num_items % this.num_cols === 0 ? this.num_rows : this.num_rows + 1;
        }
        else if (this.num_rows !== undefined &&
            this.num_rows !== null &&
            this.num_rows !== 0) {
            this.num_cols = Math.floor(num_items / this.num_rows);
            this.num_cols =
                num_items % this.num_rows === 0 ? this.num_cols : this.num_cols + 1;
            if (this.row_groups > 1 && this.num_cols % 2 === 0) {
                this.num_cols++;
            }
        }
        else {
            this.num_cols = Math.floor(Math.sqrt(num_items));
            if (this.row_groups > 1 && this.num_cols % 2 === 0) {
                this.num_cols++;
            }
            this.num_rows = Math.floor(num_items / this.num_cols);
            this.num_rows =
                num_items % this.num_cols === 0 ? this.num_rows : this.num_rows + 1;
        }
        // row_groups cannot be greater than the number of rows
        this.row_groups = Math.min(this.row_groups, this.num_rows);
        // if there is only one row_group, then the number of columns are
        // not necessarily equal to the variable this.num_cols as we draw
        // row first. So we need to adjust the this.num_cols variable
        // according to the num_rows.
        if (this.row_groups == 1) {
            this.num_cols = Math.floor(num_items / this.num_rows);
            this.num_cols =
                num_items % this.num_rows === 0 ? this.num_cols : this.num_cols + 1;
        }
        // depending on the number of rows, we need to decide when to
        // switch direction. The below functions tells us where to swtich
        // direction.
        this.set_row_limits();
    }
    compute_dimensions_and_draw() {
        this.set_area_dimensions(this.data.length);
        this.update_plotarea_dimensions();
        this.draw_map();
        this.clear_selected();
        this.apply_selected();
        // when data is changed
        this.fig_hover.selectAll('rect').remove();
        this.hide_tooltip();
    }
    update_default_tooltip() {
        this.tooltip_fields = this.model.get('tooltip_fields');
        const formats = this.model.get('tooltip_formats');
        this.tooltip_formats = this.tooltip_fields.map((field, index) => {
            const fmt = formats[index];
            if (fmt === undefined || fmt === '') {
                return (d) => d;
            }
            else {
                return d3.format(fmt);
            }
        });
    }
    create_scale_views() {
        for (const key in this.scales) {
            this.stopListening(this.scales[key]);
        }
        const scale_models = this.model.get('scales');
        const that = this;
        const scale_promises = {};
        _.each(scale_models, (model, key) => {
            scale_promises[key] = that.create_child_view(model);
        });
        return widgets
            .resolvePromisesDict(scale_promises)
            .then((d) => {
            that.scales = d;
            that.set_scales();
        });
    }
    set_scales() {
        const that = this;
        const color_scale = this.scales.color;
        if (color_scale) {
            color_scale.set_range();
            color_scale.on('color_scale_range_changed', that.update_map_colors, that);
            this.update_domains();
            this.listenTo(color_scale, 'domain_changed', () => {
                that.update_map_colors();
            });
            this.update_map_colors();
        }
    }
    show_groups(model, value) {
        this.fig_names.style('display', value ? 'inline' : 'none');
        this.fig_map
            .selectAll('.market_map_text')
            .style('opacity', value ? 0.2 : 1);
        this.fig_map
            .selectAll('.market_map_rect')
            .style('stroke-opacity', value ? 0.2 : 1);
    }
    draw_map() {
        this.reset_drawing_controls();
        // Removing pre existing elements from the map
        this.fig_map.selectAll('.element_group').remove();
        this.fig_names.selectAll('.names_object').remove();
        this.rect_groups = this.fig_map
            .selectAll('.element_group')
            .data(this.groups);
        const color_scale = this.scales.color;
        const that = this;
        this.rect_groups = this.rect_groups
            .enter()
            .append('g')
            .attr('class', 'element_group')
            .attr('transform', (d, i) => {
            return that.get_group_transform(i);
        })
            .merge(this.rect_groups);
        this.rect_groups.exit().remove();
        this.end_points = [];
        this.rect_groups.nodes().forEach((d, i) => {
            const data = that.grouped_data[that.groups[i]];
            const return_arr = that.get_new_cords();
            const ends = that.get_end_points(return_arr[2], data.length, return_arr[0], return_arr[1], return_arr[3], return_arr[4]);
            ends.forEach((point) => {
                that.end_points.push(point);
            });
            const element_count = that.running_sums[i];
            let groups = d3
                .select(d)
                .selectAll('.rect_element')
                .data(data);
            // Appending the <g> <rect> and <text> elements to the newly
            // added nodes
            const new_groups = groups
                .enter()
                .append('g')
                .classed('rect_element', true);
            new_groups
                .append('rect')
                .attr('x', 0)
                .attr('y', 0)
                .classed('market_map_rect', true);
            // Grouping calls to style into a single call to styles
            // leads to build error despite the import of d3-selection-multi
            new_groups
                .append('text')
                .classed('market_map_text', true)
                .style('text-anchor', 'middle')
                .style('pointer-events', 'none')
                .style('dominant-baseline', 'central');
            groups = new_groups.merge(groups);
            // Update the attributes of the entire set of nodes
            groups
                .attr('transform', (data, ind) => {
                return that.get_cell_transform(ind);
            })
                .on('click', function (data, ind) {
                that.cell_click_handler(data, element_count + ind, this);
            })
                .on('mouseover', function (data, ind) {
                that.mouseover_handler(data, element_count + ind, this);
            })
                .on('mousemove', (data) => {
                that.mousemove_handler();
            })
                .on('mouseout', function (data, ind) {
                that.mouseout_handler(data, element_count + ind, this);
            })
                .attr('class', function (data, index) {
                return (d3.select(this).attr('class') +
                    ' ' +
                    'rect_' +
                    (element_count + index));
            });
            groups
                .selectAll('.market_map_rect')
                .attr('width', that.column_width)
                .attr('height', that.row_height)
                .style('stroke-opacity', that.model.get('show_groups') ? 0.2 : 1.0)
                .style('stroke', that.model.get('stroke'))
                .style('fill', (elem, j) => {
                return color_scale && elem.color !== undefined && elem.color !== null
                    ? color_scale.scale(elem.color)
                    : that.colors_map(i);
            });
            groups
                .selectAll('.market_map_text')
                .attr('x', that.column_width / 2.0)
                .attr('y', that.row_height / 2.0)
                .text((data, j) => {
                return data.display;
            })
                .style('opacity', that.model.get('show_groups') ? 0.2 : 1.0);
            // Removing the old nodes
            groups.exit().remove();
            that.create_bounding_path(d, ends);
            const min_x = d3.min(ends, (end_point) => {
                return end_point.x;
            });
            const min_y = d3.min(ends, (end_point) => {
                return end_point.y;
            });
            const obj = that.fig_names
                .append('foreignObject')
                .attr('class', 'names_object')
                .attr('x', min_x)
                .attr('y', min_y)
                .append('xhtml:div')
                .attr('class', 'names_div')
                .text(that.groups[i]);
            utils_1.applyStyles(obj, {
                display: 'flex',
                'flex-direction': 'row',
                'align-content': 'center',
                'align-items': 'center',
                width: '100%',
                height: '100%',
                'justify-content': 'center',
                'word-wrap': 'break-word',
                font: '24px sans-serif',
                color: 'black',
            });
        });
        this.update_font_style();
        this.draw_group_names();
    }
    draw_group_names() {
        // Get all the bounding rects of the paths around each of the
        // sectors. Get their client bounding rect.
        const paths = this.svg
            .selectAll('.bounding_path')
            .nodes();
        const clientRects = paths.map((path) => {
            return path.getBoundingClientRect();
        });
        const text_elements = this.fig_names
            .selectAll('.names_object')
            .data(clientRects);
        text_elements
            .attr('width', (d) => {
            return d.width;
        })
            .attr('height', (d) => {
            return d.height;
        });
    }
    recolor_chart() {
        const that = this;
        this.update_data();
        this.rect_groups = this.fig.selectAll('.element_group').data(this.groups);
        const color_scale = this.scales.color;
        this.rect_groups.nodes().forEach((d, i) => {
            const data = that.grouped_data[that.groups[i]];
            d3.select(d)
                .selectAll('.rect_element')
                .data(data)
                .select('rect')
                .style('stroke', that.model.get('stroke'))
                .style('fill', (elem, j) => {
                return color_scale && elem.color !== undefined && elem.color !== null
                    ? color_scale.scale(elem.color)
                    : that.colors_map(i);
            });
        });
    }
    update_font_style() {
        // This is a bit awkward because we did not figure out how to get
        // Typescript to recognize the d3-select-multi typings.
        const x = this.svg.selectAll('.market_map_text');
        utils_1.applyStyles(x, this.model.get('font_style'));
    }
    update_map_colors() {
        if (this.rect_groups !== undefined && this.rect_groups !== null) {
            this.recolor_chart();
        }
    }
    cell_click_handler(data, id, cell) {
        if (this.model.get('enable_select')) {
            const selected = Object.assign(this.model.get('selected').slice(), this.model.get('selected'));
            const index = selected.indexOf(data.name);
            if (index == -1) {
                // not already selected, so add to selected
                selected.push(data.name);
            }
            else {
                // already in selected list, so delete from selected
                selected.splice(index, 1);
            }
            this.model.set('selected', selected);
            this.touch();
        }
    }
    apply_selected() {
        const selected = this.model.get('selected');
        const that = this;
        if (selected === undefined || selected === null || selected.length === 0) {
            this.clear_selected();
        }
        else {
            selected.forEach((data) => {
                const selected_cell = that.fig_map
                    .selectAll('.rect_element')
                    .filter((d, i) => {
                    return d.name === data;
                });
                const rect = that.fig_click
                    .append('rect')
                    .data(selected_cell.data())
                    .attr('transform', selected_cell.attr('transform'))
                    .attr('x', 0)
                    .attr('y', 0)
                    .attr('width', that.column_width)
                    .attr('height', that.row_height);
                utils_1.applyStyles(rect, {
                    stroke: that.selected_stroke,
                    'stroke-width': '3px',
                    fill: 'none',
                });
            });
        }
    }
    clear_selected() {
        this.fig_click.selectAll('rect').remove();
    }
    mouseover_handler(data, id, cell) {
        const transform = d3.select(cell).attr('transform');
        if (this.model.get('enable_hover')) {
            const rect = this.fig_hover
                .append('rect')
                .attr('class', 'hover_' + id)
                .attr('transform', transform)
                .attr('x', 0)
                .attr('y', 0)
                .attr('width', this.column_width)
                .attr('height', this.row_height);
            utils_1.applyStyles(rect, {
                stroke: this.hovered_stroke,
                'stroke-width': '3px',
                fill: 'none',
                'pointer-events': 'none',
            });
            this.show_tooltip(d3.event, data);
            this.send({ event: 'hover', data: data.name, ref_data: data.ref_data });
        }
    }
    update_selected_stroke(model, value) {
        this.selected_stroke = value;
        this.fig_click.selectAll('rect').style('stroke', value);
    }
    update_hovered_stroke(model, value) {
        this.hovered_stroke = value;
        // I do not need to update anything else because when hovered color
        // is being updated you are not hovering over anything.
    }
    mouseout_handler(data, id, cell) {
        this.fig_hover.select('.hover_' + id).remove();
        this.hide_tooltip();
    }
    show_tooltip(event, data) {
        const that = this;
        if (!this.tooltip_view &&
            (!this.tooltip_fields || this.tooltip_fields.length == 0)) {
            return;
        }
        else {
            const tooltip_div = this.tooltip_div;
            tooltip_div.transition().style('opacity', 0.9).style('display', null);
            this.move_tooltip();
            tooltip_div.select('table').remove();
            const ref_data = data.ref_data;
            if (!this.tooltip_view) {
                const tooltip_table = tooltip_div
                    .append('table')
                    .selectAll('tr')
                    .data(this.tooltip_fields);
                tooltip_table.exit().remove();
                const table_rows = tooltip_table.enter().append('tr');
                table_rows
                    .append('td')
                    .attr('class', 'tooltiptext')
                    .text((datum) => {
                    return datum;
                });
                table_rows
                    .append('td')
                    .attr('class', 'tooltiptext')
                    .text((datum, index) => {
                    return ref_data === null || ref_data === undefined
                        ? null
                        : that.tooltip_formats[index](ref_data[datum]);
                });
            }
            this.popper.enableEventListeners();
            this.move_tooltip();
        }
    }
    mousemove_handler() {
        this.move_tooltip();
    }
    move_tooltip() {
        this.popper_reference.x = d3.event.clientX;
        this.popper_reference.y = d3.event.clientY;
        this.popper.scheduleUpdate();
    }
    hide_tooltip() {
        this.tooltip_div.style('pointer-events', 'none');
        this.tooltip_div.transition().style('opacity', 0).style('display', 'none');
        this.popper.disableEventListeners();
    }
    create_tooltip_widget() {
        const tooltip_model = this.model.get('tooltip_widget');
        if (this.tooltip_view !== null && this.tooltip_view !== undefined) {
            //remove the previous tooltip
            this.tooltip_view.remove();
            this.tooltip_view = null;
        }
        const that = this;
        if (tooltip_model) {
            const tooltip_widget_creation_promise = this.create_child_view(tooltip_model);
            tooltip_widget_creation_promise.then((view) => {
                that.tooltip_view = view;
                messaging_1.MessageLoop.sendMessage(view.pWidget, widgets_1.Widget.Msg.BeforeAttach);
                that.tooltip_div.node().appendChild(view.el);
                messaging_1.MessageLoop.sendMessage(view.pWidget, widgets_1.Widget.Msg.AfterAttach);
            });
        }
    }
    get_group_transform(index) {
        return 'translate(' + '0' + ', 0)';
    }
    get_cell_transform(index) {
        if (!this.past_border_y()) {
            if (this.past_border_x()) {
                this.y_direction = -1 * this.y_direction;
                this.prev_x += this.x_direction;
            }
            else {
                this.x_direction = -1 * this.x_direction;
                this.prev_y += this.y_direction;
                this.group_iter += 1;
            }
        }
        else {
            this.prev_y += this.y_direction;
        }
        return ('translate(' +
            this.prev_x * this.column_width +
            ', ' +
            this.prev_y * this.row_height +
            ')');
    }
    get_new_cords() {
        let new_x = this.prev_x;
        let new_y = this.prev_y;
        let y_direction = this.y_direction;
        let x_direction = this.x_direction;
        let group_iter = this.group_iter;
        if (!this.past_border_y()) {
            if (this.past_border_x()) {
                y_direction = -1 * this.y_direction;
                new_x += this.x_direction;
            }
            else {
                x_direction = -1 * this.x_direction;
                new_y += this.y_direction;
                group_iter += 1;
            }
        }
        else {
            new_y += this.y_direction;
        }
        return [
            new_x,
            new_y,
            group_iter,
            x_direction,
            y_direction,
            new_x * this.column_width,
            new_y * this.row_height,
        ];
    }
    past_border_y() {
        if (this.y_direction == 1) {
            return this.prev_y + 1 < this.row_limits[this.group_iter];
        }
        else {
            return this.prev_y - 1 > this.row_limits[this.group_iter - 1] - 1;
        }
    }
    past_border_x() {
        if (this.x_direction == 1) {
            return this.prev_x + 1 < this.num_cols;
        }
        else {
            return this.prev_x - 1 > -1;
        }
    }
    colors_updated() {
        this.colors = this.model.get('colors');
        this.recolor_chart();
    }
    get_color(index, length) {
        return this.colors[index % length];
    }
    set_row_limits() {
        const step = Math.floor(this.num_rows / this.row_groups);
        this.row_limits = [];
        for (let iter = this.row_groups - 1; iter > -1; iter--) {
            this.row_limits.unshift(iter * step);
        }
        this.row_limits[this.row_groups] = this.num_rows;
    }
    get_end_points(group_iter, num_cells, start_col, start_row, x_direction, y_direction) {
        //start_row is the 0-based index and not a 1-based index, i.e., it
        //is not the column number in the truest sense
        // Function to get the end points of the rectangle representing the
        // groups.
        // Requires the direction variables to be updated before this
        // function is called
        let top_row = this.row_limits[group_iter - 1];
        let bottom_row = this.row_limits[group_iter];
        let across = false;
        let init_x = x_direction;
        let init_y = y_direction;
        const end_points = [];
        let current_row;
        const rows_remaining = init_y == 1 ? bottom_row - start_row : start_row - top_row + 1;
        let cols_remaining = init_x == 1 ? this.num_cols - 1 - start_col : start_col; // this is the num of columns remaining
        //after the cuirrent column has been filled
        let elem_remaining = num_cells;
        //this holds the number of continuous cols that will be filled
        //between the current top and bottom rows
        let num_rows = bottom_row - top_row;
        if (elem_remaining !== 0) {
            // starting corener of the path
            this.calc_end_point_source(start_col, start_row, init_x, init_y).forEach((d) => {
                end_points.push(d);
            });
            const elem_filled = Math.min(rows_remaining, elem_remaining);
            if (elem_filled === elem_remaining) {
                // There are enough elements only to fill one column
                // partially. We add the three end points and exit
                // The adjacent corner from the starting corner. This is
                // required because the elements are filled in the first
                // row itself.
                this.calc_end_point_source(start_col, start_row, -1 * init_x, init_y).forEach((d) => {
                    end_points.push(d);
                });
                current_row = start_row + (elem_remaining - 1) * init_y;
                this.calc_end_point_dest(start_col, current_row, -1 * init_x, init_y).forEach((e) => {
                    end_points.push(e);
                });
                this.calc_end_point_dest(start_col, current_row, init_x, init_y).forEach((e) => {
                    end_points.push(e);
                });
                /*
                        console.log("new set");
                        end_points.forEach(function(point) { console.log(point); });
                        console.log("end set");
                       */
                return end_points;
            }
            elem_remaining = elem_remaining - elem_filled;
            if (cols_remaining === 0) {
                // Since this is the last column, the adjacent corner from
                // the starting corner is added here too
                this.calc_end_point_source(start_col, start_row, init_x * -1, init_y).forEach((d) => {
                    end_points.push(d);
                });
            }
            else if (rows_remaining !== bottom_row - top_row) {
                // If the starting row is not the starting row of a group,
                // the poirnt adjacent to the starting point needs to be
                // added.
                this.calc_end_point_source(start_col, start_row, init_x * -1, init_y).forEach((d) => {
                    end_points.push(d);
                });
                if (elem_remaining > num_rows) {
                    // If next row is completely filled, then the top row
                    // element of the next column is an end point. That is
                    // being added here.
                    this.calc_end_point_dest(start_col + init_x, init_y == 1 ? top_row : bottom_row - 1, init_x * -1, init_y * -1).forEach((d) => {
                        end_points.push(d);
                    });
                }
            }
            else if (elem_remaining < num_rows) {
                // one continuous row in this case
                this.calc_end_point_source(start_col, start_row, -1 * init_x, init_y).forEach((d) => {
                    end_points.push(d);
                });
            }
            start_row = start_row + init_y * (elem_filled - 1);
            //first set of end points are added here
            this.calc_end_point_dest(start_col, start_row, -1 * init_x, init_y).forEach((d) => {
                end_points.push(d);
            });
            if (elem_remaining === 0) {
                this.calc_end_point_dest(start_col, start_row, init_x, init_y).forEach((d) => {
                    end_points.push(d);
                });
                /*
                        console.log("new set");
                        end_points.forEach(function(point) { console.log(point); });
                        console.log("end set");
                       */
                return end_points;
            }
            if (cols_remaining !== 0 && elem_remaining > num_rows) {
                start_col = start_col + init_x;
            }
        }
        while (elem_remaining > num_rows) {
            let no_cont_cols;
            if (num_rows * cols_remaining < elem_remaining) {
                no_cont_cols = cols_remaining;
                const leftover_elem = elem_remaining - no_cont_cols * num_rows;
                no_cont_cols += Math.floor(leftover_elem /
                    (this.row_limits[group_iter + 1] - this.row_limits[group_iter]));
            }
            else {
                no_cont_cols = Math.floor(elem_remaining / num_rows);
            }
            if (no_cont_cols > cols_remaining) {
                start_col = init_x === 1 ? this.num_cols - 1 : 0;
                if (cols_remaining !== 0) {
                    this.calc_end_point_dest(start_col, top_row, init_x, -1).forEach((d) => {
                        end_points.push(d);
                    });
                }
                no_cont_cols = cols_remaining;
                cols_remaining = this.num_cols;
                group_iter += 1;
                top_row = bottom_row;
                bottom_row = this.row_limits[group_iter];
                start_row = top_row;
                init_x = -1 * init_x;
                init_y = Math.pow(-1, no_cont_cols) * init_y * -1;
                this.calc_end_point_dest(start_col, bottom_row - 1, -1 * init_x, 1).forEach((d) => {
                    end_points.push(d);
                });
            }
            else if (no_cont_cols === cols_remaining) {
                start_col = init_x === 1 ? this.num_cols - 1 : 0;
                if (cols_remaining !== 0) {
                    this.calc_end_point_dest(start_col, top_row, init_x, -1).forEach((d) => {
                        end_points.push(d);
                    });
                }
                no_cont_cols = cols_remaining;
                cols_remaining = this.num_cols;
                group_iter += 1;
                if (group_iter < this.row_limits.length) {
                    top_row = bottom_row;
                    bottom_row = this.row_limits[group_iter];
                    start_row = top_row;
                    init_x = -1 * init_x;
                    init_y = Math.pow(-1, no_cont_cols) * init_y * -1;
                    across = true;
                }
                else {
                    init_y = 1;
                    init_x = 1;
                }
            }
            else {
                // The number of elements are such that the row group is
                // not exhausted.
                init_y = Math.pow(-1, no_cont_cols) * init_y;
                //As I am moving down this time, next time I will move up
                //and I might not reach the top row, it might be an end
                //point.
                start_row = init_y === 1 ? top_row : bottom_row - 1;
                start_col = start_col + init_x * (no_cont_cols - 1);
                this.calc_end_point_source(start_col, start_row, -1 * init_x, init_y).forEach((d) => {
                    end_points.push(d);
                });
            }
            elem_remaining -= no_cont_cols * num_rows;
            num_rows = bottom_row - top_row;
            //reset direction
            //this is an end point
        }
        //all elements are exhausted
        if (elem_remaining === 0) {
            // The column is exactly filled. In this case, the only end
            // point I need to add is the outer edge w.r.t. the direction
            // in which we are travelling
            start_row = init_y === 1 ? bottom_row - 1 : top_row;
            init_x = across ? -1 * init_x : init_x;
            this.calc_end_point_dest(start_col, start_row, init_x, init_y).forEach((d) => {
                end_points.push(d);
            });
        }
        else {
            // The previous column was exactly filled and the last column
            // is partially filled
            init_y = -1 * init_y; // Since we are in the next column, the direction of y has to be reversed
            start_row = init_y === 1 ? top_row : bottom_row - 1;
            start_col = across ? start_col : start_col + init_x;
            // this is the outer edge of the start of the last column w.r.t
            // the current direction of travel.
            this.calc_end_point_source(start_col, start_row, init_x * -1, init_y).forEach((d) => {
                end_points.push(d);
            });
            // The points corresponding to the cell at which we stop.
            current_row = start_row + (elem_remaining - 1) * init_y; // this is the row in which we end
            // Two points need to be added. The boundary of the last cell
            // in the y-direction.
            this.calc_end_point_dest(start_col, current_row, init_x, init_y).forEach((d) => {
                end_points.push(d);
            });
            this.calc_end_point_dest(start_col, current_row, -1 * init_x, init_y).forEach((d) => {
                end_points.push(d);
            });
        }
        /*
            console.log("new set");
            end_points.forEach(function(point) { console.log(point); });
            console.log("end set");
           */
        return end_points;
    }
    create_bounding_path(elem, end_points) {
        const values = [];
        const editing_copy = end_points.slice();
        values.push(end_points[0]);
        editing_copy.splice(0, 1);
        //do union based on which direction you are trying to move in and
        //draw the path
        //best way seems to be horizaontal followed by vertical
        const props = ['x', 'y'];
        let iter = 0;
        let prop = props[iter % 2];
        let other_prop = props[(iter + 1) % 2];
        const curr_elem = values[0];
        let match = curr_elem[prop];
        let dim = curr_elem[other_prop];
        let max_iter = 2 * editing_copy.length;
        let final_val = 0;
        while (editing_copy.length > 1 && max_iter > 0) {
            const filtered_array = editing_copy.filter((elem) => {
                return elem[prop] == match;
            });
            if (filtered_array.length > 0) {
                iter++;
                const min_elem = d3.min(filtered_array, (elem) => {
                    return elem[other_prop];
                });
                const max_elem = d3.max(filtered_array, (elem) => {
                    return elem[other_prop];
                });
                if (min_elem < dim && max_elem > dim) {
                    if (prop == 'y') {
                        if (this.x_direction == 1) {
                            final_val = max_elem;
                        }
                        else {
                            final_val = min_elem;
                        }
                    }
                    else {
                        // There are elements greater than and lesser than
                        // reference value. I am trying to see if there are
                        // multiple elements greater or lesser. If there is
                        // only one in one of the directions, that is the
                        // direction I draw the line in.
                        const lesser_arr = filtered_array.filter((elem) => {
                            return elem[other_prop] < dim;
                        });
                        const greater_arr = filtered_array.filter((elem) => {
                            return elem[other_prop] > dim;
                        });
                        if (lesser_arr.length == 1) {
                            final_val = min_elem;
                        }
                        else if (greater_arr.length == 1) {
                            final_val = max_elem;
                        }
                        else {
                            final_val = d3.max(lesser_arr, (elem) => {
                                return elem[other_prop];
                            });
                        }
                    }
                }
                else {
                    if (min_elem > dim) {
                        final_val = min_elem;
                    }
                    else {
                        final_val = max_elem;
                    }
                }
                const match_elem = editing_copy.filter((elem) => {
                    return elem[prop] == match && elem[other_prop] == final_val;
                });
                match_elem.forEach((elem) => {
                    editing_copy.splice(editing_copy.indexOf(elem), 1);
                });
                const value = {};
                value[prop] = match;
                value[other_prop] = final_val;
                values.push(value);
            }
            else {
                final_val = dim;
            }
            //swap prop and other_prop
            const temp = prop;
            prop = other_prop;
            other_prop = temp;
            dim = match;
            match = final_val;
            max_iter--;
        }
        if (editing_copy.length > 0) {
            values.push(editing_copy[0]);
        }
        values.push(end_points[0]);
        const line = d3
            .line()
            .curve(d3.curveLinear)
            .x((d) => {
            return d.x;
        })
            .y((d) => {
            return d.y;
        });
        const bounding_path = d3
            .select(elem)
            .append('path')
            .attr('class', 'bounding_path')
            .attr('d', () => {
            return line(values);
        })
            .attr('fill', 'none')
            .style('stroke', this.model.get('group_stroke'))
            .style('stroke-width', 3);
        return bounding_path;
    }
    calc_end_point_source(curr_x, curr_y, x_direction, y_direction) {
        curr_y = y_direction == 1 ? curr_y : curr_y + 1;
        curr_x = x_direction == 1 ? curr_x : curr_x + 1;
        return [{ x: curr_x * this.column_width, y: curr_y * this.row_height }];
    }
    calc_end_point_dest(curr_x, curr_y, x_direction, y_direction) {
        curr_y = y_direction == -1 ? curr_y : curr_y + 1;
        curr_x = x_direction == -1 ? curr_x : curr_x + 1;
        return [{ x: curr_x * this.column_width, y: curr_y * this.row_height }];
    }
}
exports.MarketMap = MarketMap;
//# sourceMappingURL=MarketMap.js.map