Skip to content

Commit 6f5d7fe

Browse files
committed
feat(data-browser): add graph visualization
1 parent 1c94668 commit 6f5d7fe

File tree

6 files changed

+186
-10
lines changed

6 files changed

+186
-10
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React from 'react';
2+
import Chart from 'components/Chart/Chart.react';
3+
import { ChartColorSchemes } from 'lib/Constants';
4+
import styles from './GraphPanel.scss';
5+
6+
function parseDate(val) {
7+
if (!val) {
8+
return null;
9+
}
10+
if (val instanceof Date) {
11+
return val.getTime();
12+
}
13+
if (typeof val === 'string') {
14+
const d = new Date(val);
15+
return isNaN(d) ? null : d.getTime();
16+
}
17+
if (val.iso) {
18+
const d = new Date(val.iso);
19+
return isNaN(d) ? null : d.getTime();
20+
}
21+
return null;
22+
}
23+
24+
export default function GraphPanel({ selectedCells, order, data, columns, width }) {
25+
if (!selectedCells || selectedCells.rowStart < 0) {
26+
return null;
27+
}
28+
const { rowStart, rowEnd, colStart, colEnd } = selectedCells;
29+
const columnNames = order.slice(colStart, colEnd + 1).map(o => o.name);
30+
const columnTypes = columnNames.map(name => columns[name]?.type);
31+
const timeSeries =
32+
columnTypes.length > 1 &&
33+
columnTypes[0] === 'Date' &&
34+
columnTypes.slice(1).every(t => t === 'Number');
35+
36+
const chartData = {};
37+
if (timeSeries) {
38+
for (let j = 1; j < columnNames.length; j++) {
39+
chartData[columnNames[j]] = { color: ChartColorSchemes[j - 1], points: [] };
40+
}
41+
for (let i = rowStart; i <= rowEnd; i++) {
42+
const row = data[i];
43+
if (!row) continue;
44+
const ts = parseDate(row.attributes[columnNames[0]]);
45+
if (ts === null) continue;
46+
for (let j = 1; j < columnNames.length; j++) {
47+
const val = row.attributes[columnNames[j]];
48+
if (typeof val === 'number' && !isNaN(val)) {
49+
chartData[columnNames[j]].points.push([ts, val]);
50+
}
51+
}
52+
}
53+
} else {
54+
let seriesIndex = 0;
55+
columnNames.forEach((col, idx) => {
56+
if (columnTypes[idx] === 'Number') {
57+
chartData[col] = { color: ChartColorSchemes[seriesIndex], points: [] };
58+
seriesIndex++;
59+
}
60+
});
61+
let x = 0;
62+
for (let i = rowStart; i <= rowEnd; i++, x++) {
63+
const row = data[i];
64+
if (!row) continue;
65+
columnNames.forEach(col => {
66+
const val = row.attributes[col];
67+
if (typeof val === 'number' && !isNaN(val)) {
68+
chartData[col].points.push([x, val]);
69+
}
70+
});
71+
}
72+
}
73+
74+
if (Object.keys(chartData).length === 0) {
75+
return <div className={styles.empty}>No numeric data selected.</div>;
76+
}
77+
78+
const chartWidth = width - 20;
79+
return (
80+
<div className={styles.graphPanel}>
81+
<Chart width={chartWidth} height={400} data={chartData} />
82+
</div>
83+
);
84+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.graphPanel {
2+
height: 100%;
3+
overflow: auto;
4+
background-color: #fefafb;
5+
padding: 10px;
6+
}
7+
8+
.empty {
9+
padding: 10px;
10+
color: #555;
11+
}

src/components/Toolbar/Toolbar.react.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useNavigate, useNavigationType, NavigationType } from 'react-router-dom
1515

1616
const POPOVER_CONTENT_ID = 'toolbarStatsPopover';
1717

