diff --git a/qstudio/src/main/java/com/timestored/qstudio/ChartResultPanel.java b/qstudio/src/main/java/com/timestored/qstudio/ChartResultPanel.java index 7d8af17..1ae2923 100644 --- a/qstudio/src/main/java/com/timestored/qstudio/ChartResultPanel.java +++ b/qstudio/src/main/java/com/timestored/qstudio/ChartResultPanel.java @@ -75,17 +75,6 @@ public ChartResultPanel(final QueryManager adminModel, Growler growler) { app = new ChartWidget(); - // adapts queryManager events to trigger tabChange events for kdbChartPanel - adminModel.addQueryListener(new QueryAdapter() { - - @Override public void sendingQuery(ServerConfig sc, String query) {} - - @Override public void queryResultReturned(ServerConfig sc, QueryResult qr) { - showQueryResult(qr); - } - - }); - if(adminModel.hasAnyServers()) { add(UpdateHelper.getNewsPanel(null), BorderLayout.CENTER); } diff --git a/qstudio/src/main/java/com/timestored/qstudio/QStudioFrame.java b/qstudio/src/main/java/com/timestored/qstudio/QStudioFrame.java index 86f2c22..c2ff2bb 100644 --- a/qstudio/src/main/java/com/timestored/qstudio/QStudioFrame.java +++ b/qstudio/src/main/java/com/timestored/qstudio/QStudioFrame.java @@ -176,7 +176,7 @@ public class QStudioFrame extends JFrame { private static final int SERVER_NAME_WIDTH = 190; private static final long serialVersionUID = 1L; private static final String UNIQUE_ID = "UNIQ_ID"; - public static final String VERSION = "5.00"; + public static final String VERSION = "5.01"; private final QStudioModel qStudioModel; private final ConnectionManager conMan; diff --git a/qstudio/src/main/java/com/timestored/sqldash/chart/AutoRedrawViewStrategy.java b/qstudio/src/main/java/com/timestored/sqldash/chart/AutoRedrawViewStrategy.java new file mode 100644 index 0000000..a307f74 --- /dev/null +++ b/qstudio/src/main/java/com/timestored/sqldash/chart/AutoRedrawViewStrategy.java @@ -0,0 +1,281 @@ +package com.timestored.sqldash.chart; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; + +import com.timestored.connections.JdbcTypes; +import com.timestored.theme.Icon; +import com.timestored.theme.Theme; + + +/** + * AUTO chart selection strategy that automatically picks the best chart type + * based on the result set column structure. + */ +public enum AutoRedrawViewStrategy implements ViewStrategy { + + INSTANCE; + + public static boolean displayChartActivated = false; + private static final String DESC = "Automatically selects the best chart type based on your data.
" + + "Analyzes column types (date/time, string, numeric) to pick an appropriate visualization."; + + private static final Set CANDLESTICK_COLS = new HashSet(); + static { + CANDLESTICK_COLS.add("high"); + CANDLESTICK_COLS.add("low"); + CANDLESTICK_COLS.add("h"); + CANDLESTICK_COLS.add("l"); + } + + @Override public UpdateableView getView(final ChartTheme theme) { + return new AutoUpdateableView(theme, null); + } + + /** + * Inner class that delegates to the appropriate chart strategy based on data analysis. + */ + private static class AutoUpdateableView implements UpdateableView { + + private final ChartTheme theme; + private final ChartAppearanceConfig appearanceConfig; + private UpdateableView delegateView; + private JPanel containerPanel; + + AutoUpdateableView(ChartTheme theme, ChartAppearanceConfig appearanceConfig) { + this.theme = theme; + this.appearanceConfig = appearanceConfig; + this.containerPanel = new JPanel(new BorderLayout()); + containerPanel.add(Theme.getHtmlText("Waiting for data..."), BorderLayout.CENTER); + } + + @Override public void update(ResultSet rs, ChartResultSet chartResultSet) throws ChartFormatException { + + ViewStrategy selectedStrategy = null; + if(displayChartActivated) { + selectedStrategy = selectStrategy(rs, chartResultSet); + } + // Pass appearance config to the delegate if the strategy supports it + if(selectedStrategy == null) { + delegateView =null; + } else if (selectedStrategy.supportsAppearanceConfig() && appearanceConfig != null) { + delegateView = selectedStrategy.getView(theme, appearanceConfig); + } else { + delegateView = selectedStrategy.getView(theme); + } + + containerPanel.removeAll(); + if(!displayChartActivated) { + // centre the button and highlight for emphasis + JButton activateChartBtn = new JButton("Activate Auto Charting", Theme.CIcon.CHART_CURVE_ADD.get16()); + activateChartBtn.addActionListener(e -> { + displayChartActivated = true; + try { + update(rs, chartResultSet); + } catch (ChartFormatException e1) { } + }); + JPanel btnPanel = new JPanel(); + btnPanel.add(activateChartBtn); + btnPanel.setBorder(BorderFactory.createEmptyBorder(20,20,20,20)); + containerPanel.add(btnPanel, BorderLayout.NORTH); + containerPanel.add(Theme.getHtmlText("Auto Charting is deactivated. Please enable to see chart."), BorderLayout.CENTER); + } else if(delegateView == null) { + containerPanel.add(Theme.getHtmlText("Result is large. Choose specific chart to try charting."), BorderLayout.CENTER); + } else { + containerPanel.add(delegateView.getComponent(), BorderLayout.CENTER); + delegateView.update(rs, chartResultSet); + } + + containerPanel.revalidate(); + containerPanel.repaint(); + } + + @Override public Component getComponent() { + return containerPanel; + } + } + + /** + * Selects the best chart strategy based on the result set structure. + * Rules are applied in priority order - first match wins. + */ + static ViewStrategy selectStrategy(ResultSet rs, ChartResultSet chartResultSet) { + if (chartResultSet == null) { + return DataTableViewStrategy.getInstance(); + } + if(chartResultSet.getRowCount() > 50_000) { + // For very large result sets, do not attempt auto charting + return null; + } + + + try { + // Analyze column structure + rs.beforeFirst(); + ResultSetMetaData md = rs.getMetaData(); + int colCount = md.getColumnCount(); + + int firstDateIdx = -1; + int firstStringIdx = -1; + int numericCount = 0; + int stringCount = 0; + int consecutiveNumericFromFirstString = 0; + boolean hasHighLow = false; + + for (int c = 1; c <= colCount; c++) { + int ctype = md.getColumnType(c); + String ctypeName = md.getColumnTypeName(c); + String colName = md.getColumnName(c).toLowerCase(); + + boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName); + boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName); + boolean isStringy = !isNumeric && !isTemporal; + + if (isTemporal && firstDateIdx == -1) { + firstDateIdx = c; + } + if (isStringy && firstStringIdx == -1) { + firstStringIdx = c; + } + if (isNumeric) { + numericCount++; + } + if (isStringy) { + stringCount++; + } + + // Check for candlestick columns + if (CANDLESTICK_COLS.contains(colName)) { + hasHighLow = true; + } + } + + // Count consecutive numerics after first string column ends + if (firstStringIdx > 0) { + boolean foundNonString = false; + for (int c = firstStringIdx; c <= colCount; c++) { + int ctype = md.getColumnType(c); + String ctypeName = md.getColumnTypeName(c); + boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName); + boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName); + boolean isStringy = !isNumeric && !isTemporal; + + if (!isStringy) { + foundNonString = true; + } + if (foundNonString && isNumeric) { + consecutiveNumericFromFirstString++; + } else if (foundNonString && !isNumeric) { + break; + } + } + } + + // Rule 1: Candlestick - check for high/low columns (highest priority) + if (hasHighLow && chartResultSet.getTimeCol() != null) { + return CandleStickViewStrategy.INSTANCE; + } + + // Rule 2: Time-Based Charts - date/time column appears before any string column + if (firstDateIdx > 0 && (firstStringIdx == -1 || firstDateIdx < firstStringIdx)) { + return TimeseriesViewStrategy.INSTANCE; + } + + // Rule 3: Bubble Chart - string columns followed by exactly 3 numeric columns + if (stringCount > 0 && consecutiveNumericFromFirstString == 3) { + return BubbleChartViewStrategy.INSTANCE; + } + + // Rule 4: Scatter Plot - no dates, first non-string is numeric, at least 2 numeric columns + if (firstDateIdx == -1 && numericCount >= 2) { + // Check if first non-string column is numeric + for (int c = 1; c <= colCount; c++) { + int ctype = md.getColumnType(c); + String ctypeName = md.getColumnTypeName(c); + boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName); + boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName); + boolean isStringy = !isNumeric && !isTemporal; + + if (!isStringy) { + if (isNumeric && stringCount == 0) { + return ScatterPlotViewStrategy.INSTANCE; + } + break; + } + } + } + + // Rule 5a: Pie Chart - string columns first, exactly 1 numeric column + if (stringCount > 0 && numericCount == 1 && isStringFirst(md, colCount)) { + return PieChartViewStrategy.INSTANCE; + } + + // Rule 5b: Stacked Bar Chart - string columns first, 2 or more numeric columns + if (stringCount > 0 && numericCount >= 2 && isStringFirst(md, colCount)) { + return StackedBarChartViewStrategy.INSTANCE; + } + + // Rule 6: Histogram - only numeric columns, no strings, no dates + if (numericCount > 0 && stringCount == 0 && firstDateIdx == -1) { + return HistogramViewStrategy.INSTANCE; + } + + } catch (SQLException e) { + // Fall through to default + } + + // Rule 7: Data Table (Fallback) + return DataTableViewStrategy.getInstance(); + } + + /** Checks if the first column is a string type column */ + private static boolean isStringFirst(ResultSetMetaData md, int colCount) throws SQLException { + if (colCount == 0) return false; + int ctype = md.getColumnType(1); + String ctypeName = md.getColumnTypeName(1); + return !SqlHelper.isNumeric(ctype, ctypeName) && !SqlHelper.isTemporal(ctype, ctypeName); + } + + @Override public String getDescription() { return "Auto"; } + + @Override public String getFormatExplainationHtml() { return DESC; } + @Override public String getFormatExplaination() { return DESC; } + + @Override public Icon getIcon() { return Theme.CIcon.CHART_CURVE_ADD; } + @Override public String getQueryEg(JdbcTypes jdbcType) { return null; } + + @Override public List getExamples() { + return Collections.emptyList(); + } + + @Override public String toString() { return getDescription(); } + + @Override public boolean isQuickToRender(ResultSet rs, int rowCount, int numColumnCount) { + return true; + } + + @Override public String getPulseName() { return "auto"; } + + /** + * AUTO mode supports appearance configuration because it may delegate to + * chart types (like TimeSeries, StackedBar) that support configuration. + * The config editor will be shown to allow users to customize the chart. + */ + @Override public boolean supportsAppearanceConfig() { return true; } + + @Override public UpdateableView getView(ChartTheme theme, ChartAppearanceConfig appearanceConfig) { + return new AutoUpdateableView(theme, appearanceConfig); + } +} diff --git a/qstudio/src/main/java/com/timestored/sqldash/chart/ColumnFormatManager.java b/qstudio/src/main/java/com/timestored/sqldash/chart/ColumnFormatManager.java index 8b8e49a..4a7268c 100644 --- a/qstudio/src/main/java/com/timestored/sqldash/chart/ColumnFormatManager.java +++ b/qstudio/src/main/java/com/timestored/sqldash/chart/ColumnFormatManager.java @@ -102,7 +102,7 @@ public ColType detectType(JXTable t, int modelCol) { } public JMenu buildFlatMenu(String colName, ColType type) { - JMenu m = new JMenu("Format…"); + JMenu m = new JMenu("Format..."); switch(type) { case NUMBER: diff --git a/qstudio/src/main/java/com/timestored/sqldash/chart/NoRedrawViewStrategy.java b/qstudio/src/main/java/com/timestored/sqldash/chart/NoRedrawViewStrategy.java index 0e63296..0c3ade9 100644 --- a/qstudio/src/main/java/com/timestored/sqldash/chart/NoRedrawViewStrategy.java +++ b/qstudio/src/main/java/com/timestored/sqldash/chart/NoRedrawViewStrategy.java @@ -2,13 +2,10 @@ import java.awt.BorderLayout; import java.awt.Component; +import java.awt.Dimension; import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.swing.JPanel; @@ -17,221 +14,37 @@ import com.timestored.theme.Theme; -/** - * AUTO chart selection strategy that automatically picks the best chart type - * based on the result set column structure. - */ public enum NoRedrawViewStrategy implements ViewStrategy { INSTANCE; - private static final String DESC = "Automatically selects the best chart type based on your data.
" + - "Analyzes column types (date/time, string, numeric) to pick an appropriate visualization."; - - private static final Set CANDLESTICK_COLS = new HashSet(); - static { - CANDLESTICK_COLS.add("high"); - CANDLESTICK_COLS.add("low"); - CANDLESTICK_COLS.add("h"); - CANDLESTICK_COLS.add("l"); - } - - @Override public UpdateableView getView(final ChartTheme theme) { - return new AutoUpdateableView(theme, null); - } - - /** - * Inner class that delegates to the appropriate chart strategy based on data analysis. - */ - private static class AutoUpdateableView implements UpdateableView { + private static final String DESC = "Do not draw anything.
This selection does nothing and is for use" + + " by those who do not want a chart, allowing quicker result drawing times."; - private final ChartTheme theme; - private final ChartAppearanceConfig appearanceConfig; - private UpdateableView delegateView; - private JPanel containerPanel; - - AutoUpdateableView(ChartTheme theme, ChartAppearanceConfig appearanceConfig) { - this.theme = theme; - this.appearanceConfig = appearanceConfig; - this.containerPanel = new JPanel(new BorderLayout()); - containerPanel.add(Theme.getHtmlText("Waiting for data..."), BorderLayout.CENTER); - } - - @Override public void update(ResultSet rs, ChartResultSet chartResultSet) throws ChartFormatException { - ViewStrategy selectedStrategy = selectStrategy(rs, chartResultSet); - // Pass appearance config to the delegate if the strategy supports it - if(selectedStrategy == null) { - delegateView =null; - } else if (selectedStrategy.supportsAppearanceConfig() && appearanceConfig != null) { - delegateView = selectedStrategy.getView(theme, appearanceConfig); - } else { - delegateView = selectedStrategy.getView(theme); - } - - containerPanel.removeAll(); - if(delegateView == null) { - containerPanel.add(Theme.getHtmlText("Result is large. Choose specific chart to try charting."), BorderLayout.CENTER); - } else { - containerPanel.add(delegateView.getComponent(), BorderLayout.CENTER); - delegateView.update(rs, chartResultSet); - } - - containerPanel.revalidate(); - containerPanel.repaint(); - } - - @Override public Component getComponent() { - return containerPanel; - } - } - - /** - * Selects the best chart strategy based on the result set structure. - * Rules are applied in priority order - first match wins. - */ - static ViewStrategy selectStrategy(ResultSet rs, ChartResultSet chartResultSet) { - if (chartResultSet == null) { - return DataTableViewStrategy.getInstance(); - } - if(chartResultSet.getRowCount() > 100_000) { - // For very large result sets, do not attempt auto charting - return null; - } - - - try { - // Analyze column structure - rs.beforeFirst(); - ResultSetMetaData md = rs.getMetaData(); - int colCount = md.getColumnCount(); - - int firstDateIdx = -1; - int firstStringIdx = -1; - int numericCount = 0; - int stringCount = 0; - int consecutiveNumericFromFirstString = 0; - boolean hasHighLow = false; - - for (int c = 1; c <= colCount; c++) { - int ctype = md.getColumnType(c); - String ctypeName = md.getColumnTypeName(c); - String colName = md.getColumnName(c).toLowerCase(); - - boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName); - boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName); - boolean isStringy = !isNumeric && !isTemporal; - - if (isTemporal && firstDateIdx == -1) { - firstDateIdx = c; - } - if (isStringy && firstStringIdx == -1) { - firstStringIdx = c; - } - if (isNumeric) { - numericCount++; - } - if (isStringy) { - stringCount++; - } - - // Check for candlestick columns - if (CANDLESTICK_COLS.contains(colName)) { - hasHighLow = true; - } - } + @Override public UpdateableView getView(ChartTheme theme) { + return new UpdateableView() { - // Count consecutive numerics after first string column ends - if (firstStringIdx > 0) { - boolean foundNonString = false; - for (int c = firstStringIdx; c <= colCount; c++) { - int ctype = md.getColumnType(c); - String ctypeName = md.getColumnTypeName(c); - boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName); - boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName); - boolean isStringy = !isNumeric && !isTemporal; - - if (!isStringy) { - foundNonString = true; - } - if (foundNonString && isNumeric) { - consecutiveNumericFromFirstString++; - } else if (foundNonString && !isNumeric) { - break; - } - } + @Override public void update(ResultSet rs, ChartResultSet chartResultSet) + throws ChartFormatException { + // intentionally empty. } - // Rule 1: Candlestick - check for high/low columns (highest priority) - if (hasHighLow && chartResultSet.getTimeCol() != null) { - return CandleStickViewStrategy.INSTANCE; + @Override public Component getComponent() { + JPanel p = new JPanel(new BorderLayout()); + p.add(Theme.getHeader("No Chart Drawing"), BorderLayout.NORTH); + p.add(Theme.getHtmlText(DESC), BorderLayout.CENTER); + p.setPreferredSize(new Dimension(150, 40)); + return p; } - - // Rule 2: Time-Based Charts - date/time column appears before any string column - if (firstDateIdx > 0 && (firstStringIdx == -1 || firstDateIdx < firstStringIdx)) { - return TimeseriesViewStrategy.INSTANCE; - } - - // Rule 3: Bubble Chart - string columns followed by exactly 3 numeric columns - if (stringCount > 0 && consecutiveNumericFromFirstString == 3) { - return BubbleChartViewStrategy.INSTANCE; - } - - // Rule 4: Scatter Plot - no dates, first non-string is numeric, at least 2 numeric columns - if (firstDateIdx == -1 && numericCount >= 2) { - // Check if first non-string column is numeric - for (int c = 1; c <= colCount; c++) { - int ctype = md.getColumnType(c); - String ctypeName = md.getColumnTypeName(c); - boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName); - boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName); - boolean isStringy = !isNumeric && !isTemporal; - - if (!isStringy) { - if (isNumeric && stringCount == 0) { - return ScatterPlotViewStrategy.INSTANCE; - } - break; - } - } - } - - // Rule 5a: Pie Chart - string columns first, exactly 1 numeric column - if (stringCount > 0 && numericCount == 1 && isStringFirst(md, colCount)) { - return PieChartViewStrategy.INSTANCE; - } - - // Rule 5b: Stacked Bar Chart - string columns first, 2 or more numeric columns - if (stringCount > 0 && numericCount >= 2 && isStringFirst(md, colCount)) { - return StackedBarChartViewStrategy.INSTANCE; - } - - // Rule 6: Histogram - only numeric columns, no strings, no dates - if (numericCount > 0 && stringCount == 0 && firstDateIdx == -1) { - return HistogramViewStrategy.INSTANCE; - } - - } catch (SQLException e) { - // Fall through to default - } - - // Rule 7: Data Table (Fallback) - return DataTableViewStrategy.getInstance(); - } - - /** Checks if the first column is a string type column */ - private static boolean isStringFirst(ResultSetMetaData md, int colCount) throws SQLException { - if (colCount == 0) return false; - int ctype = md.getColumnType(1); - String ctypeName = md.getColumnTypeName(1); - return !SqlHelper.isNumeric(ctype, ctypeName) && !SqlHelper.isTemporal(ctype, ctypeName); + }; } - @Override public String getDescription() { return "Auto"; } + @Override public String getDescription() { return "No Chart"; } @Override public String getFormatExplainationHtml() { return DESC; } @Override public String getFormatExplaination() { return DESC; } - @Override public Icon getIcon() { return Theme.CIcon.CHART_CURVE_ADD; } + @Override public Icon getIcon() { return null; } @Override public String getQueryEg(JdbcTypes jdbcType) { return null; } @Override public List getExamples() { @@ -244,16 +57,5 @@ private static boolean isStringFirst(ResultSetMetaData md, int colCount) throws return true; } - @Override public String getPulseName() { return "auto"; } - - /** - * AUTO mode supports appearance configuration because it may delegate to - * chart types (like TimeSeries, StackedBar) that support configuration. - * The config editor will be shown to allow users to customize the chart. - */ - @Override public boolean supportsAppearanceConfig() { return true; } - - @Override public UpdateableView getView(ChartTheme theme, ChartAppearanceConfig appearanceConfig) { - return new AutoUpdateableView(theme, appearanceConfig); - } + @Override public String getPulseName() { return null; } } diff --git a/qstudio/src/main/java/com/timestored/sqldash/chart/ViewStrategyFactory.java b/qstudio/src/main/java/com/timestored/sqldash/chart/ViewStrategyFactory.java index 93d8062..38c325b 100644 --- a/qstudio/src/main/java/com/timestored/sqldash/chart/ViewStrategyFactory.java +++ b/qstudio/src/main/java/com/timestored/sqldash/chart/ViewStrategyFactory.java @@ -11,7 +11,7 @@ public class ViewStrategyFactory { private static final List STRATEGIES = Collections.unmodifiableList(Arrays.asList(new ViewStrategy[] { - NoRedrawViewStrategy.INSTANCE, + AutoRedrawViewStrategy.INSTANCE, TimeseriesViewStrategy.INSTANCE, StepChartViewStrategy.INSTANCE, AreaChartViewStrategy.INSTANCE, @@ -25,6 +25,7 @@ public class ViewStrategyFactory { PieChartViewStrategy.INSTANCE, ScatterPlotViewStrategy.INSTANCE, DataTableViewStrategy.getInstance(), + NoRedrawViewStrategy.INSTANCE, }));