Source

client/ui/data/chartview.js

/** 
 * 
 * The **Chart** derives from [DataComponent](kiss.ui.DataComponent.html).
 * 
 * It's a [powerful chart view](https://kissjs.net/#ui=start&section=chart) with the following features:
 * 
 * @param {object} config
 * @param {string} config.name - The chart title. We use the "name" property instead of "title" to be consistent with all view names.
 * @param {Collection} config.collection - The data source collection
 * @param {boolean} [config.dashboard] - true if the chart is part of a dashboard. Default is false.
 * @param {object} [config.record] - Record to persist the view configuration into the db
 * @param {string} [config.chartType] - bar | line | pie | doughnut
 * @param {string} [config.isTimeSeries] - true if the chart is a time series. It will manage the timeField and timePeriod for X axis.
 * @param {string} [config.categoryField] - Field to group by, if not a time series
 * @param {string} [config.timeField] - Field to use as time axis, if a time series
 * @param {string} [config.timePeriod] - Grouping unit for time axis, if a time series: day, week, month, quarter, year
 * @param {string} [config.operationType] - "count" or "summary"
 * @param {string} [config.summaryOperation] - "sum", "average". Only used if operationType is "summary"
 * @param {string} [config.valueField] - Field to use as value (Y axis)
 * @param {number} [config.precision] - Number of decimal places to show, only for "number" charts. Default is 0.
 * @param {boolean} [config.startAtZero] - true to start the Y axis at zero. Not used for pie & doughnut charts
 * @param {boolean} [config.showLegend] - true to show the legend
 * @param {string} [config.legendPosition] - top, bottom, left, right
 * @param {boolean} [config.showValues] - true to show the values on the chart
 * @param {boolean} [config.showLabels] - true to show the labels on the chart
 * @param {boolean} [config.centerLabels] - true to center labels inside the chart. Default is true.
 * @param {string} [config.labelColor] - Hexa color code for labels inside the chart. Ex: #00aaee
 * @param {string} [config.color] - Hexa color code. Ex: #00aaee
 * @param {boolean} [config.showToolbar] - false to hide the toolbar (default = true)
 * @param {boolean} [config.showActions] - false to hide the custom actions menu (default = true)
 * @param {boolean} [config.canSort] - false to hide the sort button (default = true)
 * @param {boolean} [config.canFilter] - false to hide the filter button (default = true)
 * @param {object[]} [config.actions] - Array of menu actions, where each menu entry is: {text: "abc", icon: "fas fa-check", action: function() {}}
 * @param {boolean} [config.useCDN] - Set to false to use the local version of ChartJS. Default is true.
 * @returns this
 * 
 * ## Generated markup
 * ```
 * <a-chartview class="a-chartview">
 *      <div class="chartview-header">
 *          <div class="chartview-title">
 *              <!-- Chart title -->
 *          </div>
 *          <div class="chartview-toolbar">
 *              <!-- Chart view toolbar items -->
 *          </div>
 *      </div>
 *      <div class="chartview-chart">
 *          <!-- Embedded chart component -->
 *      </div>
 * </a-chartview>
 * ```
 */