18-
const Stats = ({ data, classwiseCloudFunctions, className, appId, appName }) => {
18+
const Stats = ({ data, classwiseCloudFunctions, className, appId, appName, toggleGraph, isGraphVisible }) => {
1919
const [selected, setSelected] = React.useState(null);
2020
const [open, setOpen] = React.useState(false);
2121
const buttonRef = React.useRef();
@@ -108,14 +108,21 @@ const Stats = ({ data, classwiseCloudFunctions, className, appId, appName }) =>
108108
return (
109109
<>
110110
{selected ? (
111-
<button
112-
ref={buttonRef}
113-
className={styles.stats}
114-
onClick={toggle}
115-
style={{ marginRight: rightMarginStyle }}
116-
>
117-
{`${selected.label}: ${selected.getValue(data)}`}
118-
</button>
111+
<>
112+
<button
113+
ref={buttonRef}
114+
className={styles.stats}
115+
onClick={toggle}
116+
style={{ marginRight: rightMarginStyle }}
117+
>
118+
{`${selected.label}: ${selected.getValue(data)}`}
119+
</button>
120+
{data.length > 1 ? (
121+
<button onClick={toggleGraph} className={styles.graph}>
122+
{isGraphVisible ? 'Hide Graph' : 'Show Graph'}
123+
</button>
124+
) : null}
125+
</>
119126
) : null}
120127
{open ? renderPopover() : null}
121128
</>
@@ -152,6 +159,8 @@ const Toolbar = props => {
152159
className={props.className}
153160
appId={props.appId}
154161
appName={props.appName}
162+
toggleGraph={props.toggleGraph}
163+
isGraphVisible={props.isGraphVisible}
155164
/>
156165
) : null}
157166
<div className={styles.actions}>{props.children}</div>
@@ -182,6 +191,8 @@ Toolbar.propTypes = {
182191
details: PropTypes.string,
183192
relation: PropTypes.object,
184193
selectedData: PropTypes.array,
194+
toggleGraph: PropTypes.func,
195+
isGraphVisible: PropTypes.bool,
185196
};
186197

187198
export default Toolbar;

src/components/Toolbar/Toolbar.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,19 @@ body:global(.expanded) {
9999
border: none;
100100
}
101101

102+
.graph {
103+
position: absolute;
104+
right: 120px;
105+
bottom: 10px;
106+
background: $blue;
107+
border-radius: 3px;
108+
padding: 2px 6px;
109+
font-size: 14px;
110+
color: white;
111+
box-shadow: none;
112+
border: none;
113+
}
114+
102115
.stats_popover_container {
103116
display: flex;
104117
flex-direction: column;

src/dashboard/Data/Browser/BrowserToolbar.react.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ const BrowserToolbar = ({
7979

8080
togglePanel,
8181
isPanelVisible,
82+
toggleGraph,
83+
isGraphVisible,
8284
classwiseCloudFunctions,
8385
appId,
8486
appName,
@@ -277,6 +279,8 @@ const BrowserToolbar = ({
277279
selectedData={selectedData}
278280
togglePanel={togglePanel}
279281
isPanelVisible={isPanelVisible}
282+
toggleGraph={toggleGraph}
283+
isGraphVisible={isGraphVisible}
280284
classwiseCloudFunctions={classwiseCloudFunctions}
281285
appId={appId}
282286
appName={appName}

src/dashboard/Data/Browser/DataBrowser.react.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ResizableBox } from 'react-resizable';
1717
import styles from './Databrowser.scss';
1818

1919
import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel';
20+
import GraphPanel from 'components/GraphPanel/GraphPanel';
2021

2122
const BROWSER_SHOW_ROW_NUMBER = 'browserShowRowNumber';
2223

@@ -80,7 +81,7 @@ export default class DataBrowser extends React.Component {
8081
const storedRowNumber =
8182
window.localStorage?.getItem(BROWSER_SHOW_ROW_NUMBER) === 'true';
8283

83-
this.state = {
84+
this.state = {
8485
order: order,
8586
current: null,
8687
editing: false,
@@ -99,6 +100,8 @@ export default class DataBrowser extends React.Component {
99100
showAggregatedData: true,
100101
frozenColumnIndex: -1,
101102
showRowNumber: storedRowNumber,
103+
graphVisible: false,
104+
graphWidth: 300,
102105
};
103106

104107
this.handleResizeDiv = this.handleResizeDiv.bind(this);
@@ -109,6 +112,9 @@ export default class DataBrowser extends React.Component {
109112
this.handleHeaderDragDrop = this.handleHeaderDragDrop.bind(this);
110113
this.handleResize = this.handleResize.bind(this);
111114
this.togglePanelVisibility = this.togglePanelVisibility.bind(this);
115+
this.handleGraphResizeStart = this.handleGraphResizeStart.bind(this);
116+
this.handleGraphResizeStop = this.handleGraphResizeStop.bind(this);
117+
this.handleGraphResizeDiv = this.handleGraphResizeDiv.bind(this);
112118
this.setCurrent = this.setCurrent.bind(this);
113119
this.setEditing = this.setEditing.bind(this);
114120
this.handleColumnsOrder = this.handleColumnsOrder.bind(this);
@@ -120,6 +126,7 @@ export default class DataBrowser extends React.Component {
120126
this.unfreezeColumns = this.unfreezeColumns.bind(this);
121127
this.setShowRowNumber = this.setShowRowNumber.bind(this);
122128
this.handleCellClick = this.handleCellClick.bind(this);
129+
this.toggleGraphVisibility = this.toggleGraphVisibility.bind(this);
123130
this.saveOrderTimeout = null;
124131
}
125132

@@ -215,6 +222,21 @@ export default class DataBrowser extends React.Component {
215222
this.setState({ panelWidth: size.width });
216223
}
217224

225+
handleGraphResizeStart() {
226+
this.setState({ isResizing: true });
227+
}
228+
229+
handleGraphResizeStop(event, { size }) {
230+
this.setState({
231+
isResizing: false,
232+
graphWidth: size.width,
233+
});
234+
}
235+
236+
handleGraphResizeDiv(event, { size }) {
237+
this.setState({ graphWidth: size.width });
238+
}
239+
218240
setShowAggregatedData(bool) {
219241
this.setState({
220242
showAggregatedData: bool,
@@ -264,6 +286,10 @@ export default class DataBrowser extends React.Component {
264286
}
265287
}
266288

289+
toggleGraphVisibility() {
290+
this.setState(prevState => ({ graphVisible: !prevState.graphVisible }));
291+
}
292+
267293
getAllClassesSchema(schema) {
268294
const allClasses = Object.keys(schema.data.get('classes').toObject());
269295
const schemaSimplifiedData = {};
@@ -667,6 +693,7 @@ export default class DataBrowser extends React.Component {
667693
},
668694
selectedObjectId: undefined,
669695
selectedData,
696+
graphVisible: true,
670697
});
671698
} else {
672699
this.setCurrent({ row, col });
@@ -677,6 +704,7 @@ export default class DataBrowser extends React.Component {
677704
selectedData: [],
678705
current: { row, col },
679706
firstSelectedCell: clickedCellKey,
707+
graphVisible: false,
680708
});
681709
}
682710
}
@@ -758,6 +786,29 @@ export default class DataBrowser extends React.Component {
758786
</div>
759787
</ResizableBox>
760788
)}
789+
{this.state.graphVisible && (
790+
<ResizableBox
791+
width={this.state.graphWidth}
792+
height={Infinity}
793+
minConstraints={[100, Infinity]}
794+
maxConstraints={[this.state.maxWidth, Infinity]}
795+
onResizeStart={this.handleGraphResizeStart}
796+
onResizeStop={this.handleGraphResizeStop}
797+
onResize={this.handleGraphResizeDiv}
798+
resizeHandles={['w']}
799+
className={styles.resizablePanel}
800+
>
801+
<div className={styles.aggregationPanelContainer}>
802+
<GraphPanel
803+
selectedCells={this.state.selectedCells}
804+
order={this.state.order}
805+
data={this.props.data}
806+
columns={this.props.columns}
807+
width={this.state.graphWidth}
808+
/>
809+
</div>
810+
</ResizableBox>
811+
)}
761812
</div>
762813

763814
<BrowserToolbar
@@ -787,6 +838,8 @@ export default class DataBrowser extends React.Component {
787838
allClassesSchema={this.state.allClassesSchema}
788839
togglePanel={this.togglePanelVisibility}
789840
isPanelVisible={this.state.isPanelVisible}
841+
toggleGraph={this.toggleGraphVisibility}
842+
isGraphVisible={this.state.graphVisible}
790843
appId={this.props.app.applicationId}
791844
appName={this.props.appName}
792845
{...other}

0 commit comments

Comments
 (0)