<commit_before>import {
    _,
    AgEvent,
    Autowired,
    BeanStub,
    CellRange,
    ChartType,
    Column,
    ColumnController,
    Events,
    EventService,
    PostConstruct
} from "ag-grid-community";
import {RangeController} from "../../rangeController";
import {ChartDatasource, ChartDatasourceParams} from "./chartDatasource";
import {ChartOptions} from "./gridChartComp";

export interface ChartModelUpdatedEvent extends AgEvent {}

export type ColState = {
    column?: Column,
    colId: string,
    displayName: string,
    selected: boolean
}

export class ChartModel extends BeanStub {

    public static EVENT_CHART_MODEL_UPDATED = 'chartModelUpdated';
    public static DEFAULT_CATEGORY = 'AG-GRID-DEFAULT-CATEGORY';

    @Autowired('eventService') private eventService: EventService;
    @Autowired('columnController') private columnController: ColumnController;
    @Autowired('rangeController') rangeController: RangeController;

    private readonly aggregate: boolean;
    private cellRanges: CellRange[];

    private chartType: ChartType;
    private chartData: any[];

    private dimensionColState: ColState[] = [];
    private valueColState: ColState[] = [];

    private width: number;
    private height: number;
    private showTooltips: boolean;
    private insideDialog: boolean;

    private firstCellRange: CellRange;

    private datasource: ChartDatasource;

    public constructor(chartOptions: ChartOptions, cellRanges: CellRange[]) {
        super();
        this.chartType = chartOptions.chartType;
        this.aggregate = chartOptions.aggregate;
        this.width = chartOptions.width;
        this.height = chartOptions.height;
        this.showTooltips = chartOptions.showTooltips;
        this.insideDialog = chartOptions.insideDialog;

        this.initCellRanges(cellRanges);
    }

    @PostConstruct
    private init(): void {
        this.datasource = new ChartDatasource();
        this.getContext().wireBean(this.datasource);

        this.initColumnState();
        this.updateModel();

        this.addDestroyableEventListener(this.eventService, 'fred', this.updateModel.bind(this));
        this.addDestroyableEventListener(this.eventService, Events.EVENT_MODEL_UPDATED, this.updateModel.bind(this));
        this.addDestroyableEventListener(this.eventService, Events.EVENT_CELL_VALUE_CHANGED, this.updateModel.bind(this));
    }

    private initCellRanges(cellRanges: CellRange[]): void {
        cellRanges.forEach(range => range.chartMode = true);
        this.firstCellRange = cellRanges[0];
        this.cellRanges = cellRanges;
    }

    private initColumnState(): void {
        const colsFromAllRanges: Column[] = _.flatten(this.cellRanges.map(range => range.columns));

        const {dimensionCols, valueCols} = this.getAllChartColumns();

        if (valueCols.length === 0) {
            console.warn("ag-Grid - charts require at least one visible column set with 'enableValue=true'");
            return;
        }

        this.valueColState = valueCols.map(column => {
            return {
                column,
                colId: column.getColId(),
                displayName: this.getFieldName(column),
                selected: colsFromAllRanges.indexOf(column) > -1
            };
        });

        this.dimensionColState = dimensionCols.map(column => {
            return {
                column,
                colId: column.getColId(),
                displayName: this.getFieldName(column),
                selected: false
            };
        });

        const dimensionsInCellRange = dimensionCols.filter(col => colsFromAllRanges.indexOf(col) > -1);

        if (dimensionsInCellRange.length > 0) {
            // select the first dimension from the range
            const selectedDimensionId = dimensionsInCellRange[0].getColId();
            this.dimensionColState.forEach(cs => cs.selected = cs.colId === selectedDimensionId);

        } else {
            // add a default category if no dimensions in range
            const defaultCategory = {
                colId: ChartModel.DEFAULT_CATEGORY,
                displayName: '(None)',
                selected: true
            };
            this.dimensionColState.push(defaultCategory);
        }
    }

    private updateModel() {
        if (this.cellRanges.length === 0 || this.valueColState.length === 0) return;

        const startRow = this.rangeController.getRangeStartRow(this.cellRanges[0]).rowIndex;
        const endRow = this.rangeController.getRangeEndRow(this.cellRanges[0]).rowIndex;

        const categoryIds = [this.getSelectedCategory()];

        const fields = this.valueColState
            .filter(cs => cs.selected)
            .map(cs => cs.column) as Column[];

        const params: ChartDatasourceParams = {
            categoryIds: categoryIds,
            fields: fields,
            startRow: startRow,
            endRow: endRow,
            aggregate: this.aggregate
        };

        this.chartData = this.datasource.getData(params);

        this.raiseChartUpdatedEvent();
    }

