diff --git a/js/src/classes/Chart.js b/js/src/classes/Chart.js index c13b8adb53..0f1312c27f 100644 --- a/js/src/classes/Chart.js +++ b/js/src/classes/Chart.js @@ -4,6 +4,13 @@ import 'updated-jqplot/dist/plugins/jqplot.pieRenderer'; import 'updated-jqplot/dist/plugins/jqplot.highlighter'; import 'updated-jqplot/dist/plugins/jqplot.enhancedPieLegendRenderer'; +import 'updated-jqplot/dist/plugins/jqplot.barRenderer.js'; +import 'updated-jqplot/dist/plugins/jqplot.canvasAxisLabelRenderer.js'; +import 'updated-jqplot/dist/plugins/jqplot.canvasTextRenderer.js'; +import 'updated-jqplot/dist/plugins/jqplot.categoryAxisRenderer.js'; +import 'updated-jqplot/dist/plugins/jqplot.dateAxisRenderer.js'; +import 'updated-jqplot/dist/plugins/jqplot.pointLabels.js'; + /** * Chart type enumerations */ diff --git a/js/src/functions/Table/TableChart.js b/js/src/functions/Table/TableChart.js new file mode 100644 index 0000000000..236a246851 --- /dev/null +++ b/js/src/functions/Table/TableChart.js @@ -0,0 +1,238 @@ +import JQPlotChartFactory, { DataTable, ColumnType } from '../../classes/Chart'; +import { escapeHtml } from '../../utils/Sanitise'; +import { PMA_Messages as PMA_messages } from '../../variables/export_variables'; +import { PMA_ajaxShowMessage } from '../../utils/show_ajax_messages'; + +export var TableChartEnum = { + chart_data: {}, + temp_chart_title: null, + + currentChart: null, + currentSettings: null, + + dateTimeCols: [], + numericCols: [] +}; + +function extractDate (dateString) { + var matches; + var match; + var dateTimeRegExp = /[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/; + var dateRegExp = /[0-9]{4}-[0-9]{2}-[0-9]{2}/; + + matches = dateTimeRegExp.exec(dateString); + if (matches !== null && matches.length > 0) { + match = matches[0]; + return new Date(match.substr(0, 4), parseInt(match.substr(5, 2), 10) - 1, match.substr(8, 2), match.substr(11, 2), match.substr(14, 2), match.substr(17, 2)); + } else { + matches = dateRegExp.exec(dateString); + if (matches !== null && matches.length > 0) { + match = matches[0]; + return new Date(match.substr(0, 4), parseInt(match.substr(5, 2), 10) - 1, match.substr(8, 2)); + } + } + return null; +} + +function PMA_queryChart (data, columnNames, settings) { + if ($('#querychart').length === 0) { + return; + } + + var plotSettings = { + title : { + text : settings.title, + escapeHtml: true + }, + grid : { + drawBorder : false, + shadow : false, + background : 'rgba(0,0,0,0)' + }, + legend : { + show : true, + placement : 'outsideGrid', + location : 'e', + rendererOptions: { + numberColumns: 2 + } + }, + axes : { + xaxis : { + label : escapeHtml(settings.xaxisLabel) + }, + yaxis : { + label : settings.yaxisLabel + } + }, + stackSeries : settings.stackSeries + }; + + // create the chart + var factory = new JQPlotChartFactory(); + var chart = factory.createChart(settings.type, 'querychart'); + + // create the data table and add columns + var dataTable = new DataTable(); + if (settings.type === 'timeline') { + dataTable.addColumn(ColumnType.DATE, columnNames[settings.mainAxis]); + } else if (settings.type === 'scatter') { + dataTable.addColumn(ColumnType.NUMBER, columnNames[settings.mainAxis]); + } else { + dataTable.addColumn(ColumnType.STRING, columnNames[settings.mainAxis]); + } + + var i; + if (settings.seriesColumn === null) { + $.each(settings.selectedSeries, function (index, element) { + dataTable.addColumn(ColumnType.NUMBER, columnNames[element]); + }); + + // set data to the data table + var columnsToExtract = [settings.mainAxis]; + $.each(settings.selectedSeries, function (index, element) { + columnsToExtract.push(element); + }); + var values = []; + var newRow; + var row; + var col; + for (i = 0; i < data.length; i++) { + row = data[i]; + newRow = []; + for (var j = 0; j < columnsToExtract.length; j++) { + col = columnNames[columnsToExtract[j]]; + if (j === 0) { + if (settings.type === 'timeline') { // first column is date type + newRow.push(extractDate(row[col])); + } else if (settings.type === 'scatter') { + newRow.push(parseFloat(row[col])); + } else { // first column is string type + newRow.push(row[col]); + } + } else { // subsequent columns are of type, number + newRow.push(parseFloat(row[col])); + } + } + values.push(newRow); + } + dataTable.setData(values); + } else { + var seriesNames = {}; + var seriesNumber = 1; + var seriesColumnName = columnNames[settings.seriesColumn]; + for (i = 0; i < data.length; i++) { + if (! seriesNames[data[i][seriesColumnName]]) { + seriesNames[data[i][seriesColumnName]] = seriesNumber; + seriesNumber++; + } + } + + $.each(seriesNames, function (seriesName, seriesNumber) { + dataTable.addColumn(ColumnType.NUMBER, seriesName); + }); + + var valueMap = {}; + var xValue; + var value; + var mainAxisName = columnNames[settings.mainAxis]; + var valueColumnName = columnNames[settings.valueColumn]; + for (i = 0; i < data.length; i++) { + xValue = data[i][mainAxisName]; + value = valueMap[xValue]; + if (! value) { + value = [xValue]; + valueMap[xValue] = value; + } + seriesNumber = seriesNames[data[i][seriesColumnName]]; + value[seriesNumber] = parseFloat(data[i][valueColumnName]); + } + + var values = []; + $.each(valueMap, function (index, value) { + values.push(value); + }); + dataTable.setData(values); + } + + // draw the chart and return the chart object + chart.draw(dataTable, plotSettings); + return chart; +} + +export function drawChart () { + TableChartEnum.currentSettings.width = $('#resizer').width() - 20; + TableChartEnum.currentSettings.height = $('#resizer').height() - 20; + + // TODO: a better way using .redraw() ? + if (TableChartEnum.currentChart !== null) { + TableChartEnum.currentChart.destroy(); + } + + var columnNames = []; + $('select[name="chartXAxis"] option').each(function () { + columnNames.push(escapeHtml($(this).text())); + }); + try { + TableChartEnum.currentChart = PMA_queryChart(TableChartEnum.chart_data, columnNames, TableChartEnum.currentSettings); + if (TableChartEnum.currentChart !== null) { + $('#saveChart').attr('href', TableChartEnum.currentChart.toImageString()); + } + } catch (err) { + PMA_ajaxShowMessage(err.message, false); + } +} + +export function getSelectedSeries () { + var val = $('select[name="chartSeries"]').val() || []; + var ret = []; + $.each(val, function (i, v) { + ret.push(parseInt(v, 10)); + }); + return ret; +} + +export function onXAxisChange () { + var $xAxisSelect = $('select[name="chartXAxis"]'); + TableChartEnum.currentSettings.mainAxis = parseInt($xAxisSelect.val(), 10); + if (TableChartEnum.dateTimeCols.indexOf(TableChartEnum.currentSettings.mainAxis) !== -1) { + $('span.span_timeline').show(); + } else { + $('span.span_timeline').hide(); + if (TableChartEnum.currentSettings.type === 'timeline') { + $('input#radio_line').prop('checked', true); + TableChartEnum.currentSettings.type = 'line'; + } + } + if (TableChartEnum.numericCols.indexOf(TableChartEnum.currentSettings.mainAxis) !== -1) { + $('span.span_scatter').show(); + } else { + $('span.span_scatter').hide(); + if (TableChartEnum.currentSettings.type === 'scatter') { + $('input#radio_line').prop('checked', true); + TableChartEnum.currentSettings.type = 'line'; + } + } + var xaxis_title = $xAxisSelect.children('option:selected').text(); + $('input[name="xaxis_label"]').val(xaxis_title); + TableChartEnum.currentSettings.xaxisLabel = xaxis_title; +} + +export function onDataSeriesChange () { + var $seriesSelect = $('select[name="chartSeries"]'); + TableChartEnum.currentSettings.selectedSeries = getSelectedSeries(); + var yaxis_title; + if (TableChartEnum.currentSettings.selectedSeries.length === 1) { + $('span.span_pie').show(); + yaxis_title = $seriesSelect.children('option:selected').text(); + } else { + $('span.span_pie').hide(); + if (TableChartEnum.currentSettings.type === 'pie') { + $('input#radio_line').prop('checked', true); + TableChartEnum.currentSettings.type = 'line'; + } + yaxis_title = PMA_messages.strYValues; + } + $('input[name="yaxis_label"]').val(yaxis_title); + TableChartEnum.currentSettings.yaxisLabel = yaxis_title; +} diff --git a/js/src/tbl_chart.js b/js/src/tbl_chart.js new file mode 100644 index 0000000000..81849e4dd7 --- /dev/null +++ b/js/src/tbl_chart.js @@ -0,0 +1,201 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +import { TableChartEnum, + drawChart, + onDataSeriesChange, + onXAxisChange, + getSelectedSeries +} from './functions/Table/TableChart'; +import { PMA_ajaxRemoveMessage, PMA_ajaxShowMessage } from './utils/show_ajax_messages'; +import { checkSqlQuery } from './functions/Sql/SqlQuery'; +import { PMA_prepareForAjaxRequest } from './functions/AjaxRequest'; +import { sqlQueryOptions } from './utils/sql'; + +/** + * Unbind all event handlers before tearing down a page + */ +export function teardown1 () { + $('input[name="chartType"]').off('click'); + $('input[name="barStacked"]').off('click'); + $('input[name="chkAlternative"]').off('click'); + $('input[name="chartTitle"]').off('focus').off('keyup').off('blur'); + $('select[name="chartXAxis"]').off('change'); + $('select[name="chartSeries"]').off('change'); + $('select[name="chartSeriesColumn"]').off('change'); + $('select[name="chartValueColumn"]').off('change'); + $('input[name="xaxis_label"]').off('keyup'); + $('input[name="yaxis_label"]').off('keyup'); + $('#resizer').off('resizestop'); + $('#tblchartform').off('submit'); +} + +export function onload1 () { + // handle manual resize + $('#resizer').on('resizestop', function (event, ui) { + // make room so that the handle will still appear + $('#querychart').height($('#resizer').height() * 0.96); + $('#querychart').width($('#resizer').width() * 0.96); + if (TableChartEnum.currentChart !== null) { + TableChartEnum.currentChart.redraw({ + resetAxes : true + }); + } + }); + + // handle chart type changes + $('input[name="chartType"]').on('click', function () { + var type = TableChartEnum.currentSettings.type = $(this).val(); + if (type === 'bar' || type === 'column' || type === 'area') { + $('span.barStacked').show(); + } else { + $('input[name="barStacked"]').prop('checked', false); + $.extend(true, TableChartEnum.currentSettings, { stackSeries : false }); + $('span.barStacked').hide(); + } + drawChart(); + }); + + // handle chosing alternative data format + $('input[name="chkAlternative"]').on('click', function () { + var $seriesColumn = $('select[name="chartSeriesColumn"]'); + var $valueColumn = $('select[name="chartValueColumn"]'); + var $chartSeries = $('select[name="chartSeries"]'); + if ($(this).is(':checked')) { + $seriesColumn.prop('disabled', false); + $valueColumn.prop('disabled', false); + $chartSeries.prop('disabled', true); + TableChartEnum.currentSettings.seriesColumn = parseInt($seriesColumn.val(), 10); + TableChartEnum.currentSettings.valueColumn = parseInt($valueColumn.val(), 10); + } else { + $seriesColumn.prop('disabled', true); + $valueColumn.prop('disabled', true); + $chartSeries.prop('disabled', false); + TableChartEnum.currentSettings.seriesColumn = null; + TableChartEnum.currentSettings.valueColumn = null; + } + drawChart(); + }); + + // handle stacking for bar, column and area charts + $('input[name="barStacked"]').on('click', function () { + if ($(this).is(':checked')) { + $.extend(true, TableChartEnum.currentSettings, { stackSeries : true }); + } else { + $.extend(true, TableChartEnum.currentSettings, { stackSeries : false }); + } + drawChart(); + }); + + // handle changes in chart title + $('input[name="chartTitle"]') + .focus(function () { + TableChartEnum.temp_chart_title = $(this).val(); + }) + .on('keyup', function () { + TableChartEnum.currentSettings.title = $('input[name="chartTitle"]').val(); + drawChart(); + }) + .blur(function () { + if ($(this).val() !== TableChartEnum.temp_chart_title) { + drawChart(); + } + }); + + // handle changing the x-axis + $('select[name="chartXAxis"]').on('change', function () { + onXAxisChange(); + drawChart(); + }); + + // handle changing the selected data series + $('select[name="chartSeries"]').on('change', function () { + onDataSeriesChange(); + drawChart(); + }); + + // handle changing the series column + $('select[name="chartSeriesColumn"]').on('change', function () { + TableChartEnum.currentSettings.seriesColumn = parseInt($(this).val(), 10); + drawChart(); + }); + + // handle changing the value column + $('select[name="chartValueColumn"]').on('change', function () { + TableChartEnum.currentSettings.valueColumn = parseInt($(this).val(), 10); + drawChart(); + }); + + // handle manual changes to the chart x-axis labels + $('input[name="xaxis_label"]').on('keyup', function () { + TableChartEnum.currentSettings.xaxisLabel = $(this).val(); + drawChart(); + }); + + // handle manual changes to the chart y-axis labels + $('input[name="yaxis_label"]').on('keyup', function () { + TableChartEnum.currentSettings.yaxisLabel = $(this).val(); + drawChart(); + }); + + // handler for ajax form submission + $('#tblchartform').submit(function (event) { + var $form = $(this); + if (sqlQueryOptions.codemirror_editor) { + $form[0].elements.sql_query.value = sqlQueryOptions.codemirror_editor.getValue(); + } + if (!checkSqlQuery($form[0])) { + return false; + } + + var $msgbox = PMA_ajaxShowMessage(); + PMA_prepareForAjaxRequest($form); + $.post($form.attr('action'), $form.serialize(), function (data) { + if (typeof data !== 'undefined' && + data.success === true && + typeof data.chartData !== 'undefined') { + TableChartEnum.chart_data = JSON.parse(data.chartData); + drawChart(); + PMA_ajaxRemoveMessage($msgbox); + } else { + PMA_ajaxShowMessage(data.error, false); + } + }, 'json'); // end $.post() + + return false; + }); + + // from jQuery UI + $('#resizer').resizable({ + minHeight: 240, + minWidth: 300 + }) + .width($('#div_view_options').width() - 50) + .trigger('resizestop'); + + TableChartEnum.currentSettings = { + type : 'line', + width : $('#resizer').width() - 20, + height : $('#resizer').height() - 20, + xaxisLabel : $('input[name="xaxis_label"]').val(), + yaxisLabel : $('input[name="yaxis_label"]').val(), + title : $('input[name="chartTitle"]').val(), + stackSeries : false, + mainAxis : parseInt($('select[name="chartXAxis"]').val(), 10), + selectedSeries : getSelectedSeries(), + seriesColumn : null + }; + + var vals = $('input[name="dateTimeCols"]').val().split(' '); + $.each(vals, function (i, v) { + TableChartEnum.dateTimeCols.push(parseInt(v, 10)); + }); + + vals = $('input[name="numericCols"]').val().split(' '); + $.each(vals, function (i, v) { + TableChartEnum.numericCols.push(parseInt(v, 10)); + }); + + onXAxisChange(); + onDataSeriesChange(); + + $('#tblchartform').submit(); +}