kiss.ui.ChartView = class ChartView extends kiss.ui.DataComponent {
    /**
     * Its a Custom Web Component. Do not use the constructor directly with the **new** keyword.
     * Instead, use one of the following methods:
     * 
     * Create the Web Component and call its **init** method:
     * ```
     * const myChartView = document.createElement("a-chartview").init(config)
     * ```
     * 
     * Or use the shorthand for it:
     * ```
     * const myChartView = createChartView({
     *   id: "my-chartview",
     *   color: "#00aaee",
     *   collection: kiss.app.collections["opportunity"],
     *   chartType: "bar",
     *   categoryField: "Country"
     * })
     * 
     * myChartView.render()
     * ```
     */
    constructor() {
        super()
    }

    /**
     * Generates a Chart view from a JSON config
     * 
     * @ignore
     * @param {object} config - JSON config
     * @returns {HTMLElement}
     */
    init(config) {
        // This component must be resized with its parent container
        config.autoSize = true

        // Init the parent DataComponent
        super.init(config)

        // Options
        this.dashboard = (config.dashboard === true)
        this.showToolbar = (config.showToolbar !== false)
        this.showActions = (config.showActions !== false)
        this.showSetup = (config.showSetup !== false)
        this.canSort = (config.canSort !== false)
        this.canFilter = (config.canFilter !== false)
        this.canGroup = (config.canGroup !== false)
        this.actions = config.actions || []
        this.color = config.color || "#00aaee"
        this.useCDN = (config.useCDN === false) ? false : true

        // Build chart view skeletton markup
        let id = this.id
        this.innerHTML = /*html*/
            `<div class="chartview-container">
                <div class="chartview-header">
                    <div class="chartview-title">
                        ${this.name || ""}
                    </div>
                    <div style="flex: 1"></div>
                    <div id="chartview-toolbar:${id}" class="chartview-toolbar">
                        <div id="actions:${id}"></div>
                    </div>
                </div>
                <div class="chartview-chart"></div>
            </div>`.removeExtraSpaces()

        // Set chart components
        this.header = this.querySelector(".chartview-header")
        this.headerTitle = this.querySelector(".chartview-title")
        this.toolbar = this.querySelector(".chartview-toolbar")
        this.chartContainer = this.querySelector(".chartview-chart")

        this._initChartParams(config)
            ._initSubscriptions()
            ._initClickEvents()

        return this
    }

    /**
     * 
     * CHART METHODS
     * 
     */

    /**
     * Load data into the chart.
     * 
     * @ignore
     */
    async load() {
        try {
            log(`kiss.ui - Chart ${this.id} - Loading collection <${this.collection.id} (changed: ${this.collection.hasChanged})>`)

            // Apply filter, sort, group, projection
            // Priority is given to local config, then to the passed collection, then to default
            this.collection.filter = this.filter
            this.collection.filterSyntax = this.filterSyntax
            this.collection.sort = this.sort
            this.collection.sortSyntax = this.sortSyntax
            this.collection.group = this.group
            this.collection.projection = this.projection
            this.collection.groupUnwind = this.groupUnwind

            // Load records
            await this.collection.find()

            // Render the chart toolbar
            this._renderToolbar()

        } catch (err) {
            log(err)
            log(`kiss.ui - Chart ${this.id} - Couldn't load data properly`)
        }
    }

    /**
     * Set the chart title
     * 
     * @param {string} newTitle 
     */
    setTitle(newTitle) {
        this.headerTitle.innerHTML = newTitle
    }

    /**
     * Update the chart color (toolbar buttons + modal windows)
     * 
     * @param {string} newColor
     */
    setColor(newColor) {
        this.color = newColor
        Array.from(this.toolbar.children).forEach(item => {
            if (item && item.firstChild && item.firstChild.type == "button") item.firstChild.setIconColor(newColor)
        })
    }

    /**
     * Update the chart layout
     */
    updateLayout(msg) {
        if (this.isConnected) {
            this._render()
        }
    }

    /**
     * Update the chart size according to its container
     * 
     * @private
     * @ignore
     * @returns this
     */
    updateSize() {
        if (!this.chart) return this

        this._initChartSize()
        this.chart.resize(this.chartWidth, this.chartHeight)
        return this
    }    

    /**
     * Display the setup window to configure the chart
     */
    showSetupWindow() {
        let _this = this
        let chartType
        let color = this.color

        //
        // CHART TYPE
        //
        const sectionForChartType = {
            id: "chart-setup-type",
            class: "chartview-wizard-section",
            layout: "horizontal",
            defaultConfig: {
                type: "button",
                margin: "0 0.5rem 0 0",
                flex: 1,
                justifyContent: "center",
                height: "6.4rem",
                iconSize: "4rem",
                iconColor: this.color,
                action: function () {
                    chartType = this.config.chartType
                    $("chart-setup-type").highlightButton(chartType)
                    $("chartType").setValue(chartType)

                    let data = $("chart-setup").getData()
                    publish("EVT_CHART_SETUP_CHANGED", data)
                }
            },
            items: [{
                    tip: txtTitleCase("#bar chart"),
                    icon: kiss.global.getChartIcon("bar"),
                    chartType: "bar"
                },
                {
                    tip: txtTitleCase("#line chart"),
                    icon: kiss.global.getChartIcon("line"),
                    chartType: "line"
                },
                {
                    tip: txtTitleCase("#pie chart"),
                    icon: kiss.global.getChartIcon("pie"),
                    chartType: "pie"
                },
                {
                    tip: txtTitleCase("#pie chart"),
                    icon: kiss.global.getChartIcon("doughnut"),
                    chartType: "doughnut"
                },
                {
                    tip: txtTitleCase("#number chart"),
                    icon: kiss.global.getChartIcon("number"),
                    chartType: "number"
                },
                {
                    id: "chartType",
                    type: "text",
                    value: this.chartType,
                    hidden: true
                }
            ],
            methods: {
                load: () => {
                    chartType = this.chartType
                    $("chart-setup-type").highlightButton(this.chartType)
                },
                highlightButton(chartType) {
                    const allButtons = this.querySelectorAll("a-button")
                    allButtons.forEach(button => {
                        if (button.config.chartType != chartType) {
                            button.setColor(color)
                            button.setIconColor(color)
                            button.setBackgroundColor("var(--button-background)")
                        } else {
                            button.setColor("#ffffff")
                            button.setIconColor("#ffffff")
                            button.setBackgroundColor(color)
                        }
                    })
                }
            }
        }

        // 
        // CHART DATA
        // 
        const categoryFields = this.model.getFieldsAsOptions([
            "text",
            "select",
            "selectViewColumn",
            "selectViewColumns",
            "checkbox",
            "directory",
            "rating",
            "icon",
            "color"
        ]).filter(field => !field.isFromPlugin) // For now, don't use the fields coming from plugins

        const sectionForChartData = {
            class: "chartview-wizard-section",
            type: "panel",
            title: txtTitleCase("data"),
            headerColor: "var(--body)",
            headerBackgroundColor: "var(--body-background-alt)",
            border: "none",

            defaultConfig: {
                width: "100%",
                labelWidth: "50%",
                fieldWidth: "50%",
                labelPosition: "left"
            },

            items: [
                // TITLE (NAME)
                {
                    type: "text",
                    id: "name",
                    label: txtTitleCase("title"),
                    value: this.name || ""
                },                
                // TIME SERIES
                {
                    type: "checkbox",
                    id: "isTimeSeries",
                    label: txtTitleCase("#time series"),
                    value: this.isTimeSeries,
                    shape: "switch",
                    width: "100%",
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.chartType == "pie" || data.chartType == "doughnut" || data.chartType == "number") return this.hide()
                            this.show()
                        }
                    },
                    events: {
                        change: () => publish("EVT_CHART_SETUP_CHANGED", $("chart-setup").getData())
                    }
                },
                // CATEGORY FIELD
                {
                    type: "select",
                    id: "categoryField",
                    label: txtTitleCase("#category field"),
                    multiple: false,
                    options: categoryFields,
                    value: this.categoryField,
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color,
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.isTimeSeries && data.chartType != "pie" && data.chartType != "doughnut") return this.hide()
                            if (data.chartType == "number") return this.hide()
                            this.show()
                        }
                    }
                },
                // TIME FIELD
                {
                    type: "select",
                    id: "timeField",
                    label: txtTitleCase("time axis"),
                    multiple: false,
                    options: this.model.getFieldsAsOptions("date"),
                    value: this.timeField,
                    autocomplete: "off",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color,
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.isTimeSeries && data.chartType != "pie" && data.chartType != "doughnut" && data.chartType != "number") return this.show()
                            this.hide()
                        }
                    }
                },
                // TIME GROUPING UNIT
                {
                    type: "select",
                    id: "timePeriod",
                    label: txtTitleCase("group by"),
                    multiple: false,
                    options: [
                        {
                            value: "week",
                            label: txtTitleCase("week")
                        },
                        {
                            value: "month",
                            label: txtTitleCase("month")
                        },
                        {
                            value: "quarter",
                            label: txtTitleCase("quarter")
                        },
                        {
                            value: "year",
                            label: txtTitleCase("year")
                        }
                    ],
                    value: this.timePeriod || "month",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color,
                    autocomplete: "off",
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.isTimeSeries && data.chartType != "pie" && data.chartType != "doughnut" && data.chartType != "number") return this.show()
                            this.hide()
                        }
                    }
                },
                // OPERATION TYPE
                {
                    type: "select",
                    id: "operationType",
                    label: txtTitleCase("chart values"),
                    multiple: false,
                    autocomplete: "off",
                    options: [{
                            label: txtTitleCase("#count data"),
                            value: "count"
                        },
                        {
                            label: txtTitleCase("#summarize data"),
                            value: "summary"
                        },
                    ],
                    value: this.operationType || "count",
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color,
                    events: {
                        change: () => publish("EVT_CHART_SETUP_CHANGED", $("chart-setup").getData())
                    }
                },
                // SUMMARY OPERATION
                {
                    type: "select",
                    id: "summaryOperation",
                    label: txtTitleCase("summary operation"),
                    multiple: false,
                    options: [{
                            label: txtTitleCase("sum"),
                            value: "sum"
                        },
                        {
                            label: txtTitleCase("average"),
                            value: "average"
                        },
                    ],
                    value: this.summaryOperation || "sum",
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color,
                    autocomplete: "off",
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.operationType != "summary") return this.hide()
                            this.show()
                        }
                    }
                },
                // VALUE FIELD
                {
                    type: "select",
                    id: "valueField",
                    label: txtTitleCase("#summary field"),
                    multiple: false,
                    options: this.model.getFieldsAsOptions(["number", "slider"]),
                    value: this.valueField,
                    autocomplete: "off",
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color,
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.operationType != "summary") return this.hide()
                            this.show()
                        }
                    }
                },

                // Sort & filter
                {
                    layout: "horizontal",
                    margin: "2rem 0 0 0",
                    defaultConfig: {
                        type: "button",
                        width: "100%",
                        labelWidth: "50%",
                        fieldWidth: "50%",
                        labelPosition: "left",
                        iconColor: this.color,
                        height: "4rem"
                    },                    
                    items: [
                        {
                            type: "button",
                            text: txtTitleCase("to sort"),
                            icon: "fas fa-sort",
                            action: () => this.showSortWindow(),
                            margin: "0 0.5rem 0 0"
                        },
                        {
                            type: "button",
                            text: txtTitleCase("to filter"),
                            icon: "fas fa-filter",
                            action: () => this.showFilterWindow()
                        }
                    ]
                }
            ]
        }

        //
        // CHART LAYOUT
        // 
        const sectionForChartLayout = {
            class: "chartview-wizard-section",
            type: "panel",
            title: txtTitleCase("layout"),
            headerColor: "var(--body)",
            headerBackgroundColor: "var(--body-background-alt)",
            border: "none",

            defaultConfig: {
                width: "100%",
                labelWidth: "50%",
                fieldWidth: "50%",
                labelPosition: "left"
            },

            items: [
                // SHOW LEGEND
                {
                    type: "checkbox",
                    id: "showLegend",
                    label: txtTitleCase("show legend"),
                    shape: "switch",
                    value: this.showLegend === false ? false : true,
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.chartType != "number") return this.show()
                            this.hide()
                        }
                    },
                    events: {
                        change: () => publish("EVT_CHART_SETUP_CHANGED", $("chart-setup").getData())
                    }
                },
                // LEGEND POSITION
                {
                    type: "select",
                    id: "legendPosition",
                    label: txtTitleCase("legend position"),
                    autocomplete: "off",
                    options: [{
                            value: "top",
                            label: txtTitleCase("top")
                        },
                        {
                            value: "bottom",
                            label: txtTitleCase("bottom")
                        },
                        {
                            value: "left",
                            label: txtTitleCase("left")
                        },
                        {
                            value: "right",
                            label: txtTitleCase("right")
                        }
                    ],
                    value: this.legendPosition || "top",
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.showLegend === false || data.chartType == "number") return this.hide()
                            this.show()
                        }
                    }
                },
                // SHOW VALUES ON CHART
                {
                    type: "checkbox",
                    id: "showValues",
                    label: txtTitleCase("show values on chart"),
                    shape: "switch",
                    value: this.showValues === false ? false : true,
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.chartType == "line" || data.chartType == "number") return this.hide()
                            this.show()
                        }
                    },
                    events: {
                        change: () => publish("EVT_CHART_SETUP_CHANGED", $("chart-setup").getData())
                    }
                },
                // SHOW LABELS ON CHART
                {
                    type: "checkbox",
                    id: "showLabels",
                    label: txtTitleCase("show labels on chart"),
                    shape: "switch",
                    value: !!this.showLabels,
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.chartType == "pie" || data.chartType == "doughnut") return this.show()
                            this.hide()
                        }
                    },
                    events: {
                        change: () => publish("EVT_CHART_SETUP_CHANGED", $("chart-setup").getData())
                    }
                },
                // LABELS POSITION ON CHART
                {
                    type: "checkbox",
                    id: "centerLabels",
                    label: txtTitleCase("#center labels"),
                    shape: "switch",
                    value: this.centerLabels === false ? false : true,
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.chartType == "number") return this.hide()
                            else if (
                                (data.showValues && (data.chartType != "line")) ||
                                (data.showLabels && (data.chartType == "pie" || data.chartType == "doughnut"))
                            ) return this.show()
                            this.hide()
                        }
                    }
                },
                // LABELS COLOR ON CHART
                {
                    type: "color",
                    id: "labelColor",
                    label: txtTitleCase("color"),
                    value: this.labelColor || "#000000",
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (
                                (data.showValues && (data.chartType != "line")) ||
                                (data.showLabels && (data.chartType == "pie" || data.chartType == "doughnut")) ||
                                (data.chartType == "number")
                            ) return this.show()
                            this.hide()
                        }
                    }
                },
                // START AT ZERO
                {
                    type: "checkbox",
                    id: "startAtZero",
                    label: txtTitleCase("start at zero"),
                    value: this.startAtZero === false ? false : true,
                    shape: "switch",
                    width: "100%",
                    subscriptions: {
                        EVT_CHART_SETUP_CHANGED: function (data) {
                            if (data.chartType == "pie" || data.chartType == "doughnut" || data.chartType == "number") return this.hide()
                            this.show()
                        }
                    }
                },                
                // NUMBER PRECISION
                {
                    hidden: true, // Not used yet
                    type: "select",
                    id: "precision",
                    label: txtTitleCase("number style"),
                    autocomplete: "off",
                    value: this.precision || 0,
                    options: [{
                            label: "1",
                            value: 0
                        },
                        {
                            label: "1.0",
                            value: 1
                        },
                        {
                            label: "1.00",
                            value: 2
                        },
                        {
                            label: "1.000",
                            value: 3
                        },
                        {
                            label: "1.0000",
                            value: 4
                        },
                        {
                            label: "1.00000",
                            value: 5
                        },
                        {
                            label: "1.000000",
                            value: 6
                        },
                        {
                            label: "1.0000000",
                            value: 7
                        },
                        {
                            label: "1.00000000",
                            value: 8
                        }
                    ],
                    optionsColor: this.color,
                    // subscriptions: {
                    //     EVT_CHART_SETUP_CHANGED: function (data) {
                    //         if (data.chartType != "number") return this.hide()
                    //         this.show()
                    //     }
                    // }
                },
                // FIELD UNIT
                {
                    hidden: true, // Not used yet
                    type: "text",
                    id: "unit",
                    label: txtTitleCase("unit"),
                    value: this.unit,

                    // subscriptions: {
                    //     EVT_CHART_SETUP_CHANGED: function (data) {
                    //         if (data.chartType != "number") return this.hide()
                    //         this.show()
                    //     }
                    // }
                }
            ]
        }

        // 
        // Build the final panel
        // 
        const viewId = this.id
        createPanel({
            id: "chart-setup",
            title: txtTitleCase("setup the chart"),
            icon: "fas fa-cog",
            draggable: true,
            closable: true,
            modal: true,
            backdropFilter: true,
            top: 0,
            left: "calc(100vw - 50rem)",
            width: "50rem",
            height: () => kiss.screen.current.height,
            headerHeight: "4.9rem",
            headerBackgroundColor: this.color,
            overflowY: "auto",
            animation: {
                name: "fadeIn",
                speed: "faster"
            },

            items: [
                sectionForChartType,
                sectionForChartData,
                sectionForChartLayout,

                // Save button
                {
                    layout: "horizontal",
                    overflow: "unset",
                    defaultConfig: {
                        type: "button",
                        flex: 1,
                        height: "4rem",
                        margin: "0.5rem 1rem"
                    },
                    items: [{
                        type: "button",
                        icon: "fas fa-check",
                        iconColor: "var(--green)",
                        text: txtTitleCase("save"),
                        action: () => $("chart-setup").save()
                    }]
                }
            ],

            methods: {
                load() {
                    // Allow the different fields to show/hide at startup depending on the chart config
                    publish("EVT_CHART_SETUP_CHANGED", this.getData())

                    // Focus on the first field
                    setTimeout(() => $("name").focus(), 100)
                },
                async save() {
                    const {
                        name,
                        chartType,
                        isTimeSeries,
                        categoryField,
                        timeField,
                        timePeriod,
                        operationType,
                        summaryOperation,
                        valueField,
                        startAtZero,
                        showLegend,
                        legendPosition,
                        showValues,
                        showLabels,
                        centerLabels,
                        labelColor,
                        precision,
                        unit
                    } = $("chart-setup").getData()

                    // Controls...
                    if (!chartType ||
                        (!isTimeSeries && !categoryField && chartType != "number") ||
                        (chartType == "pie" && !categoryField) ||
                        (chartType == "doughnut" && !categoryField) ||
                        (isTimeSeries && !timeField) ||
                        (operationType == "summary" && !summaryOperation) ||
                        (operationType == "summary" && !valueField)
                    ) {
                        return createNotification(txtTitleCase("#chart wrong params"))
                    }

                    // Broadcast the new chart setup
                    publish("EVT_VIEW_SETUP:" + viewId, {
                        name,
                        chartType,
                        isTimeSeries,
                        categoryField,
                        timeField,
                        timePeriod,
                        operationType,
                        summaryOperation,
                        valueField,
                        startAtZero,
                        showLegend,
                        legendPosition,
                        showValues,
                        showLabels,
                        centerLabels,
                        labelColor,
                        precision,
                        unit
                    })

                    // If the chart is part of a dashboard, tell the dashboard to update
                    if (_this.dashboard) publish("EVT_DASHBOARD_SETUP", viewId)
                }
            }
        }).render()
    }

    /**
     * Display the source records of the chart
     * 
     * @param {string} [category] - Optional category to filter the records
     */
    async showRecords(category) {
        const model = this.model
        const tempDatatableId = "chart-data-" + this.id
        const tempCollection = this.collection.clone()
        let newFilter

        if (category) {
            if (!this.isTimeSeries) {
                // Bar, Pie, Doughnut
                const categoryField = this.model.getField(this.categoryField)
                newFilter = {
                    type: "group",
                    operator: "and",
                    filters: [
                        this.collection.filter,
                        {
                            type: "filter",
                            fieldId: categoryField.id,
                            fieldType: categoryField.type,
                            operator: "=",
                            value: category
                        }
                    ]
                }
            }
            // else if (this.isTimeSeries) {
            //     // Bar, Line
            //     const timeField = this.model.getField(this.timeField)
            // }

            tempCollection.filter = newFilter
            tempCollection.group = []
        }

        // Load the records
        await tempCollection.find()

        // Create the datatable
        const datatable = createDatatable({
            id: "datatable-" + tempDatatableId,
            type: "datatable",
            collection: tempCollection,
            color: this.color,
            showActions: false,
            showLinks: false,
            canEdit: false,
            canAddField: false,
            canEditField: false,
            canCreateRecord: false,
            canSelect: false,
            autoSize: true
        })       
    
        // Build the panel to show the datatable
        createPanel({
            modal: true,
            closable: true,
            title: "<b>" + model.namePlural + "</b>",
            icon: model.icon,
            headerBackgroundColor: model.color,
            display: "flex",
            layout: "vertical",
            align: "center",
            verticalAlign: "center",
            background: "var(--body-background)",
            padding: 0,
            width: () => "calc(100vw - 2rem)",
            height: () => "calc(100vh - 2rem)",
            autoSize: true,
            methods: {
                load() {
                    setTimeout(() => this.setItems([datatable]), 50)
                }
            }
        }).render()
    }  


    /**
     * Define the specific chart params:
     * - chartType: pie, bar, line
     * - chartValueField: field used to display the values
     * 
     * @private
     * @ignore
     * @param {object} config - {chartType, chartValueField}
     * @returns this
     */
    _initChartParams(config) {
        if (this.record) {
            this.chartType = config.chartType || this.record.config.chartType
            this.isTimeSeries = config.isTimeSeries || this.record.config.isTimeSeries
            this.categoryField = config.categoryField || this.record.config.categoryField
            this.timeField = config.timeField || this.record.config.timeField
            this.timePeriod = config.timePeriod || this.record.config.timePeriod
            this.operationType = config.operationType || this.record.config.operationType
            this.summaryOperation = config.summaryOperation || this.record.config.summaryOperation
            this.valueField = config.valueField || this.record.config.valueField
            this.startAtZero = config.startAtZero || this.record.config.startAtZero
            this.showLegend = config.showLegend || this.record.config.showLegend
            this.legendPosition = config.legendPosition || this.record.config.legendPosition
            this.showValues = config.showValues || this.record.config.showValues
            this.showLabels = config.showLabels || this.record.config.showLabels
            this.centerLabels = config.centerLabels || this.record.config.centerLabels
            this.labelColor = config.labelColor || this.record.config.labelColor
            this.precision = config.precision || this.record.config.precision || 0
            this.unit = config.unit || this.record.config.unit || ""

        } else {
            this.chartType = config.chartType || this.config.chartType
            this.isTimeSeries = config.isTimeSeries || false
            this.categoryField = config.categoryField || this.config.categoryField
            this.timeField = config.timeField || this.config.timeField
            this.timePeriod = config.timePeriod || "month"
            this.operationType = config.operationType || "count"
            this.summaryOperation = config.summaryOperation || "sum"
            this.valueField = config.valueField || this.config.valueField
            this.startAtZero = (config.startAtZero === false) ? false : true
            this.showLegend = (config.showLegend === false) ? false : true
            this.legendPosition = config.legendPosition || "top"
            this.showValues = (config.showValues === false) ? false : true
            this.showLabels = config.showLabels || false
            this.centerLabels = (config.centerLabels === false) ? false : true
            this.labelColor = config.labelColor || "#000000"
            this.precision = config.precision || 0
            this.unit = config.unit || ""
        }

        // Pie and doughnut charts don't support time series
        this.isTimeSeries = this.isTimeSeries && (this.chartType == "line" || this.chartType == "bar")

        // Set category field
        if (this.isTimeSeries) {
            // In time series, we need to sort by time
            this.sort = [{
                [this.timeField]: "asc"
            }]
            this.group = [this.timeField]
        } else {
            if (this.chartType != "number") {
                this.group = [this.categoryField]
            }
            else {
                this.group = []
            }
        }

        // Defaults to the first number field
        if (!this.valueField) {
            let modelNumberFields = this.model.getFieldsByType(["number"])
            if (modelNumberFields.length != 0) {
                this.valueField = modelNumberFields[0].id
            } else {
                this.valueField = null
            }
        }

        return this
    }

    /**
     * Initialize subscriptions to PubSub
     * 
     * @private
     * @ignore
     * @returns this
     */
    _initSubscriptions() {
        super._initSubscriptions()

        const viewModelId = this.modelId.toUpperCase()

        // React to database mutations
        this.subscriptions = this.subscriptions.concat([
            // Local events (not coming from websocket)
            subscribe("EVT_VIEW_SETUP:" + this.id, (msgData) => this._updateConfig(msgData)),

            // React to database mutations
            subscribe("EVT_DB_INSERT:" + viewModelId, (msgData) => this._reloadWhenNeeded(msgData)),
            subscribe("EVT_DB_UPDATE:" + viewModelId, (msgData) => this._reloadWhenNeeded(msgData)),
            subscribe("EVT_DB_DELETE:" + viewModelId, (msgData) => this._reloadWhenNeeded(msgData)),
            subscribe("EVT_DB_INSERT_MANY:" + viewModelId, (msgData) => this._reloadWhenNeeded(msgData, 2000)),
            subscribe("EVT_DB_UPDATE_MANY:" + viewModelId, (msgData) => this._reloadWhenNeeded(msgData, 2000)),
            subscribe("EVT_DB_DELETE_MANY:" + viewModelId, (msgData) => this._reloadWhenNeeded(msgData, 2000)),
            subscribe("EVT_DB_UPDATE_BULK", (msgData) => this._reloadWhenNeeded(msgData, 2000)),
            subscribe("EVT_DB_UPDATE:VIEW", (msgData) => this._updateTitle(msgData))
        ])

        return this
    }

    /**
     * Initialize click events
     * 
     * @private
     * @ignore
     * @returns this
     */
    _initClickEvents() {
        this.onclick = (e) => {
            const target = e.target
            if (!target.closest(".a-button") && !target.closest(".chartview-number") && target.tagName != "CANVAS") {
                publish("EVT_CHART_CLICKED", {
                    chartId: this.id,
                    event: e
                })
            }
        }
        return this
    }

    /**
     * Initialize chart sizes inside component
     * 
     * @private
     * @ignore
     * @returns this
     */
    _initChartSize() {
        const VERTICAL_SPACE = 80 // total space when adding toolbar, header, margins, paddings...
        const HORIZONTAL_SPACE = 40
        let width = this.clientWidth - HORIZONTAL_SPACE
        let height = this.clientHeight - VERTICAL_SPACE

        // Maintain max aspect ratio to 2
        // if (width > (2 * height)) width = 2 * height

        this.chartWidth = width
        this.chartHeight = height
        return this
    }

    /**
     * Update the chart title
     * 
     * @private
     * @ignore
     */
    _updateTitle(msgData) {
        if (!this.record) return
        if (msgData.id == this.record.id && msgData.data.name) {
            this.setTitle(msgData.data.name)
        }
    }


    /**
     * Update the chart configuration
     * 
     * @private
     * @ignore
     * @param {object} newConfig 
     */
    async _updateConfig(newConfig) {
        let currentConfig
        let finalConfig

        // Evaluate if the new config impacts data or not
        let needsDataReload = false
        if (this.chartType != newConfig.chartType) needsDataReload = true
        if (this.isTimeSeries != newConfig.isTimeSeries) needsDataReload = true
        if (this.isTimeSeries && this.timeField != newConfig.timeField) needsDataReload = true
        if (this.isTimeSeries && this.timePeriod != newConfig.timePeriod) needsDataReload = true
        if (this.categoryField != newConfig.categoryField) needsDataReload = true
        if (this.operationType != newConfig.operationType) needsDataReload = true
        if (this.operationType == "summary" && this.summaryOperation != newConfig.summaryOperation) needsDataReload = true
        if (this.valueField != newConfig.valueField) needsDataReload = true
        if (needsDataReload) this.collection.hasChanged = true
        
        // Get the current config
        if (this.record) {
            currentConfig = this.record.config
        } else {
            currentConfig = {
                chartType: this.chartType,
                isTimeSeries: this.isTimeSeries,
                categoryField: this.categoryField,
                timeField: this.timeField,
                timePeriod: this.timePeriod,
                operationType: this.operationType,
                summaryOperation: this.summaryOperation,
                valueField: this.valueField,
                startAtZero: this.startAtZero,
                showLegend: this.showLegend,
                legendPosition: this.legendPosition,
                showValues: this.showValues,
                showLabels: this.showLabels,
                centerLabels: this.centerLabels,
                labelColor: this.labelColor,
                precision: this.precision,
                unit: this.unit
            }
        }
        
        // Update the chart configuration
        if (newConfig.hasOwnProperty("name")) this.name = newConfig.name
        if (newConfig.hasOwnProperty("chartType")) this.chartType = newConfig.chartType
        if (newConfig.hasOwnProperty("isTimeSeries")) this.isTimeSeries = newConfig.isTimeSeries
        if (newConfig.hasOwnProperty("categoryField")) this.categoryField = newConfig.categoryField
        if (newConfig.hasOwnProperty("timeField")) this.timeField = newConfig.timeField
        if (newConfig.hasOwnProperty("timePeriod")) this.timePeriod = newConfig.timePeriod
        if (newConfig.hasOwnProperty("operationType")) this.operationType = newConfig.operationType
        if (newConfig.hasOwnProperty("summaryOperation")) this.summaryOperation = newConfig.summaryOperation
        if (newConfig.hasOwnProperty("valueField")) this.valueField = newConfig.valueField
        if (newConfig.hasOwnProperty("startAtZero")) this.startAtZero = newConfig.startAtZero
        if (newConfig.hasOwnProperty("showLegend")) this.showLegend = newConfig.showLegend
        if (newConfig.hasOwnProperty("legendPosition")) this.legendPosition = newConfig.legendPosition
        if (newConfig.hasOwnProperty("showValues")) this.showValues = newConfig.showValues
        if (newConfig.hasOwnProperty("showLabels")) this.showLabels = newConfig.showLabels
        if (newConfig.hasOwnProperty("centerLabels")) this.centerLabels = newConfig.centerLabels
        if (newConfig.hasOwnProperty("labelColor")) this.labelColor = newConfig.labelColor
        if (newConfig.hasOwnProperty("precision")) this.precision = newConfig.precision
        if (newConfig.hasOwnProperty("unit")) this.unit = newConfig.unit

        // Apply the new config
        let config = Object.assign(currentConfig, newConfig)
        this.setTitle(this.name)

        // Pie and doughnut charts don't support time series
        this.isTimeSeries = newConfig.isTimeSeries && (newConfig.chartType == "line" || newConfig.chartType == "bar")
        
        if (this.isTimeSeries) {
            // In time series, we need to sort by time
            finalConfig = {
                name: this.name,
                sort: [{
                    [config.timeField]: "asc"
                }],
                group: [config.timeField],
                config
            }
        } else {
            if (config.chartType != "number") {
                finalConfig = {
                    name: this.name,
                    group: [config.categoryField],
                    config
                }
            }
            else {
                finalConfig = {
                    name: this.name,
                    group: [],
                    config
                }
            }
        }

        // Apply the new config locally
        if (!this.record) {
            Object.assign(this, finalConfig)
        }

        // Store the new config in the record
        await this.updateConfig(finalConfig, needsDataReload)
    }

    /**
     * 
     * RENDERING THE CHART
     * 
     */

    /**
     * Consolidates data for temporal display in Chart.js
     * 
     * @param {Array} rawData - Array of raw data [{ x: <date>, y: <value> }]
     * @param {String} interval - Sampling interval ("day", "week", "month", "quarter", "year")
     * @param {String} operation - Aggregation operation ("count", "sum", "average")
     * @returns {Array} - Consolidated dataset for Chart.js
     */
    _consolidateData(rawData, interval, operation) {
        // Parse a date from various formats (ISO string, Date object, etc.)
        const parseDate = (date) => {
            if (typeof date === "string") {
                // Convert to Date object regardless of the string format
                return new Date(date)
            }
            if (date instanceof Date) {
                return date
            }
            throw new Error("Invalid date format")
        }

        // Format the date into a key based on the specified interval
        const formatKey = (date, interval) => {
            const parsedDate = parseDate(date)
            const year = parsedDate.getFullYear()
            const month = (parsedDate.getMonth() + 1).toString().padStart(2, "0")
            const day = parsedDate.getDate().toString().padStart(2, "0")

            switch (interval) {
                case "day":
                    return `${year}-${month}-${day}`

                case "week": {
                    const firstDayOfYear = new Date(year, 0, 1)
                    const weekNumber = Math.ceil((((parsedDate - firstDayOfYear) / 86400000) + firstDayOfYear.getDay() + 1) / 7)
                    return `${year}-W${weekNumber}`
                }
                case "month":
                    return `${year}-${month}`

                case "quarter":
                    const qMonth = Math.ceil((parsedDate.getMonth() + 1) / 3)
                    return `${year}-Q${qMonth}`

                case "year":
                    return `${year}`

                default:
                    throw new Error("Unsupported interval")
            }
        }

        // Group data by the calculated keys
        const groupedData = rawData.reduce((acc, {
            x,
            y
        }) => {
            const key = formatKey(x, interval)
            if (!acc[key]) {
                acc[key] = {
                    total: 0,
                    count: 0
                }
            }
            acc[key].total += y
            acc[key].count += 1
            return acc
        }, {})

        // Convert grouped data into a Chart.js-compatible dataset
        return Object.entries(groupedData).map(([key, {
            total,
            count
        }]) => {
            const value = operation === "average" ? total / count : total
            return {
                x: key,
                y: value
            }
        })
    }

    /**
     * Render the chart / number chart
     * 
     * @private
     * @ignore
     * @returns this
     */
    _render() {
        if (this.chartType == "number") return this._renderNumber()
        return this._renderChart()
    }

    /**
     * Render a simple "number" chart
     * 
     * @private
     * @ignore
     * @returns this
     */
    _renderNumber() {
        // Replace the chart by a number
        if (this.chart) {
            this.chart.destroy()
            this.chart = null
        }

        // Reset container
        this.chartContainer.innerHTML = ""
        this.chartContainer.classList.remove("chartview-container-empty")

        // Aggregate data according to the operation
        let operation = (this.operationType == "count") ? "count" : this.summaryOperation
        const valueField = this.valueField
        let total = 0

        // Get the field's unit, if any
        let unit
        let precision
        if (operation != "count") {
            const field = this.model.getField(this.valueField)
            unit = field.unit || ""
            precision = field.precision || 0
        }

        switch (operation) {
            case "count":
                total = this.collection.records.length
                break
            case "sum":
                total = this.collection.records.reduce((acc, record) => acc + record[valueField] || 0, 0)
                break
            case "average":
                total = this.collection.records.reduce((acc, record) => acc + record[valueField] || 0, 0) / this.collection.records.length
                break
            default:
        }

        this.number = createBlock({
            target: this.chartContainer,
            class: "chartview-number",
            backgroundColor: this.labelColor + "20",
            items: [
                {
                    type: "html",
                    color: this.labelColor,
                    html: total.format(precision) + (unit ? ` <span class="chartview-unit">${unit}</span>` : "")
                }
            ],
            events: {
                click: () => this.showRecords()
            }
        }).render()
        return this
    }

    /**
     * Render a chart using Chart.js
     * 
     * @private
     * @ignore
     * @returns this
     */    
    _renderChart() {
        /**
         * datasource
         * filter
         * sort
         * group
         * title
         * subtitle
         * 
         * number:
         *  color
         *  values:
         *      count
         *      summary
         *          sum
         *          avg
         *          (+ median, min, max)     
         * bar:
         *  x-axis: field
         *      time:
         *          unit (week, month, quarter, year)
         *          format
         *  y-axis: count
         *  categories
         *  values:
         *      count
         *      summary
         *          sum
         *          avg
         *          (+ median, min, max)
         *  color
         *  size (small medium large)
         *  orientation
         *  show records count on chart
         * 
         * pie:
         *  categories (= group)
         *  values:
         *      count
         *      summary
         *         sum
         *         avg
         *         (+ median, min, max)
         *  size
         * 
         */
        if (this.collection.group.length === 0) {
            // No group: can't render a Chart view
            this.chartContainer.classList.remove("chartview-chart-empty")
            this.chartContainer.innerHTML = `<div class="chartview-help">${txtTitleCase("#chart help")}</div>`

            // Destroy the chart if it exists
            if (this.chart) {
                this.chart.destroy()
                this.chart = null
            }
            else if (this.number) {
                this.number.deepDelete()
                this.number = null
            }

            return this

        } else {

            // If there are no records, show the "empty" icon and exit
            if (this.collection.records.length == "0") {
                this.chartContainer.classList.add("chartview-container-empty")
                return this
            }

            // Reset container
            this.chartContainer.classList.remove("chartview-container-empty")

            // Get data from the collection
            let sourceData = this.collection.records.filter(record => record.$type == "group")

            // Normalize data to [{x: "foo", y: 100}, ...]
            let xyData
            let operation = (this.operationType == "count") ? "count" : this.summaryOperation
            const valueField = this.valueField
            
            // TODO: implement valueRenderer compatibility with canvas which does not support HTML tags
            // const categoryField = this.model.getField(this.categoryField)
            // const renderer = categoryField.valueRenderer
            const renderer = false

            switch (operation) {
                case "count":
                    if (!renderer) {
                        xyData = sourceData.map(rec => {
                            return {
                                x: "" + rec.$name,
                                y: rec.$size
                            }
                        })
                    }
                    else {
                        xyData = sourceData.map(rec => {
                            return {
                                x: renderer({
                                    value: rec.$name,
                                    record: rec
                                }),
                                y: rec.$size
                            }
                        })
                    }
                    break

                case "sum":
                    if (!renderer) {
                        xyData = sourceData.map(rec => {
                            return {
                                x: "" + rec.$name,
                                y: rec[valueField]?.sum || 0
                            }
                        })
                    }
                    else {
                        xyData = sourceData.map(rec => {
                            return {
                                x: renderer(rec.$name),
                                y: rec[valueField]?.sum || 0
                            }
                        })
                    }
                    break
                    
                case "average":
                    if (!renderer) {
                        xyData = sourceData.map(rec => {
                            return {
                                x: "" + rec.$name,
                                y: rec[valueField]?.avg || 0
                            }
                        })
                    }
                    else {
                        xyData = sourceData.map(rec => {
                            return {
                                x: renderer(rec.$name),
                                y: rec[valueField]?.avg || 0
                            }
                        })
                    }

                    break
                default:
            }

            // Consolidate data for time series
            let normalizedData = xyData
            if (this.isTimeSeries) normalizedData = this._consolidateData(xyData, this.timePeriod, operation)

            // Filters out empty categories
            normalizedData = normalizedData.filter(record => record.x !== "" && record.x !== "undefined")

            // Get the color of each category
            let groupFieldId = this.collection.group[0]
            const startIndex = 0//Math.floor(Math.random() * 20)
            let colors = normalizedData.map((record, index) => {
                return this._getCategoryColor(groupFieldId, record.x, startIndex + index)
            })

            // Build the chart data
            const data = {
                datasets: [{
                    data: normalizedData,
                    borderWidth: 1,
                    borderRadius: 5,
                    backgroundColor: colors
                }]
            }

            // Plugin to add margin to the legend
            // const legendMargin = {
            //     id: "legendMargin",
            //     afterInit(chart, args, plugins) {
            //         const originalFit = chart.legend.fit
            //         const margin = plugins.margin || 0
            //         chart.legend.fit = function fit() {
            //             if (originalFit) originalFit.call(this)
            //             this.height += margin * 2
            //             this.width += margin * 2
            //             return
            //         }
            //     }
            // }

            // Build chart plugins property
            const currentChartType = this.chartType
            const showLabels = this.showLabels
            const showValues = this.showValues
            const displayLabels = (this.showValues && this.chartType != "line") || (this.showLabels && (this.chartType == "pie" || this.chartType == "doughnut"))
            const legendTitleFieldId = (this.isTimeSeries) ? this.timeField : this.categoryField
            const legendTitleField = (legendTitleFieldId) ? this.model.getField(legendTitleFieldId) : ""
            const legendText = (legendTitleField) ? legendTitleField.label : txtTitleCase("legend")

            let plugins = {

                // Adjust tooltip content depending on the chart type
                tooltip: {
                    callbacks: {
                        label: function (tooltipItem) {
                            const label = tooltipItem.raw.x || ''
                            const value = tooltipItem.raw.y || 0
                            if (currentChartType == "line" || currentChartType == "bar") return value
                            return `${label}: ${value}`
                        }
                    }
                },

                // Legend setup
                legend: {
                    display: this.showLegend,
                    position: this.legendPosition,
                    title: {
                        display: true,
                        text: legendText,
                        font: {
                            weight: "bold",
                            size: 14
                        },
                    },
                    labels: {
                        boxWidth: 10,
                        boxHeight: 10,
                        generateLabels: function (chart) {
                            const dataset = chart.data.datasets[0]
                            return dataset.data.map((record, index) => ({
                                datasetIndex: 0,
                                text: record.x,
                                fillStyle: colors[index],
                                textAlign: "left",
                                borderRadius: {
                                    topLeft: 3,
                                    topRight: 3,
                                    bottomLeft: 3,
                                    bottomRight: 3
                                }
                            }))
                        }
                    },
                    onClick: (event, legendItem, legend) => {
                        kiss.context.chartLegendClicked = true
                        const category = legendItem.text
                        if (this.isTimeSeries) return // Not implemented yet
                        this.showRecords(category)
                    }
                },

                // Data labels setup (plugin)
                datalabels: {
                    display: displayLabels,
                    align: "center",
                    anchor: (this.centerLabels) ? "center" : "end",
                    color: this.labelColor,
                    font: {
                        weight: "normal"
                    },
                    formatter: (value) => {
                        if (currentChartType == "pie" || currentChartType == "doughnut") {
                            if (showLabels && showValues) return value.x + ": " + Math.round(value.y || 0)
                            if (showLabels) return value.x
                            if (showValues) return Math.round(value.y || 0)
                        }
                        return Math.round(value.y || 0)
                    }
                }
            }

            // Add legend margin if needed
            // if (this.showLegend) {
            //     plugins.legendMargin = {
            //         margin: 20 // Only works for top and left positions at the moment
            //     }
            // }

            // Build chart options property
            let options = {
                scales: {
                    y: {
                        display: (currentChartType == "pie" || currentChartType == "doughnut") ? false : true,
                        beginAtZero: this.startAtZero
                    }
                },
                responsive: true,
                maintainAspectRatio: false,
                normalized: true,
                animation: false,
                plugins
            }

            if (this.chartType == "pie" || this.chartType == "doughnut") {
                // Key for pie and doughnut chart
                options.parsing = {
                    key: "y"
                }
            } else if (this.isTimeSeries) {
                // X-axis for time series
                options.scales.x = {
                    type: "time",
                    time: {
                        unit: this.timePeriod,
                        displayFormats: {
                            day: "DD/MM/YYYY",
                            week: "YYYY [W]WW",
                            year: "YYYY",
                            quarter: "[Q]Q-YYYY"
                        }
                    }
                }

                // Adjust scale according to the time period
                if (this.timePeriod == "week") {
                    // options.scales.x.time.parser = "YYYY-[W]WW"
                }
                else if (this.timePeriod == "quarter") {
                    options.scales.x.time.parser = "YYYY-[Q]Q"
                }
            }

            // Compute the chart width
            this._initChartSize()

            options.onClick = (event, elements) => {
                if (elements.length > 0) {
                    const element = elements[0].element
                    const context = element.$context
                    const category = context.raw.x
                    if (this.isTimeSeries) return // Not implemented yet
                    this.showRecords(category)
                }
            }

            if (this.chart) {
                // The chart already exists, we just update it
                this.chart.refresh({
                    chartType: this.chartType,
                    width: this.chartWidth,
                    height: this.chartHeight,
                    useDataLabels: displayLabels,
                    useMoment: true,
                    data,
                    options,
                    useCDN: this.useCDN
                    // plugins: (this.showLegend) ? [legendMargin]: []
                })
            } else {
                // The chart doesn't exist, we create it
                this.chartContainer.innerHTML = ""
                this.chart = createChart({
                    target: this.chartContainer,
                    chartType: this.chartType,
                    width: this.chartWidth,
                    height: this.chartHeight,
                    useDataLabels: displayLabels,
                    useMoment: true,
                    data,
                    options,
                    useCDN: this.useCDN
                    // plugins: (this.showLegend) ? [legendMargin]: []
                })

                this.chart.render()
            }

            // Manage when the user clicks on the chart but not a colored element of the chart
            this.chart.onclick = (event) => {
                setTimeout(() => {
                    if (kiss.context.chartLegendClicked == true) {
                        kiss.context.chartLegendClicked = false
                        return
                    }
    
                    const elements = this.chart.chart.getElementsAtEventForMode(event, "nearest", { intersect: true }, false)
                    if (elements.length === 0) {
                        publish("EVT_CHART_CLICKED", {
                            chartId: this.id,
                            event
                        })
                    }
                }, 50)
            }

        }
        return this        
    }

    /**
     * Get the color of a category, if any
     * 
     * @param {string} groupFieldId 
     * @param {*} columnValue 
     * @returns {string} The color of the category
     */
    _getCategoryColor(groupFieldId, columnValue, index) {
        const field = this.model.getField(groupFieldId)
        const options = field.options || []
        const randomColor = "#" + kiss.global.palette[(index * 2) % 40]
        if (Array.isArray(options)) {
            const option = options.find(option => option.value == columnValue)
            return (option) ? (option.color || randomColor) : randomColor
        }
        return randomColor
    }

    /**
     * Render the toolbar
     * 
     * @private
     * @ignore
     */
    _renderToolbar() {
        if (this.isToolbarRendered) return

        // Actions button
        createButton({
            hidden: this.showActions === false,
            target: "actions:" + this.id,
            icon: "fas fa-chevron-down",
            iconColor: this.color,
            width: "3.2rem",
            border: "none",
            boxShadow: "none",
            action: (event) => {
                createMenu({
                    left: event.x - 10,
                    top: event.y - 10,
                    items: [
                        // Setup
                        {
                            text: txtTitleCase("setup the chart"),
                            icon: "fas fa-cog",
                            iconColor: this.color,
                            action: () => this.showSetupWindow()
                        },
                        // Refresh
                        {
                            text: txtTitleCase("refresh"),
                            icon: "fas fa-undo-alt",
                            iconColor: this.color,
                            action: () => this.reload()
                        },
                        // Download image as PNG
                        {   hidden: this.chartType == "number",
                            text: txtTitleCase("download image"),
                            icon: "fas fa-image",
                            iconColor: this.color,
                            action: () => {
                                if (!this.chart) return
                                this.chart.downloadBase64Image("image/png", 1)
                            }
                        },

                        // Delete the chart from a dashboard
                        (this.dashboard) ? "-" : "",
                        (this.dashboard) ? {
                            text: txtTitleCase("delete"),
                            icon: "fas fa-trash",
                            iconColor: "var(--red)",
                            action: () => publish("EVT_DASHBOARD_CHART_DELETED", this.id)
                        } : ""
                    ]
                }).render()
                
            }
        }).render()

        // Flag the toolbar as "rendered", so that the method _renderToolbar() is idempotent
        this.isToolbarRendered = true
    }
}

// Create a Custom Element and add a shortcut to create it
customElements.define("a-chartview", kiss.ui.ChartView)

/**
 * Shorthand to create a new Chart view. See [kiss.ui.ChartView](kiss.ui.ChartView.html)
 * 
 * @param {object} config
 * @returns HTMLElement
 */
const createChartView = (config) => document.createElement("a-chartview").init(config)

;