    public update(updatedColState: ColState): void {
        this.updateColumnState(updatedColState);
        this.updateCellRanges(updatedColState);
        this.updateModel();
    }

    private updateColumnState(updatedCol: ColState) {
        const idsMatch = (cs: ColState) => cs.colId === updatedCol.colId;
        const isDimensionCol = this.dimensionColState.filter(idsMatch).length > 0;
        const isValueCol = this.valueColState.filter(idsMatch).length > 0;

        if (isDimensionCol) {
            // only one dimension should be selected
            this.dimensionColState.forEach(cs => cs.selected = idsMatch(cs));

        } else if (isValueCol) {
            // just update the selected value on the supplied value column
            this.valueColState.forEach(cs => cs.selected = idsMatch(cs) ? updatedCol.selected : cs.selected);
        }
    }

    private updateCellRanges(updatedColState: ColState) {
        if (updatedColState.selected) {

            const column = this.columnController.getGridColumn(updatedColState.colId) as Column;

            const firstCellRange: CellRange = this.firstCellRange;
            this.cellRanges!.push({
                startRow: firstCellRange.startRow,
                endRow: firstCellRange.endRow,
                columns: [column],
                chartMode: true
            });

        } else {
            const colsMatch = (col: Column) => col.getColId() === updatedColState.colId;

            const rangesMatch = (cellRange: CellRange) => cellRange.columns.filter(colsMatch).length === 1;
            const matchingRange = this.cellRanges!.filter(rangesMatch)[0];

            const shouldRemoveRange = matchingRange.columns.length === 1;
            if (shouldRemoveRange) {

                //TODO remove
                const rangesDontMatch = (cellRange: CellRange) => cellRange.columns.filter(colsMatch).length !== 1;

                if (this.cellRanges!.length === 1) {
                    this.firstCellRange = this.cellRanges![0];
                }

                this.cellRanges = this.cellRanges!.filter(rangesDontMatch);
            } else {
                this.cellRanges!.forEach(cellRange => {
                   if (rangesMatch(cellRange)) {
                       const colsDontMatch = (col: Column) => col.getColId() !== updatedColState.colId;
                       cellRange.columns = cellRange.columns.filter(colsDontMatch);<commit_msg>AG-2839 - added handling for multiple ranges to the chart model.<commit_after>import {
    _,
    AgEvent,
    Autowired,
    BeanStub,
    CellRange,
    ChartType,
    Column,
    ColumnController,
    Events,
    EventService,
    PostConstruct
} from "ag-grid-community";
import {RangeController} from "../../rangeController";
import {ChartDatasource, ChartDatasourceParams} from "./chartDatasource";
import {ChartOptions} from "./gridChartComp";

export interface ChartModelUpdatedEvent extends AgEvent {}

export type ColState = {
    column?: Column,
    colId: string,
    displayName: string,
    selected: boolean
}

export class ChartModel extends BeanStub {

    public static EVENT_CHART_MODEL_UPDATED = 'chartModelUpdated';
    public static DEFAULT_CATEGORY = 'AG-GRID-DEFAULT-CATEGORY';

    @Autowired('eventService') private eventService: EventService;
    @Autowired('columnController') private columnController: ColumnController;
    @Autowired('rangeController') rangeController: RangeController;

    private readonly aggregate: boolean;
    private cellRanges: CellRange[];

    private chartType: ChartType;
    private chartData: any[];

    private dimensionColState: ColState[] = [];
    private valueColState: ColState[] = [];

    private width: number;
    private height: number;
    private showTooltips: boolean;
    private insideDialog: boolean;

    private firstCellRange: CellRange;

    private datasource: ChartDatasource;

    public constructor(chartOptions: ChartOptions, cellRanges: CellRange[]) {
        super();
        this.chartType = chartOptions.chartType;
        this.aggregate = chartOptions.aggregate;
        this.width = chartOptions.width;
        this.height = chartOptions.height;
        this.showTooltips = chartOptions.showTooltips;
        this.insideDialog = chartOptions.insideDialog;

        this.initCellRanges(cellRanges);
    }

    @PostConstruct
    private init(): void {
        this.datasource = new ChartDatasource();
        this.getContext().wireBean(this.datasource);

        this.initColumnState();
        this.updateModel();

        this.addDestroyableEventListener(this.eventService, 'fred', this.updateModel.bind(this));
        this.addDestroyableEventListener(this.eventService, Events.EVENT_MODEL_UPDATED, this.updateModel.bind(this));
        this.addDestroyableEventListener(this.eventService, Events.EVENT_CELL_VALUE_CHANGED, this.updateModel.bind(this));
    }

    private initCellRanges(cellRanges: CellRange[]): void {
        cellRanges.forEach(range => range.chartMode = true);
        this.firstCellRange = cellRanges[0];
        this.cellRanges = cellRanges;
    }

    private initColumnState(): void {
        const colsFromAllRanges: Column[] = _.flatten(this.cellRanges.map(range => range.columns));

        const {dimensionCols, valueCols} = this.getAllChartColumns();

        if (valueCols.length === 0) {
            console.warn("ag-Grid - charts require at least one visible column set with 'enableValue=true'");
            return;
        }

        this.valueColState = valueCols.map(column => {
            return {
                column,
                colId: column.getColId(),
                displayName: this.getFieldName(column),
                selected: colsFromAllRanges.indexOf(column) > -1
            };
        });

        this.dimensionColState = dimensionCols.map(column => {
            return {
                column,
                colId: column.getColId(),
                displayName: this.getFieldName(column),
                selected: false
            };
        });

        const dimensionsInCellRange = dimensionCols.filter(col => colsFromAllRanges.indexOf(col) > -1);

        if (dimensionsInCellRange.length > 0) {
            // select the first dimension from the range
            const selectedDimensionId = dimensionsInCellRange[0].getColId();
            this.dimensionColState.forEach(cs => cs.selected = cs.colId === selectedDimensionId);

        } else {
            // add a default category if no dimensions in range
            const defaultCategory = {
                colId: ChartModel.DEFAULT_CATEGORY,
                displayName: '(None)',
                selected: true
            };
            this.dimensionColState.push(defaultCategory);
        }
    }

    private updateModel() {
        if (this.cellRanges.length === 0 || this.valueColState.length === 0) return;

        const startRow = this.rangeController.getRangeStartRow(this.cellRanges[0]).rowIndex;
        const endRow = this.rangeController.getRangeEndRow(this.cellRanges[0]).rowIndex;

        const categoryIds = [this.getSelectedCategory()];

        const fields = this.valueColState
            .filter(cs => cs.selected)
            .map(cs => cs.column) as Column[];

        const params: ChartDatasourceParams = {
            categoryIds: categoryIds,
            fields: fields,
            startRow: startRow,
            endRow: endRow,
            aggregate: this.aggregate
        };

        this.chartData = this.datasource.getData(params);

        this.raiseChartUpdatedEvent();
    }

    public update(updatedColState: ColState): void {
        this.updateColumnState(updatedColState);
        this.updateCellRanges(updatedColState);
        this.updateModel();
    }

    private updateColumnState(updatedCol: ColState) {
        const idsMatch = (cs: ColState) => cs.colId === updatedCol.colId;
        const isDimensionCol = this.dimensionColState.filter(idsMatch).length > 0;
        const isValueCol = this.valueColState.filter(idsMatch).length > 0;

        if (isDimensionCol) {
            // only one dimension should be selected
            this.dimensionColState.forEach(cs => cs.selected = idsMatch(cs));

        } else if (isValueCol) {
            // just update the selected value on the supplied value column
            this.valueColState.forEach(cs => cs.selected = idsMatch(cs) ? updatedCol.selected : cs.selected);
        }
    }

    private updateCellRanges(updatedColState: ColState) {
        if (updatedColState.selected) {

            const column = this.columnController.getGridColumn(updatedColState.colId) as Column;

            const firstCellRange: CellRange = this.firstCellRange;
            this.cellRanges!.push({
                startRow: firstCellRange.startRow,
                endRow: firstCellRange.endRow,
                columns: [column],
                chartMode: true
            });

        } else {
            const colsMatch = (col: Column) => col.getColId() === updatedColState.colId;

            const rangesMatch = (cellRange: CellRange) => cellRange.columns.filter(colsMatch).length === 1;
            const matchingRange = this.cellRanges!.filter(rangesMatch)[0];

            const shouldRemoveRange = matchingRange.columns.length === 1;
            if (shouldRemoveRange) {

                //TODO remove dimensions that are not selected
                const rangesDontMatch = (cellRange: CellRange) => cellRange.columns.filter(colsMatch).length !== 1;

                if (this.cellRanges!.length === 1) {
                    this.firstCellRange = this.cellRanges![0];
                }

                this.cellRanges = this.cellRanges!.filter(rangesDontMatch);
            } else {
                this.cellRanges!.forEach(cellRange => {
                   if (rangesMatch(cellRange)) {
                       const colsDontMatch = (col: Column) => col.getColId() !== updatedColState.colId;
                       cellRange.columns = cellRange.columns.filter(colsDontMatch);
{'fraction_non_alphanumeric': 0.10165343915343915, 'fraction_numerical': 0.0026455026455026454, 'mean_word_length': 2.4452039188881294, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1089197', 'n_tokens_mistral': 4143, 'n_tokens_neox': 3978, 'n_words': 829}