# sql-chart-builder/3.0.4/asset/admin.js

SQL Chart Builder, version 3.0.4. 589 lines.

- Page: https://pluginprobe.com/plugins/sql-chart-builder/3.0.4/code/asset/admin.js
- Raw: https://pluginprobe.com/plugins/sql-chart-builder/3.0.4/raw/asset/admin.js
- Modified: 2026-09-14T09:11:30+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/sql-chart-builder/3.0.4/code/asset/admin.js#L10-L20`.

```javascript
/* SQL Chart Builder 3.0 — admin chart builder UI */
jQuery(function ($) {
    'use strict';

    var $app = $('#gvnsql-app');
    if (!$app.length) return;

    var cfg = window.gvnSqlBuilder || { tables: [], prefix: 'wp_', ajaxurl: window.ajaxurl, nonce: '' };
    var $sql = $('#guaven_sqlcharts_code');

    /* ============ tabs ============ */
    $('#gvnsql-tabs').on('click', '.gvnsql-tab', function () {
        var tab = $(this).data('tab');
        $('.gvnsql-tab').removeClass('active');
        $(this).addClass('active');
        $('.gvnsql-section').removeClass('active').filter('[data-tab="' + tab + '"]').addClass('active');
    });

    /* ============ chart type cards + guides ============ */
    $app.on('change', '.gvnsql-typecard input[type=radio]', function () {
        var type = $(this).val();
        $('.gvnsql-typecard').removeClass('selected');
        $(this).closest('.gvnsql-typecard').addClass('selected');
        $('.gvnsql-guide').hide().filter('[data-guide="' + type + '"]').show();
    });

    $app.on('click', '.gvnsql-use-example', function () {
        var $btn = $(this);
        if ($.trim($sql.val()) !== '' && !window.confirm('Replace the current SQL query with the example?')) return;
        $sql.val($btn.data('sql'));
        $('#guaven_sqlcharts_xarg_s').val($btn.data('x'));
        $('#guaven_sqlcharts_yarg_s').val($btn.data('y'));
        if ($('#guaven_sqlcharts_xarg_l').val() === '') $('#guaven_sqlcharts_xarg_l').val($btn.data('x'));
        if ($('#guaven_sqlcharts_yarg_l').val() === '') $('#guaven_sqlcharts_yarg_l').val($btn.data('yl'));
        prefetchTablesInQuery();
        refreshColMapSelects();
        refreshFilterNameSelects();
    });

    /* ============ insert-at-caret helper ============ */
    function insertAtCaret(text) {
        var el = $sql.get(0);
        el.focus();
        var start = el.selectionStart, end = el.selectionEnd, val = el.value;
        // add a space before if gluing to a word character
        if (start > 0 && /[\w)]/.test(val.charAt(start - 1)) && !/^[\s,)]/.test(text)) text = ' ' + text;
        el.value = val.substring(0, start) + text + val.substring(end);
        el.selectionStart = el.selectionEnd = start + text.length;
        $sql.trigger('input');
    }

    /* ============ toolbar ============ */
    $('#gvnsql-toolbar').on('click', '.gvnsql-kw', function () {
        insertAtCaret($(this).data('insert'));
    });

    var $tableSel = $('#gvnsql-table-select'),
        $colSel = $('#gvnsql-column-select'),
        $tagSel = $('#gvnsql-tag-select');

    $.each(cfg.tables, function (i, t) {
        $tableSel.append($('<option>').val(t).text(t));
    });

    $tableSel.on('change', function () {
        var t = this.value;
        if (!t) return;
        insertAtCaret(t + ' ');
        loadColumns(t);
        this.selectedIndex = 0;
    });

    $colSel.on('change', function () {
        if (!this.value) return;
        insertAtCaret(this.value + ' ');
        this.selectedIndex = 0;
    });

    var defaultTags = ['{current_user_id}', '{current_user_login}', '{current_user_email}', '{current_user_display_name}'];
    function rebuildTagSelect() {
        $tagSel.find('option:not(:first)').remove();
        var tags = defaultTags.slice();
        $('#gvnsql-filters-rows .gvnsql-filter-name').each(function () {
            var name = $.trim(this.value);
            if (name) tags.push('{' + name + '}');
        });
        $.each(tags, function (i, t) { $tagSel.append($('<option>').val(t).text(t)); });
    }
    $tagSel.on('change', function () {
        if (!this.value) return;
        insertAtCaret(this.value + ' ');
        this.selectedIndex = 0;
    });

    /* ============ schema: column fetching + datalist ============ */
    var columnCache = {}; // table => [ {name,type}, ... ]

    function loadColumns(table, done) {
        if (columnCache[table]) {
            refreshColumnUI();
            if (done) done(columnCache[table]);
            return;
        }
        $.post(cfg.ajaxurl, { action: 'gvnsql_get_columns', nonce: cfg.nonce, table: table }, function (res) {
            if (res && res.success) {
                columnCache[table] = res.data;
                refreshColumnUI();
                if (done) done(res.data);
            }
        });
    }

    function allKnownColumns() {
        var names = {};
        $.each(columnCache, function (table, cols) {
            $.each(cols, function (i, c) { names[c.name] = true; });
        });
        return Object.keys(names);
    }

    function refreshColumnUI() {
        var $dl = $('#gvnsql-columns').empty();
        $colSel.find('option:not(:first)').remove();
        $.each(columnCache, function (table, cols) {
            var $group = $('<optgroup>').attr('label', table);
            $.each(cols, function (i, c) {
                $dl.append($('<option>').val(c.name));
                $group.append($('<option>').val(c.name).text(c.name + '  (' + c.type + ')'));
            });
            $colSel.append($group);
        });
        refreshColMapSelects();
    }

    function prefetchTablesInQuery() {
        var text = $sql.val();
        if (!text) return;
        var found = 0;
        $.each(cfg.tables, function (i, t) {
            if (found >= 6) return false;
            if (text.indexOf(t) !== -1 && !columnCache[t]) { loadColumns(t); found++; }
        });
    }
    prefetchTablesInQuery();
    rebuildTagSelect();

    /* ============ SQL query parsing (columns/tags used by the mapping & filter dropdowns) ============ */

    function splitTopLevel(str, sep) {
        var out = [], depth = 0, cur = '';
        for (var i = 0; i < str.length; i++) {
            var ch = str.charAt(i);
            if (ch === '(') depth++;
            else if (ch === ')') depth--;
            if (ch === sep && depth === 0) { out.push(cur); cur = ''; }
            else cur += ch;
        }
        out.push(cur);
        return out;
    }

    function parseQueryInfo() {
        var text = $sql.val() || '';
        var selectCols = [], whereCols = [], tags = [];
        $.each(text.split(';'), function (qi, q) {
            var m = q.match(/select\s+([\s\S]*?)\s+from\b/i);
            if (m) {
                $.each(splitTopLevel(m[1], ','), function (i, expr) {
                    expr = $.trim(expr);
                    if (!expr) return;
                    var name = null;
                    var alias = expr.match(/\s(?:as\s+)?`?([a-z_][a-z0-9_]*)`?\s*$/i);
                    if (alias && !/^(asc|desc|distinct)$/i.test(alias[1])) name = alias[1];
                    else {
                        var plain = expr.match(/^`?([a-z_][a-z0-9_]*)`?$/i) || expr.match(/\.`?([a-z_][a-z0-9_]*)`?$/i);
                        if (plain) name = plain[1];
                    }
                    if (name && $.inArray(name, selectCols) === -1) selectCols.push(name);
                });
            }
            var w = q.match(/\bwhere\b([\s\S]*?)(\bgroup\s+by\b|\border\s+by\b|\blimit\b|$)/i);
            if (w) {
                var re = /([a-z_][a-z0-9_.]*)\s*(?:=|!=|<>|<=|>=|<|>|\blike\b|\bin\b|\bbetween\b)/gi, mm;
                while ((mm = re.exec(w[1]))) {
                    var col = mm[1].split('.').pop();
                    if ($.inArray(col, whereCols) === -1) whereCols.push(col);
                }
            }
            var tre = /\{([a-z_][a-z0-9_]*)\}/gi, tm;
            while ((tm = tre.exec(q))) {
                if ($.inArray('{' + tm[1] + '}', defaultTags) === -1 && $.inArray(tm[1], tags) === -1) tags.push(tm[1]);
            }
        });
        return { selectCols: selectCols, whereCols: whereCols, tags: tags };
    }

    /* ============ column mapping dropdowns (X / Y axis) ============ */

    function refreshColMapSelects() {
        var info = parseQueryInfo();
        $('.gvnsql-colselect').each(function () {
            var $sel = $(this);
            var $input = $('#' + $sel.data('target'));
            var cur = $.trim($input.val());
            var opts = info.selectCols.slice();
            $.each(info.whereCols.concat(allKnownColumns()), function (i, c) {
                if ($.inArray(c, opts) === -1) opts.push(c);
            });
            $sel.empty().append($('<option>').val('').text('— select a column —'));
            if (info.selectCols.length) {
                var $g = $('<optgroup label="Columns in your SELECT">');
                $.each(info.selectCols, function (i, c) { $g.append($('<option>').val(c).text(c)); });
                $sel.append($g);
            }
            var rest = opts.filter(function (c) { return $.inArray(c, info.selectCols) === -1; });
            if (rest.length) {
                var $g2 = $('<optgroup label="Other known columns">');
                $.each(rest, function (i, c) { $g2.append($('<option>').val(c).text(c)); });
                $sel.append($g2);
            }
            $sel.append($('<option>').val('__custom__').text('Custom / type manually…'));
            if (cur && $.inArray(cur, opts) !== -1) $sel.val(cur);
            else if (cur) $sel.val('__custom__');
            else $sel.val('');
            syncColCustom($sel, opts.length);
        });
    }

    function syncColCustom($sel, optCount) {
        var $input = $('#' + $sel.data('target'));
        var custom = $sel.val() === '__custom__' || optCount === 0;
        $input.toggle(custom);
        $sel.toggle(optCount > 0);
    }

    $app.on('change', '.gvnsql-colselect', function () {
        var $sel = $(this);
        var $input = $('#' + $sel.data('target'));
        if ($sel.val() === '__custom__') {
            $input.show().trigger('focus');
        } else {
            if ($sel.val() !== '') $input.val($sel.val());
            $input.hide();
        }
    });

    /* ============ SQL autocomplete ============ */
    var KEYWORDS = ['SELECT', 'FROM', 'WHERE', 'GROUP BY', 'ORDER BY', 'LIMIT', 'INNER JOIN', 'LEFT JOIN',
        'RIGHT JOIN', 'JOIN', 'ON', 'AS', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS NULL',
        'IS NOT NULL', 'DISTINCT', 'COUNT(*)', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'HAVING', 'UNION',
        'DESC', 'ASC', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'SUBSTRING', 'SUBSTR', 'CONCAT',
        'DATE_FORMAT', 'NOW()', 'CURDATE()', 'YEAR', 'MONTH', 'DAY', 'INTERVAL', 'DATE_SUB', 'DATE_ADD'];

    var $editorWrap = $sql.closest('.gvnsql-editorwrap');
    var $ac = $('<div class="gvnsql-ac" style="display:none"></div>').appendTo($editorWrap);
    var acItems = [], acIndex = -1, acWordStart = 0;

    // mirror div used to find the pixel position of the caret inside the textarea
    var $mirror = $('<div class="gvnsql-mirror" aria-hidden="true"></div>').appendTo($editorWrap);
    function caretCoordinates() {
        var el = $sql.get(0);
        var style = window.getComputedStyle(el);
        ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing',
         'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
         'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
         'boxSizing'].forEach(function (p) { $mirror.css(p, style[p]); });
        $mirror.css('width', $sql.outerWidth() + 'px');
        var before = el.value.substring(0, el.selectionStart);
        $mirror.text(before);
        var $marker = $('<span>​</span>');
        $mirror.append($marker);
        var mpos = $marker.position();
        var lineH = parseInt(style.lineHeight, 10);
        if (isNaN(lineH)) lineH = Math.round(parseInt(style.fontSize, 10) * 1.4) || 18;
        return {
            left: mpos.left,
            top: mpos.top + lineH - el.scrollTop
        };
    }

    function currentWord() {
        var el = $sql.get(0);
        var pos = el.selectionStart, val = el.value, start = pos;
        while (start > 0 && /[\w.{}]/.test(val.charAt(start - 1))) start--;
        return { start: start, word: val.substring(start, pos) };
    }

    function buildCandidates(word) {
        var lower = word.toLowerCase();
        var out = [];

        // table.column completion
        if (word.indexOf('.') !== -1) {
            var parts = word.split('.');
            var table = parts[0], colPrefix = (parts[1] || '').toLowerCase();
            if ($.inArray(table, cfg.tables) !== -1) {
                if (!columnCache[table]) loadColumns(table);
                $.each(columnCache[table] || [], function (i, c) {
                    if (!colPrefix || c.name.toLowerCase().indexOf(colPrefix) === 0)
                        out.push({ text: table + '.' + c.name, label: table + '.' + c.name, kind: 'column' });
                });
            }
            return out.slice(0, 12);
        }

        $.each(KEYWORDS, function (i, k) {
            if (!lower || k.toLowerCase().indexOf(lower) === 0)
                out.push({ text: k + ' ', label: k, kind: 'keyword' });
        });
        $.each(cfg.tables, function (i, t) {
            if (!lower || t.toLowerCase().indexOf(lower) === 0)
                out.push({ text: t + ' ', label: t, kind: 'table', table: t });
        });
        $.each(allKnownColumns(), function (i, c) {
            if (!lower || c.toLowerCase().indexOf(lower) === 0)
                out.push({ text: c + ' ', label: c, kind: 'column' });
        });
        $.each(defaultTags, function (i, t) {
            if (!lower || t.toLowerCase().indexOf('{' + lower) === 0 || t.toLowerCase().indexOf(lower) === 1)
                out.push({ text: t + ' ', label: t, kind: 'tag' });
        });
        $('#gvnsql-filters-rows .gvnsql-filter-name').each(function () {
            var name = $.trim(this.value);
            if (name && (!lower || name.toLowerCase().indexOf(lower) === 0 || ('{' + name).toLowerCase().indexOf(lower) === 0))
                out.push({ text: '{' + name + '} ', label: '{' + name + '}', kind: 'tag' });
        });
        return out.slice(0, 12);
    }

    function showAC(force) {
        var cw = currentWord();
        if (!force && cw.word.length < 1) { hideAC(); return; }
        acItems = buildCandidates(cw.word);
        if (!acItems.length) { hideAC(); return; }
        acWordStart = cw.start;
        acIndex = 0;
        var html = '';
        $.each(acItems, function (i, item) {
            html += '<div class="gvnsql-ac-item' + (i === 0 ? ' active' : '') + '" data-i="' + i + '">' +
                '<span class="gvnsql-ac-kind gvnsql-ac-kind-' + item.kind + '">' + item.kind.charAt(0).toUpperCase() + '</span>' +
                $('<span>').text(item.label).html() + '</div>';
        });
        $ac.html(html).show();
        var pos = caretCoordinates();
        var wrapH = $editorWrap.height();
        var top = Math.min(pos.top + 4, wrapH - 10);
        $ac.css({ left: Math.min(pos.left, $editorWrap.width() - $ac.outerWidth() - 8) + 'px', top: top + 'px' });
    }

    function hideAC() { $ac.hide(); acItems = []; acIndex = -1; }

    function applyAC(i) {
        if (!acItems[i]) return;
        var el = $sql.get(0);
        var item = acItems[i];
        var val = el.value, pos = el.selectionStart;
        el.value = val.substring(0, acWordStart) + item.text + val.substring(pos);
        el.selectionStart = el.selectionEnd = acWordStart + item.text.length;
        hideAC();
        el.focus();
        if (item.kind === 'table') loadColumns(item.table);
    }

    $sql.on('input', function () { showAC(false); });

    // re-parse the query for the mapping/filter dropdowns while the user types
    var gvnsqlParseTimer = null;
    $sql.on('input', function () {
        clearTimeout(gvnsqlParseTimer);
        gvnsqlParseTimer = setTimeout(function () {
            refreshColMapSelects();
            refreshFilterNameSelects();
        }, 600);
    });
    $sql.on('click', hideAC);
    $sql.on('blur', function () { setTimeout(hideAC, 200); });

    $sql.on('keydown', function (e) {
        if (e.ctrlKey && e.code === 'Space') { e.preventDefault(); showAC(true); return; }
        if (!acItems.length) return;
        if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
            e.preventDefault();
            acIndex = (acIndex + (e.key === 'ArrowDown' ? 1 : acItems.length - 1)) % acItems.length;
            $ac.find('.gvnsql-ac-item').removeClass('active').eq(acIndex).addClass('active');
        } else if (e.key === 'Enter' || e.key === 'Tab') {
            e.preventDefault();
            applyAC(acIndex);
        } else if (e.key === 'Escape') {
            hideAC();
        }
    });

    $ac.on('mousedown', '.gvnsql-ac-item', function (e) {
        e.preventDefault();
        applyAC($(this).data('i'));
    });

    /* ============ dynamic filters builder ============ */
    var $filterRows = $('#gvnsql-filters-rows'),
        $filterRaw = $('#guaven_sqlcharts_variables');

    function filterRowHtml(f) {
        f = $.extend({ name: '', def: '', label: '', type: 'text' }, f);
        var types = { text: 'Text', number: 'Number', date: 'Date (with datepicker)', '': 'No input (tag only)' };
        var opts = '';
        $.each(types, function (v, l) {
            opts += '<option value="' + v + '"' + (v === f.type ? ' selected' : '') + '>' + l + '</option>';
        });
        return $('<div class="gvnsql-filter-row">' +
            '<select class="gvnsql-filter-name-select" title="Tag name — suggested from your SQL query"></select>' +
            '<input type="text" class="gvnsql-filter-name" placeholder="tag name (e.g. fromdate)">' +
            '<select class="gvnsql-filter-type">' + opts + '</select>' +
            '<input type="text" class="gvnsql-filter-label" placeholder="Label above chart">' +
            '<input type="text" class="gvnsql-filter-default" placeholder="Default value">' +
            '<button type="button" class="button gvnsql-filter-insert" title="Insert {tag} into the SQL query at the cursor">Insert into query</button>' +
            '<button type="button" class="button-link gvnsql-filter-remove" title="Remove">&times;</button>' +
            '</div>')
            .find('.gvnsql-filter-name').val(f.name).end()
            .find('.gvnsql-filter-label').val(f.label).end()
            .find('.gvnsql-filter-default').val(f.def).end();
    }

    function parseFilters(raw) {
        var out = [];
        $.each($.trim(raw || '').split('|'), function (i, chunk) {
            chunk = $.trim(chunk);
            if (!chunk) return;
            var p = chunk.split('~').map(function (s) { return $.trim(s); });
            if (!p[0]) return;
            out.push({ name: p[0], def: p[1] || '', label: p[2] || '', type: p[3] || '' });
        });
        return out;
    }

    function serializeFilters() {
        var parts = [];
        $filterRows.find('.gvnsql-filter-row').each(function () {
            var $r = $(this);
            var name = $.trim($r.find('.gvnsql-filter-name').val());
            if (!name) return;
            var seg = [name,
                $.trim($r.find('.gvnsql-filter-default').val()),
                $.trim($r.find('.gvnsql-filter-label').val())];
            var type = $r.find('.gvnsql-filter-type').val();
            if (type) seg.push(type);
            parts.push(seg.join('~'));
        });
        $filterRaw.val(parts.join(' | '));
        rebuildTagSelect();
    }

    /* tag-name dropdown per row: suggests {tags} already in the query and WHERE-clause columns */
    function refreshFilterNameSelects() {
        var info = parseQueryInfo();
        $filterRows.find('.gvnsql-filter-row').each(function () {
            var $row = $(this);
            var $sel = $row.find('.gvnsql-filter-name-select');
            var $input = $row.find('.gvnsql-filter-name');
            var cur = $.trim($input.val());
            var opts = [];
            $.each(info.tags.concat(info.whereCols), function (i, t) {
                if ($.inArray(t, opts) === -1) opts.push(t);
            });
            $sel.empty().append($('<option>').val('').text('— choose tag —'));
            if (info.tags.length) {
                var $g = $('<optgroup label="{tags} in your query">');
                $.each(info.tags, function (i, t) { $g.append($('<option>').val(t).text('{' + t + '}')); });
                $sel.append($g);
            }
            var rest = info.whereCols.filter(function (c) { return $.inArray(c, info.tags) === -1; });
            if (rest.length) {
                var $g2 = $('<optgroup label="Columns from WHERE part">');
                $.each(rest, function (i, c) { $g2.append($('<option>').val(c).text(c)); });
                $sel.append($g2);
            }
            $sel.append($('<option>').val('__custom__').text('Custom name…'));
            if (cur && $.inArray(cur, opts) !== -1) $sel.val(cur);
            else if (cur) $sel.val('__custom__');
            else $sel.val('');
            var custom = $sel.val() === '__custom__' || opts.length === 0;
            $input.toggle(custom);
            $sel.toggle(opts.length > 0);
        });
    }

    $filterRows.on('change', '.gvnsql-filter-name-select', function () {
        var $row = $(this).closest('.gvnsql-filter-row');
        var $input = $row.find('.gvnsql-filter-name');
        if (this.value === '__custom__') {
            $input.show().trigger('focus');
        } else {
            if (this.value !== '') $input.val(this.value);
            $input.hide();
        }
        serializeFilters();
    });

    $.each(parseFilters($filterRaw.val()), function (i, f) {
        $filterRows.append(filterRowHtml(f));
    });
    rebuildTagSelect();
    refreshFilterNameSelects();

    $('#gvnsql-add-filter').on('click', function () {
        $filterRows.append(filterRowHtml({}));
        refreshFilterNameSelects();
    });

    $filterRows.on('input change', 'input,select', serializeFilters);
    $filterRows.on('click', '.gvnsql-filter-remove', function () {
        $(this).closest('.gvnsql-filter-row').remove();
        serializeFilters();
    });
    $filterRows.on('click', '.gvnsql-filter-insert', function () {
        var name = $.trim($(this).closest('.gvnsql-filter-row').find('.gvnsql-filter-name').val());
        if (!name) { window.alert('Give the filter a tag name first.'); return; }
        insertAtCaret('{' + name + '} ');
        $('.gvnsql-tab[data-tab="data"]').trigger('click');
    });

    $('#gvnsql-raw-toggle').on('click', function () {
        $('.gvnsql-raw').toggle();
    });
    // raw textarea edited manually -> rebuild rows
    $filterRaw.on('change', function () {
        $filterRows.empty();
        $.each(parseFilters($filterRaw.val()), function (i, f) {
            $filterRows.append(filterRowHtml(f));
        });
        rebuildTagSelect();
    });

    /* ============ colors builder ============ */
    var $colorsRows = $('#gvnsql-colors-rows'),
        $colorsHidden = $('#guaven_sqlcharts_colors');

    function colorRow(value) {
        return $('<span class="gvnsql-color-item">' +
            '<input type="color" value="' + value + '">' +
            '<button type="button" class="button-link gvnsql-color-remove" title="Remove">&times;</button>' +
            '</span>');
    }

    var colorsRawMode = false;

    function serializeColors() {
        if (colorsRawMode) return; // legacy non-hex value: leave it untouched
        var vals = [];
        $colorsRows.find('input[type=color]').each(function () { vals.push(this.value.toUpperCase()); });
        $colorsHidden.val(vals.join(','));
    }

    var existingColors = $.trim($colorsHidden.val());
    if (existingColors) {
        var tokens = existingColors.split(',').map(function (c) { return $.trim(c); }).filter(function (c) { return c !== ''; });
        colorsRawMode = tokens.some(function (c) { return !/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(c); });
        if (colorsRawMode) {
            // legacy format we can't map to color pickers (e.g. "255,0,0") — expose it as editable text
            $colorsHidden.attr('type', 'text').addClass('gvnsql-colors-rawinput');
            $('#gvnsql-add-color').hide();
        } else {
            $.each(tokens, function (i, c) {
                if (/^#[0-9a-f]{3}$/i.test(c)) c = '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];
                $colorsRows.append(colorRow(c.toUpperCase()));
            });
        }
    }

    $('#gvnsql-add-color').on('click', function () {
        // keep in sync with guaven_sqlcharts_default_palette() in functions.php
        var palette = ['#4E79A7', '#F28E2B', '#E15759', '#76B7B2', '#59A14F', '#EDC948', '#B07AA1', '#FF9DA7', '#9C755F', '#BAB0AC'];
        $colorsRows.append(colorRow(palette[$colorsRows.children().length % palette.length]));
        serializeColors();
    });
    $colorsRows.on('input change', 'input[type=color]', serializeColors);
    $colorsRows.on('click', '.gvnsql-color-remove', function () {
        $(this).closest('.gvnsql-color-item').remove();
        serializeColors();
    });

    /* make sure everything is serialized before WP submits the post form */
    $('#post').on('submit', function () {
        serializeFilters();
        serializeColors();
    });

    refreshColMapSelects();
});

```
