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 {Collection} config.collection - The data source collection
 * @param {object} [config.record] - Record to persist the view configuration into the db
 * @param {string} [config.chartType] - pie, bar, line...
 * @param {string} [config.chartValueField] - Field used to display the values
 * @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 {number|string} [config.width]
 * @param {number|string} [config.height]
 * @returns this
 * 
 * ## Generated markup
 * ```
 * <a-chartview class="a-chartview">
 *      <div class="chartview-toolbar">
 *          <!-- Chart view toolbar items -->
 *      </div>
 *      <div class="chartview-chart">
 *          <!-- Embedded chart -->
 *      </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",
     *   chartValueField: "Amount",
     *   group: ["Country"]
     * })
     * 
     * myChartView.render()
     * ```
     */
    constructor() {
        super()
    }

    /**
     * Generates a Chart 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.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"

        // Build chart skeletton markup
        let id = this.id
        this.innerHTML = /*html*/
            `<div class="chartview">
                <div id="chartview-toolbar:${id}" class="chartview-toolbar">
                    <div id="actions:${id}"></div>
                    <div id="setup:${id}"></div>
                    <div id="sort:${id}"></div>
                    <div id="filter:${id}"></div>
                    <div id="refresh:${id}"></div>
                </div>

                <div class="chartview-container">
                    <div class="chartview-title">${this.name}</div>
                    <div class="chartview-chart"></div>
                </div>
            </div>`.removeExtraSpaces()

        // Set chart components
        this.chartView = this.querySelector(".chartview")
        this.chartTitle = this.querySelector(".chartview-title")
        this.chartToolbar = this.querySelector(".chartview-toolbar")
        this.chartContainer = this.querySelector(".chartview-chart")

        this._initChartParams(config)
            ._initSize(config)
            ._initSubscriptions()

        return this
    }

    /**
     * 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.chartValueField = config.chartValueField || this.record.config.chartValueField
            this.chartValueOperation = config.chartValueOperation || this.record.config.chartValueOperation
        } else {
            this.chartType = config.chartType || this.config.chartType
            this.chartValueField = config.chartValueField || this.config.chartValueField
            this.chartValueOperation = config.chartValueOperation || "count"
        }

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

        return this
    }    

    /**
     * Display the setup window to configure the chart:
     * - page 1: choose the chart type
     * - page 2: choose the grouping field
     * - page 3: choose the value field
     */
    showSetupWindow() {
        let chartType
        let chartGroupingField
        let chartValueField
        let chartValueOperation

        // Page 1: choose the chart type
        const page1 = {
            id: "chart-setup-1",
            items: [
                {
                    type: "html",
                    html: txtTitleCase("#chart help 1"),
                    class: "chartview-wizard"
                },
                {
                    layout: "vertical",
                    defaultConfig: {
                        type: "button",
                        margin: 10,
                        height: 80,
                        iconSize: 40,
                        iconColor: "var(--blue)",
                        action: function() {
                            chartType = this.config.chartType
                            $("chart-setup-1").highlightButton(chartType)
                            publish("EVT_CHART_TYPE_CHANGED", chartType)
                        }
                    },
                    items: [
                        {
                            text: txtTitleCase("montrer une répartition, avec peu de catégories"),
                            icon: "fas fa-chart-pie",
                            chartType: "pie"
                        },
                        {
                            text: txtTitleCase("montrer une répartition, avec beaucoup de catégories"),
                            icon: "fas fa-chart-bar",
                            chartType: "bar"
                        },
                        {
                            text: txtTitleCase("montrer une évolution temporelle"),
                            icon: "fas fa-chart-line",
                            chartType: "line"
                        }
                    ]
                }
            ],
            methods: {
                load: () => {
                    chartType = this.chartType
                    $("chart-setup-1").highlightButton(this.chartType)
                },
                validate: () => {
                    if (!chartType) {
                        createNotification("Vous devez choisir un type de graphique")
                        return false
                    }
                    return true
                },
                highlightButton(chartType) {
                    const allButtons = this.querySelectorAll("a-button")
                    allButtons.forEach(button => {
                        if (button.config.chartType != chartType) {
                            button.setColor("var(--button-text)")
                            button.setIconColor("var(--blue)")
                            button.setBackgroundColor("var(--button-background)")
                        }
                        else {
                            button.setColor("#ffffff")
                            button.setIconColor("#ffffff")
                            button.setBackgroundColor("#00aaee")
                        }
                    })
                }
            }
        }

        // Page 2: choose the grouping field
        const page2 = {
            id: "chart-setup-2",
            items: [
                {
                    id: "title-chart-setup-category",
                    type: "html",
                    html: txtTitleCase("Quel champ voulez-vous utiliser pour grouper les données ?"),
                    class: "chartview-wizard",
                    subscriptions: {
                        EVT_CHART_TYPE_CHANGED: function(type) {
                            if (type == "line") return this.hide()
                            this.show()
                        }
                    }
                },
                {
                    id: "title-chart-setup-time",
                    type: "html",
                    html: txtTitleCase("Quel champ voulez-vous utiliser pour l'axe temporel ?"),
                    class: "chartview-wizard",
                    subscriptions: {
                        EVT_CHART_TYPE_CHANGED: function(type) {
                            if (type != "line") return this.hide()
                            this.show()
                        }
                    }                    
                },                
                {
                    type: "select",
                    id: "chart-grouping-field",
                    multiple: false,
                    options: this._groupGetModelFields(),
                    value: this.group,
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color,
                    subscriptions: {
                        EVT_CHART_TYPE_CHANGED: function(type) {
                            if (type == "line") return this.hide()
                            this.show()
                        }
                    }                    
                },
                {
                    type: "select",
                    id: "chart-time-field",
                    multiple: false,
                    options: this.model.getFieldsAsOptions("date"),
                    // value: this.timeField,
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color,
                    subscriptions: {
                        EVT_CHART_TYPE_CHANGED: function(type) {
                            if (type != "line") return this.hide()
                            this.show()
                        }
                    }
                },                
                

                {
                    type: "html",
                    html: txtTitleCase("Quelle opération ?"),
                    class: "chartview-wizard"
                },
                {
                    type: "select",
                    id: "chart-operation-type",
                    multiple: false,
                    options: [
                        { value: "count", text: "Compter" },
                        { value: "summary", text: "Résumer" },
                    ],
                    value: this.chartValueOperation,
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color
                },
                {
                    type: "select",
                    id: "chart-summary-operation",
                    multiple: false,
                    options: [
                        { value: "sum", text: "Somme" },
                        { value: "average", text: "Moyenne" },
                    ],
                    value: this.chartValueOperation,
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color
                },
                {
                    type: "html",
                    html: txtTitleCase("Quel champ pour les valeurs du graphique ?"),
                    class: "chartview-wizard"
                },
                {
                    type: "select",
                    id: "chart-value-field",
                    multiple: false,
                    options: this.model.getFieldsAsOptions("number"),
                    value: this.chartValueField,
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color
                },                           
            ],
            methods: {
                validate: () => {
                    return true
                    chartGroupingField = $("chart-grouping-field").getValue()
                    if (!chartGroupingField) {
                        createNotification("Vous devez choisir un champ pour regrouper vos données")
                        return false
                    }
                    return true
                }
            }
        }

        // Page 3: choose the value field        
        const page3 = {
            id: "chart-setup-3",
            items: [
                {
                    type: "html",
                    html: txtTitleCase("Que voulez-vous utiliser pour les valeurs du graphique ?"),
                    class: "chartview-wizard"
                },
                {
                    type: "select",
                    id: "chart-value-field",
                    multiple: false,
                    options: this.model.getFieldsAsOptions("number"),
                    value: this.chartValueField,
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color
                },
                {
                    type: "select",
                    id: "chart-value-operation",
                    multiple: false,
                    options: [
                        { value: "sum", text: "Somme" },
                        { value: "average", text: "Moyenne" },
                        { value: "count", text: "Nombre" }
                    ],
                    value: this.chartValueOperation,
                    width: "100%",
                    maxHeight: () => kiss.screen.current.height - 200,
                    optionsColor: this.color
                }
            ],
            methods: {
                validate: () => {
                    
                    chartValueField = $("chart-value-field").getValue()
                    chartValueOperation = $("chart-value-operation").getValue()

                    if (!chartValueField) {
                        createNotification("Vous devez choisir un champ numérique pour les valeurs du graphique")
                        return false
                    }
                    return true
                }
            }
        }

        // Build the wizard
        createWizardPanel({
            title: txtTitleCase("#chart setup"),
            icon: "fas fa-cog",
            align: "center",
            verticalAlign: "center",
            draggable: true,
            closable: true,
            modal: true,
            pageValidation: true,
            items: [
                page1,
                page2,
                // page3
            ],
            action: async () => {
                chartGroupingField = $("chart-grouping-field").getValue()
                chartValueField = $("chart-value-field").getValue()
                chartValueOperation = $("chart-value-operation").getValue()

                // Broadcast the new chart setup
                publish("EVT_VIEW_SETUP:" + this.id, {
                    group: [chartGroupingField],
                    chartType,
                    chartValueField,
                    chartValueOperation
                })
            }
        }).render()
    }

    /**
     * 
     * CHART METHODS
     * 
     */

    /**
     * Load data into the chart.
     * 
     * Remark:
     * - rendering time is proportional to the number of cards and visible fields (cards x fields)
     * - rendering takes an average of 0.03 millisecond per card on an Intel i7-4790K
     * 
     * @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`)
        }
    }

    /**
     * Generic method to refresh / re-render the view
     * 
     * Note: used in dataComponent (parent class) showSearchBar method.
     * This method is invoked to refresh the view after a full-text search has been performed
     */
    refresh() {
        this._render()
    }

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

    /**
     * Show the window just under the sorting button
     */
    showSortWindow() {
        let sortButton = $("sort:" + this.id)
        const box = sortButton.getBoundingClientRect()
        super.showSortWindow(box.left, box.top + 40, this.color)
    }

    /**
     * Show the window just under the fields selector button
     */
    showFieldsWindow() {
        let selectionButton = $("select:" + this.id)
        const box = selectionButton.getBoundingClientRect()
        super.showFieldsWindow(box.left, box.top + 40, this.color)
    }

    /**
     * Show the window just under the filter button
     */
    showFilterWindow() {
        super.showFilterWindow(null, null, this.color)
    }

    /**
     * Update the chart size (recomputes its width and height functions)
     */
    updateLayout() {
        if (this.isConnected) {
            this._setWidth()
            this._setHeight()
            this._render()
        }
    }

    /**
     * Initialize chart sizes
     * 
     * @private
     * @ignore
     * @returns this
     */
    _initSize(config) {
        if (config.width) {
            this._setWidth()
        } else {
            this.style.width = this.config.width = "100%"
        }

        if (config.height) {
            this._setHeight()
        } else {
            this.style.height = this.config.height = "100%"
        }
        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._updateOneAndReload(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))
        ])

        return this
    }

    /**
     * Adjust the component width
     * 
     * @ignore
     * @param {(number|string|function)} [width] - The width to set
     */
    _setWidth() {
        let newWidth = this._computeSize("width")

        setTimeout(() => {
            this.style.width = newWidth
            this.chartView.style.width = this.clientWidth.toString() + "px"
        }, 50)
    }

    /**
     * Adjust the components height
     * 
     * @private
     * @ignore
     * @param {(number|string|function)} [height] - The height to set
     */
    _setHeight() {
        let newHeight = this._computeSize("height")
        this.style.height = this.chartView.style.height = newHeight
    }

    /**
     * Update the chart configuration
     * 
     * @private
     * @ignore
     * @param {object} newConfig 
     */
    async _updateConfig(newConfig) {
        if (newConfig.hasOwnProperty("group")) this.group = newConfig.group
        if (newConfig.hasOwnProperty("chartType")) this.chartType = newConfig.chartType
        if (newConfig.hasOwnProperty("chartValueField")) this.chartValueField = newConfig.chartValueField
        if (newConfig.hasOwnProperty("chartValueOperation")) this.chartValueOperation = newConfig.chartValueOperation

        let currentConfig
        if (this.record) {
            currentConfig = this.record.config
        }
        else {
            currentConfig = {
                chartType: this.chartType,
                chartValueField: this.chartValueField,
                chartValueOperation: this.chartValueOperation
            }
        }

        let config = Object.assign(currentConfig, newConfig)
        await this.updateConfig({
            group: this.group,
            config
        })
    }

    /**
     * 
     * RENDERING THE CHART
     * 
     */

    /**
     * Render the chart
     * 
     * @private
     * @ignore
     * @returns this
     */
    _render() {

        /**
         * 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("#kanban help")}</div>`

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

            return this

        } else {

            // Show / hide "empty" icon and header
            if (this.collection.records.length == "0") {
                this.chartContainer.classList.add("chartview-chart-empty")
                return this
            }

            this.chartContainer.classList.remove("chartview-chart-empty")

            log("===================")
            log(this.collection.records)
            log(this.chartValueOperation)

            let data = this.collection.records.filter(record => record.$type == "group")
            let labels = data.map(record => record.$name)
            let datasets = data.map(record => record.$size)
            const valueField = this.chartValueField

            switch(this.chartValueOperation) {
                case "count":
                    datasets = data.map(record => record.$size)
                    break
                case "sum":
                    datasets = data.map(record => record[valueField]?.sum || 0)
                    break
                case "average":
                    datasets = data.map(record => record[valueField]?.avg || 0)
                    break
            }

            // datasets = datasets.map((value, index) => {
            //     return {
            //         x: labels[index],
            //         y: value
            //     }
            // })

            // Compute the chart width
            let width
            let height
            const w = this.chartContainer.clientWidth
            const h = this.chartContainer.clientHeight
            log("!!!!!!!!!!!!!!!!!!!!!!")
            log(w)
            log(h)

            const min = Math.min(w, h)
            log(min)
            
            if (this.chartType == "pie") {
                if (w > h) {
                    log("w > h")
                    
                    width = min
                    height = h
                    console.log(width, height, h, w)
                }
                else {
                    log("w < h")
                    

                    width = w
                    height = min
                    console.log(width, height, h, w)
                }
            }
            else {
                width = "100%"
                height = "100%"
            }

            // Get the color of each category
            let groupFieldId = this.collection.group[0]
            let colors = labels.map(label => {
                return this._getCategoryColor(groupFieldId, label)
            })

            if (this.chart) {
                // The chart already exists, we just update it
                this.chart.refresh({
                    chartType: this.chartType,
                    width,
                    height,
                    data: {
                        labels,
                        datasets: [{
                            label: 'Total workload by Customer',
                            data: datasets,
                            borderWidth: 1,
                            backgroundColor: colors
                        }]
                    },
                    options: {
                        scales: {
                            x: {
                                // type: "time",
                                time: {
                                    unit: "week"
                                }
                            },
                            y: {
                                beginAtZero: true
                            }
                        },
                        responsive: true,
                        maintainAspectRatio: true
                    }
                })
            }
            else {
                // The chart doesn't exist, we create it
                this.chartContainer.innerHTML = ""

                // window.moment.locale("fr")

                this.chart = createChart({
                    target: this.chartContainer,
                    width,
                    height,
                    chartType: this.chartType,
                    data: {
                        // labels,
                        datasets: [{
                            label: 'Count',
                            data: datasets,
                            borderWidth: 1,
                            backgroundColor: colors
                        }]
                    },
                    options: {
                        scales: {
                            x: {
                                // type: "time",
                                // time: {
                                //     unit: "month",
                                //     displayFormats: {
                                //         day: "DD/MM/YYYY",
                                //         quarter: "MM YYYY"
                                //     }
                                // },
                                // adapters: {
                                //     date: {
                                //         locale: window.dateFnsLocales.fr
                                //     }
                                // }
                            },
                            y: {
                                beginAtZero: true
                            }
                        },
                        responsive: true,
                        maintainAspectRatio: true
                    }
                })

                this.chart.render()
            }
        }
        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) {
        const field = this.model.getField(groupFieldId)
        const options = field.options || []
        const option = options.find(option => option.value == columnValue)
        return (option) ? option.color : kiss.tools.getRandomColor(0, 53)
    }

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

        // Actions button
        createButton({
            hidden: this.showActions === false,
            target: "actions:" + this.id,
            tip: txtTitleCase("actions"),
            icon: "fas fa-bolt",
            iconColor: this.color,
            width: 32,
            action: () => this._buildActionMenu()
        }).render()

        // Setup the chart
        createButton({
            hidden: !this.showSetup,
            target: "setup:" + this.id,
            tip: txtTitleCase("setup the chart"),
            icon: "fas fa-cog",
            iconColor: this.color,
            width: 32,
            action: () => this.showSetupWindow()
        }).render()

        // Sorting button
        createButton({
            hidden: !this.canSort,
            target: "sort:" + this.id,
            tip: txtTitleCase("to sort"),
            icon: "fas fa-sort",
            iconColor: this.color,
            width: 32,
            action: () => this.showSortWindow()
        }).render()

        // Filtering button
        createButton({
            hidden: !this.canFilter,
            target: "filter:" + this.id,
            tip: txtTitleCase("to filter"),
            icon: "fas fa-filter",
            iconColor: this.color,
            width: 32,
            action: () => this.showFilterWindow()
        }).render()

        // View refresh button
        if (!kiss.screen.isMobile) {
            createButton({
                target: "refresh:" + this.id,
                tip: txtTitleCase("refresh"),
                icon: "fas fa-undo-alt",
                iconColor: this.color,
                width: 32,
                events: {
                    click: () => this.reload()
                }
            }).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. See [kiss.ui.Chart](kiss.ui.Chart.html)
 * 
 * @param {object} config
 * @returns HTMLElement
 */
const createChartView = (config) => document.createElement("a-chartview").init(config)

;