Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions qstudio/src/main/java/com/timestored/qstudio/ChartResultPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.<br/>" +
"Analyzes column types (date/time, string, numeric) to pick an appropriate visualization.";

private static final Set<String> CANDLESTICK_COLS = new HashSet<String>();
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<ExampleView> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading