Compare.tsx 10.5 KB
Newer Older
Lijiao's avatar
Lijiao committed
1
import * as React from 'react';
2
import { renderToString } from 'react-dom/server';
3
import { Stack, Modal, IconButton, IDragOptions, ContextualMenu, Dropdown, IDropdownOption } from '@fluentui/react';
Lijiao's avatar
Lijiao committed
4
import ReactEcharts from 'echarts-for-react';
5
import { TooltipForIntermediate, TableObj, SingleAxis } from '../../static/interface';
6
import { contentStyles, iconButtonStyles } from '../buttons/ModalTheme';
7
8
import { convertDuration, parseMetrics } from '../../static/function';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
Lijiaoa's avatar
Lijiaoa committed
9
import '../../static/style/compare.scss';
10

11
12
13
14
15
/***
 * Compare file is design for [each trial intermediate result and trials compare function]
 * if trial has dict intermediate result, graph support shows all keys that type is number
 */

16
17
18
function _getWebUIWidth(): number {
    return window.innerWidth;
}
Lijiaoa's avatar
Lijiaoa committed
19
20
21
22
23
24

const dragOptions: IDragOptions = {
    moveMenuItemText: 'Move',
    closeMenuItemText: 'Close',
    menu: ContextualMenu
};
Lijiao's avatar
Lijiao committed
25

26
27
// TODO: this should be refactored to the common modules
// copied from trial.ts
28
function _parseIntermediates(trial: TableObj, key: string): number[] {
29
30
31
32
33
34
35
36
    const intermediates: number[] = [];
    for (const metric of trial.intermediates) {
        if (metric === undefined) {
            break;
        }
        const parsedMetric = parseMetrics(metric.data);
        if (typeof parsedMetric === 'object') {
            // TODO: should handle more types of metric keys
37
            intermediates.push(parsedMetric[key]);
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
        } else {
            intermediates.push(parsedMetric);
        }
    }
    return intermediates;
}

interface Item {
    id: string;
    sequenceId: number;
    duration: string;
    parameters: Map<string, any>;
    metrics: Map<string, any>;
    intermediates: number[];
}

Lijiao's avatar
Lijiao committed
54
interface CompareProps {
55
56
57
    trials: TableObj[];
    title: string;
    showDetails: boolean;
58
    intermediateKeyList?: string[];
59
    onHideDialog: () => void;
60
    changeSelectTrialIds?: () => void;
Lijiao's avatar
Lijiao committed
61
62
}

63
64
65
66
67
interface CompareState {
    intermediateKey: string; // default, dict other keys
}

class Compare extends React.Component<CompareProps, CompareState> {
Lijiao's avatar
Lijiao committed
68
69
    constructor(props: CompareProps) {
        super(props);
70
71

        this.state = {
72
73
74
            // intermediate result maybe don't have the 'default' key...
            intermediateKey:
                this.props.intermediateKeyList !== undefined ? this.props.intermediateKeyList[0] : 'default'
75
        };
Lijiao's avatar
Lijiao committed
76
77
    }

78
79
    private _generateTooltipSummary = (row: Item, value: string): string =>
        renderToString(
80
            <div className='tooldetailAccuracy'>
81
                <div>Trial No.: {row.sequenceId}</div>
82
                <div>Trial ID: {row.id}</div>
83
                <div>Intermediate metric: {value}</div>
84
85
86
            </div>
        );

87
    private _intermediates(items: Item[]): React.ReactNode {
88
89
90
91
92
93
94
95
96
97
98
        // Precondition: make sure `items` is not empty
        const xAxisMax = Math.max(...items.map(item => item.intermediates.length));
        const xAxis = Array(xAxisMax)
            .fill(0)
            .map((_, i) => i + 1); // [1, 2, 3, ..., xAxisMax]
        const dataForEchart = items.map(item => ({
            name: item.id,
            data: item.intermediates,
            type: 'line'
        }));
        const legend = dataForEchart.map(item => item.name);
Lijiao's avatar
Lijiao committed
99
100
101
102
        const option = {
            tooltip: {
                trigger: 'item',
                enterable: true,
103
                confine: true,
104
105
                formatter: (data: TooltipForIntermediate): string => {
                    const item = items.find(k => k.id === data.seriesName) as Item;
106
                    return this._generateTooltipSummary(item, data.data);
Lijiao's avatar
Lijiao committed
107
108
109
110
111
112
113
114
                }
            },
            grid: {
                left: '5%',
                top: 40,
                containLabel: true
            },
            legend: {
115
116
                type: 'scroll',
                right: 40,
117
                left: legend.length > 6 ? '15%' : null,
118
                data: legend
Lijiao's avatar
Lijiao committed
119
120
121
122
123
124
125
126
            },
            xAxis: {
                type: 'category',
                boundaryGap: false,
                data: xAxis
            },
            yAxis: {
                type: 'value',
127
128
                name: 'Metric',
                scale: true
Lijiao's avatar
Lijiao committed
129
            },
130
            series: dataForEchart
Lijiao's avatar
Lijiao committed
131
132
        };
        return (
133
134
135
136
137
138
139
            <div className='graph'>
                <ReactEcharts
                    option={option}
                    style={{ width: '100%', height: 418, margin: '0 auto' }}
                    notMerge={true} // update now
                />
            </div>
Lijiao's avatar
Lijiao committed
140
        );
141
    }
Lijiao's avatar
Lijiao committed
142

143
144
145
146
147
148
149
150
151
152
153
154
    private _renderRow(
        key: string,
        rowName: string,
        className: string,
        items: Item[],
        formatter: (item: Item) => string
    ): React.ReactNode {
        return (
            <tr key={key}>
                <td className='column'>{rowName}</td>
                {items.map(item => (
                    <td className={className} key={item.id}>
155
                        {formatter(item) || '--'}
156
157
158
159
160
                    </td>
                ))}
            </tr>
        );
    }
161

162
163
164
165
166
167
168
169
170
171
172
173
174
175
    private _overlapKeys(s: Map<string, any>[]): string[] {
        // Calculate the overlapped keys for multiple
        const intersection: string[] = [];
        for (const i of s[0].keys()) {
            let inAll = true;
            for (const t of s) {
                if (!Array.from(t.keys()).includes(i)) {
                    inAll = false;
                    break;
                }
            }
            if (inAll) {
                intersection.push(i);
            }
Lijiao's avatar
Lijiao committed
176
        }
177
178
179
180
181
182
183
184
        return intersection;
    }

    // render table column ---
    private _columns(items: Item[]): React.ReactNode {
        // Precondition: make sure `items` is not empty
        const width = _getWebUIWidth();
        let scrollClass: string = '';
185
        if (width > 1200) {
186
            scrollClass = items.length > 3 ? 'flex' : '';
187
        } else if (width < 700) {
188
            scrollClass = items.length > 1 ? 'flex' : '';
189
        } else {
190
            scrollClass = items.length > 2 ? 'flex' : '';
191
        }
192
193
        const parameterKeys = this._overlapKeys(items.map(item => item.parameters));
        const metricKeys = this._overlapKeys(items.map(item => item.metrics));
194

Lijiao's avatar
Lijiao committed
195
        return (
196
            <table className={`compare-modal-table ${scrollClass}`}>
Lijiao's avatar
Lijiao committed
197
                <tbody>
198
199
200
201
202
203
                    {this._renderRow('id', 'ID', 'value idList', items, item => item.id)}
                    {this._renderRow('trialnum', 'Trial No.', 'value', items, item => item.sequenceId.toString())}
                    {this._renderRow('duration', 'Duration', 'value', items, item => item.duration)}
                    {parameterKeys.map(k =>
                        this._renderRow(`space_${k}`, k, 'value', items, item => item.parameters.get(k))
                    )}
204
205
206
207
208
209
210
                    {metricKeys !== undefined
                        ? metricKeys.map(k =>
                              this._renderRow(`metrics_${k}`, `Metric: ${k}`, 'value', items, item =>
                                  item.metrics.get(k)
                              )
                          )
                        : null}
Lijiao's avatar
Lijiao committed
211
212
213
214
215
                </tbody>
            </table>
        );
    }

216
217
218
219
220
221
222
223
224
    private closeCompareModal = (): void => {
        const { showDetails, changeSelectTrialIds, onHideDialog } = this.props;
        if (showDetails === true) {
            // eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
            changeSelectTrialIds!();
        }
        onHideDialog();
    };

225
226
227
228
229
230
    private selectOtherKeys = (_event: React.FormEvent<HTMLDivElement>, item?: IDropdownOption): void => {
        if (item !== undefined) {
            this.setState(() => ({ intermediateKey: item.text }));
        }
    };

231
    render(): React.ReactNode {
232
        const { trials, title, showDetails, intermediateKeyList } = this.props;
233
        const { intermediateKey } = this.state;
234
        const intermediateAllKeysList: string[] = intermediateKeyList !== undefined ? intermediateKeyList : [];
235
236
237
238
239
240
241
242
243
244
        const flatten = (m: Map<SingleAxis, any>): Map<string, any> => {
            return new Map(Array.from(m).map(([key, value]) => [key.baseName, value]));
        };
        const inferredSearchSpace = TRIALS.inferredSearchSpace(EXPERIMENT.searchSpaceNew);
        const items: Item[] = trials.map(trial => ({
            id: trial.id,
            sequenceId: trial.sequenceId,
            duration: convertDuration(trial.duration),
            parameters: flatten(trial.parameters(inferredSearchSpace)),
            metrics: flatten(trial.metrics(TRIALS.inferredMetricSpace())),
245
            intermediates: _parseIntermediates(trial, intermediateKey)
246
        }));
Lijiao's avatar
Lijiao committed
247
248
249

        return (
            <Modal
250
251
                isOpen={true}
                containerClassName={contentStyles.container}
252
                className='compare-modal'
Lijiaoa's avatar
Lijiaoa committed
253
254
                allowTouchBodyScroll={true}
                dragOptions={dragOptions}
255
                onDismiss={this.closeCompareModal}
Lijiao's avatar
Lijiao committed
256
            >
257
258
                <div>
                    <div className={contentStyles.header}>
259
                        <span>{title}</span>
260
261
262
                        <IconButton
                            styles={iconButtonStyles}
                            iconProps={{ iconName: 'Cancel' }}
263
                            ariaLabel='Close popup modal'
264
                            onClick={this.closeCompareModal}
265
266
                        />
                    </div>
267
268
                    {intermediateAllKeysList.length > 1 ||
                    (intermediateAllKeysList.length === 1 && intermediateAllKeysList !== ['default']) ? (
269
270
271
272
273
274
275
276
277
278
279
280
                        <Stack horizontalAlign='end' className='selectKeys'>
                            <Dropdown
                                className='select'
                                selectedKey={intermediateKey}
                                options={intermediateAllKeysList.map((key, item) => ({
                                    key: key,
                                    text: intermediateAllKeysList[item]
                                }))}
                                onChange={this.selectOtherKeys}
                            />
                        </Stack>
                    ) : null}
281
                    <Stack className='compare-modal-intermediate'>
282
                        {this._intermediates(items)}
283
                        <Stack className='compare-yAxis'># Intermediate result</Stack>
284
                    </Stack>
285
                    {showDetails && <Stack>{this._columns(items)}</Stack>}
286
                </div>
Lijiao's avatar
Lijiao committed
287
288
289
290
291
292
            </Modal>
        );
    }
}

export default Compare;