Skip to content

Commit 85cefc8

Browse files
committed
Charting - Add user activation to prevent charting cost unless asked for
1 parent 3eee73b commit 85cefc8

File tree

6 files changed

+303
-230
lines changed

6 files changed

+303
-230
lines changed

qstudio/src/main/java/com/timestored/qstudio/ChartResultPanel.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,6 @@ public ChartResultPanel(final QueryManager adminModel, Growler growler) {
7575

7676
app = new ChartWidget();
7777

78-
// adapts queryManager events to trigger tabChange events for kdbChartPanel
79-
adminModel.addQueryListener(new QueryAdapter() {
80-
81-
@Override public void sendingQuery(ServerConfig sc, String query) {}
82-
83-
@Override public void queryResultReturned(ServerConfig sc, QueryResult qr) {
84-
showQueryResult(qr);
85-
}
86-
87-
});
88-
8978
if(adminModel.hasAnyServers()) {
9079
add(UpdateHelper.getNewsPanel(null), BorderLayout.CENTER);
9180
}

qstudio/src/main/java/com/timestored/qstudio/QStudioFrame.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public class QStudioFrame extends JFrame {
176176
private static final int SERVER_NAME_WIDTH = 190;
177177
private static final long serialVersionUID = 1L;
178178
private static final String UNIQUE_ID = "UNIQ_ID";
179-
public static final String VERSION = "5.00";
179+
public static final String VERSION = "5.01";
180180

181181
private final QStudioModel qStudioModel;
182182
private final ConnectionManager conMan;
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
package com.timestored.sqldash.chart;
2+
3+
import java.awt.BorderLayout;
4+
import java.awt.Color;
5+
import java.awt.Component;
6+
import java.sql.ResultSet;
7+
import java.sql.ResultSetMetaData;
8+
import java.sql.SQLException;
9+
import java.util.Collections;
10+
import java.util.HashSet;
11+
import java.util.List;
12+
import java.util.Set;
13+
14+
import javax.swing.BorderFactory;
15+
import javax.swing.JButton;
16+
import javax.swing.JPanel;
17+
18+
import com.timestored.connections.JdbcTypes;
19+
import com.timestored.theme.Icon;
20+
import com.timestored.theme.Theme;
21+
22+
23+
/**
24+
* AUTO chart selection strategy that automatically picks the best chart type
25+
* based on the result set column structure.
26+
*/
27+
public enum AutoRedrawViewStrategy implements ViewStrategy {
28+
29+
INSTANCE;
30+
31+
public static boolean displayChartActivated = false;
32+
private static final String DESC = "Automatically selects the best chart type based on your data.<br/>" +
33+
"Analyzes column types (date/time, string, numeric) to pick an appropriate visualization.";
34+
35+
private static final Set<String> CANDLESTICK_COLS = new HashSet<String>();
36+
static {
37+
CANDLESTICK_COLS.add("high");
38+
CANDLESTICK_COLS.add("low");
39+
CANDLESTICK_COLS.add("h");
40+
CANDLESTICK_COLS.add("l");
41+
}
42+
43+
@Override public UpdateableView getView(final ChartTheme theme) {
44+
return new AutoUpdateableView(theme, null);
45+
}
46+
47+
/**
48+
* Inner class that delegates to the appropriate chart strategy based on data analysis.
49+
*/
50+
private static class AutoUpdateableView implements UpdateableView {
51+
52+
private final ChartTheme theme;
53+
private final ChartAppearanceConfig appearanceConfig;
54+
private UpdateableView delegateView;
55+
private JPanel containerPanel;
56+
57+
AutoUpdateableView(ChartTheme theme, ChartAppearanceConfig appearanceConfig) {
58+
this.theme = theme;
59+
this.appearanceConfig = appearanceConfig;
60+
this.containerPanel = new JPanel(new BorderLayout());
61+
containerPanel.add(Theme.getHtmlText("Waiting for data..."), BorderLayout.CENTER);
62+
}
63+
64+
@Override public void update(ResultSet rs, ChartResultSet chartResultSet) throws ChartFormatException {
65+
66+
ViewStrategy selectedStrategy = null;
67+
if(displayChartActivated) {
68+
selectedStrategy = selectStrategy(rs, chartResultSet);
69+
}
70+
// Pass appearance config to the delegate if the strategy supports it
71+
if(selectedStrategy == null) {
72+
delegateView =null;
73+
} else if (selectedStrategy.supportsAppearanceConfig() && appearanceConfig != null) {
74+
delegateView = selectedStrategy.getView(theme, appearanceConfig);
75+
} else {
76+
delegateView = selectedStrategy.getView(theme);
77+
}
78+
79+
containerPanel.removeAll();
80+
if(!displayChartActivated) {
81+
// centre the button and highlight for emphasis
82+
JButton activateChartBtn = new JButton("Activate Auto Charting", Theme.CIcon.CHART_CURVE_ADD.get16());
83+
activateChartBtn.addActionListener(e -> {
84+
displayChartActivated = true;
85+
try {
86+
update(rs, chartResultSet);
87+
} catch (ChartFormatException e1) { }
88+
});
89+
JPanel btnPanel = new JPanel();
90+
btnPanel.add(activateChartBtn);
91+
btnPanel.setBorder(BorderFactory.createEmptyBorder(20,20,20,20));
92+
containerPanel.add(btnPanel, BorderLayout.NORTH);
93+
containerPanel.add(Theme.getHtmlText("Auto Charting is deactivated. Please enable to see chart."), BorderLayout.CENTER);
94+
} else if(delegateView == null) {
95+
containerPanel.add(Theme.getHtmlText("Result is large. Choose specific chart to try charting."), BorderLayout.CENTER);
96+
} else {
97+
containerPanel.add(delegateView.getComponent(), BorderLayout.CENTER);
98+
delegateView.update(rs, chartResultSet);
99+
}
100+
101+
containerPanel.revalidate();
102+
containerPanel.repaint();
103+
}
104+
105+
@Override public Component getComponent() {
106+
return containerPanel;
107+
}
108+
}
109+
110+
/**
111+
* Selects the best chart strategy based on the result set structure.
112+
* Rules are applied in priority order - first match wins.
113+
*/
114+
static ViewStrategy selectStrategy(ResultSet rs, ChartResultSet chartResultSet) {
115+
if (chartResultSet == null) {
116+
return DataTableViewStrategy.getInstance();
117+
}
118+
if(chartResultSet.getRowCount() > 50_000) {
119+
// For very large result sets, do not attempt auto charting
120+
return null;
121+
}
122+
123+
124+
try {
125+
// Analyze column structure
126+
rs.beforeFirst();
127+
ResultSetMetaData md = rs.getMetaData();
128+
int colCount = md.getColumnCount();
129+
130+
int firstDateIdx = -1;
131+
int firstStringIdx = -1;
132+
int numericCount = 0;
133+
int stringCount = 0;
134+
int consecutiveNumericFromFirstString = 0;
135+
boolean hasHighLow = false;
136+
137+
for (int c = 1; c <= colCount; c++) {
138+
int ctype = md.getColumnType(c);
139+
String ctypeName = md.getColumnTypeName(c);
140+
String colName = md.getColumnName(c).toLowerCase();
141+
142+
boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName);
143+
boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName);
144+
boolean isStringy = !isNumeric && !isTemporal;
145+
146+
if (isTemporal && firstDateIdx == -1) {
147+
firstDateIdx = c;
148+
}
149+
if (isStringy && firstStringIdx == -1) {
150+
firstStringIdx = c;
151+
}
152+
if (isNumeric) {
153+
numericCount++;
154+
}
155+
if (isStringy) {
156+
stringCount++;
157+
}
158+
159+
// Check for candlestick columns
160+
if (CANDLESTICK_COLS.contains(colName)) {
161+
hasHighLow = true;
162+
}
163+
}
164+
165+
// Count consecutive numerics after first string column ends
166+
if (firstStringIdx > 0) {
167+
boolean foundNonString = false;
168+
for (int c = firstStringIdx; c <= colCount; c++) {
169+
int ctype = md.getColumnType(c);
170+
String ctypeName = md.getColumnTypeName(c);
171+
boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName);
172+
boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName);
173+
boolean isStringy = !isNumeric && !isTemporal;
174+
175+
if (!isStringy) {
176+
foundNonString = true;
177+
}
178+
if (foundNonString && isNumeric) {
179+
consecutiveNumericFromFirstString++;
180+
} else if (foundNonString && !isNumeric) {
181+
break;
182+
}
183+
}
184+
}
185+
186+
// Rule 1: Candlestick - check for high/low columns (highest priority)
187+
if (hasHighLow && chartResultSet.getTimeCol() != null) {
188+
return CandleStickViewStrategy.INSTANCE;
189+
}
190+
191+
// Rule 2: Time-Based Charts - date/time column appears before any string column
192+
if (firstDateIdx > 0 && (firstStringIdx == -1 || firstDateIdx < firstStringIdx)) {
193+
return TimeseriesViewStrategy.INSTANCE;
194+
}
195+
196+
// Rule 3: Bubble Chart - string columns followed by exactly 3 numeric columns
197+
if (stringCount > 0 && consecutiveNumericFromFirstString == 3) {
198+
return BubbleChartViewStrategy.INSTANCE;
199+
}
200+
201+
// Rule 4: Scatter Plot - no dates, first non-string is numeric, at least 2 numeric columns
202+
if (firstDateIdx == -1 && numericCount >= 2) {
203+
// Check if first non-string column is numeric
204+
for (int c = 1; c <= colCount; c++) {
205+
int ctype = md.getColumnType(c);
206+
String ctypeName = md.getColumnTypeName(c);
207+
boolean isNumeric = SqlHelper.isNumeric(ctype, ctypeName);
208+
boolean isTemporal = SqlHelper.isTemporal(ctype, ctypeName);
209+
boolean isStringy = !isNumeric && !isTemporal;
210+
211+
if (!isStringy) {
212+
if (isNumeric && stringCount == 0) {
213+
return ScatterPlotViewStrategy.INSTANCE;
214+
}
215+
break;
216+
}
217+
}
218+
}
219+
220+
// Rule 5a: Pie Chart - string columns first, exactly 1 numeric column
221+
if (stringCount > 0 && numericCount == 1 && isStringFirst(md, colCount)) {
222+
return PieChartViewStrategy.INSTANCE;
223+
}
224+
225+
// Rule 5b: Stacked Bar Chart - string columns first, 2 or more numeric columns
226+
if (stringCount > 0 && numericCount >= 2 && isStringFirst(md, colCount)) {
227+
return StackedBarChartViewStrategy.INSTANCE;
228+
}
229+
230+
// Rule 6: Histogram - only numeric columns, no strings, no dates
231+
if (numericCount > 0 && stringCount == 0 && firstDateIdx == -1) {
232+
return HistogramViewStrategy.INSTANCE;
233+
}
234+
235+
} catch (SQLException e) {
236+
// Fall through to default
237+
}
238+
239+
// Rule 7: Data Table (Fallback)
240+
return DataTableViewStrategy.getInstance();
241+
}
242+
243+
/** Checks if the first column is a string type column */
244+
private static boolean isStringFirst(ResultSetMetaData md, int colCount) throws SQLException {
245+
if (colCount == 0) return false;
246+
int ctype = md.getColumnType(1);
247+
String ctypeName = md.getColumnTypeName(1);
248+
return !SqlHelper.isNumeric(ctype, ctypeName) && !SqlHelper.isTemporal(ctype, ctypeName);
249+
}
250+
251+
@Override public String getDescription() { return "Auto"; }
252+
253+
@Override public String getFormatExplainationHtml() { return DESC; }
254+
@Override public String getFormatExplaination() { return DESC; }
255+
256+
@Override public Icon getIcon() { return Theme.CIcon.CHART_CURVE_ADD; }
257+
@Override public String getQueryEg(JdbcTypes jdbcType) { return null; }
258+
259+
@Override public List<ExampleView> getExamples() {
260+
return Collections.emptyList();
261+
}
262+
263+
@Override public String toString() { return getDescription(); }
264+
265+
@Override public boolean isQuickToRender(ResultSet rs, int rowCount, int numColumnCount) {
266+
return true;
267+
}
268+
269+
@Override public String getPulseName() { return "auto"; }
270+
271+
/**
272+
* AUTO mode supports appearance configuration because it may delegate to
273+
* chart types (like TimeSeries, StackedBar) that support configuration.
274+
* The config editor will be shown to allow users to customize the chart.
275+
*/
276+
@Override public boolean supportsAppearanceConfig() { return true; }
277+
278+
@Override public UpdateableView getView(ChartTheme theme, ChartAppearanceConfig appearanceConfig) {
279+
return new AutoUpdateableView(theme, appearanceConfig);
280+
}
281+
}

qstudio/src/main/java/com/timestored/sqldash/chart/ColumnFormatManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public ColType detectType(JXTable t, int modelCol) {
102102
}
103103

104104
public JMenu buildFlatMenu(String colName, ColType type) {
105-
JMenu m = new JMenu("Format");
105+
JMenu m = new JMenu("Format...");
106106

107107
switch(type) {
108108
case NUMBER:

0 commit comments

Comments
 (0)