Compare.tsx 8.75 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 } 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

function _getWebUIWidth(): number {
    return window.innerWidth;
}
Lijiaoa's avatar
Lijiaoa committed
14
15
16
17
18
19

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

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// TODO: this should be refactored to the common modules
// copied from trial.ts
function _parseIntermediates(trial: TableObj): number[] {
    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
            intermediates.push(parsedMetric.default);
        } 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
49
interface CompareProps {
50
51
52
53
    trials: TableObj[];
    title: string;
    showDetails: boolean;
    onHideDialog: () => void;
54
    changeSelectTrialIds?: () => void;
Lijiao's avatar
Lijiao committed
55
56
57
58
59
60
61
}

class Compare extends React.Component<CompareProps, {}> {
    constructor(props: CompareProps) {
        super(props);
    }

62
63
    private _generateTooltipSummary = (row: Item, value: string): string =>
        renderToString(
64
            <div className='tooldetailAccuracy'>
65
                <div>Trial No.: {row.sequenceId}</div>
66
                <div>Trial ID: {row.id}</div>
67
                <div>Intermediate metric: {value}</div>
68
69
70
            </div>
        );

71
    private _intermediates(items: Item[]): React.ReactNode {
72
73
74
75
76
77
78
79
80
81
82
        // 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
83
84
85
86
        const option = {
            tooltip: {
                trigger: 'item',
                enterable: true,
87
                confine: true,
88
89
                formatter: (data: TooltipForIntermediate): string => {
                    const item = items.find(k => k.id === data.seriesName) as Item;
90
                    return this._generateTooltipSummary(item, data.data);
Lijiao's avatar
Lijiao committed
91
92
93
94
95
96
97
98
                }
            },
            grid: {
                left: '5%',
                top: 40,
                containLabel: true
            },
            legend: {
99
100
                type: 'scroll',
                right: 40,
101
                left: legend.length > 6 ? '15%' : null,
102
                data: legend
Lijiao's avatar
Lijiao committed
103
104
105
106
107
108
109
110
            },
            xAxis: {
                type: 'category',
                boundaryGap: false,
                data: xAxis
            },
            yAxis: {
                type: 'value',
111
112
                name: 'Metric',
                scale: true
Lijiao's avatar
Lijiao committed
113
            },
114
            series: dataForEchart
Lijiao's avatar
Lijiao committed
115
116
        };
        return (
117
118
119
120
121
122
123
            <div className='graph'>
                <ReactEcharts
                    option={option}
                    style={{ width: '100%', height: 418, margin: '0 auto' }}
                    notMerge={true} // update now
                />
            </div>
Lijiao's avatar
Lijiao committed
124
        );
125
    }
Lijiao's avatar
Lijiao committed
126

127
128
129
130
131
132
133
134
135
136
137
138
    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}>
139
                        {formatter(item) || '--'}
140
141
142
143
144
                    </td>
                ))}
            </tr>
        );
    }
145

146
147
148
149
150
151
152
153
154
155
156
157
158
159
    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
160
        }
161
162
163
164
165
166
167
168
        return intersection;
    }

    // render table column ---
    private _columns(items: Item[]): React.ReactNode {
        // Precondition: make sure `items` is not empty
        const width = _getWebUIWidth();
        let scrollClass: string = '';
169
        if (width > 1200) {
170
            scrollClass = items.length > 3 ? 'flex' : '';
171
        } else if (width < 700) {
172
            scrollClass = items.length > 1 ? 'flex' : '';
173
        } else {
174
            scrollClass = items.length > 2 ? 'flex' : '';
175
        }
176
177
        const parameterKeys = this._overlapKeys(items.map(item => item.parameters));
        const metricKeys = this._overlapKeys(items.map(item => item.metrics));
178

Lijiao's avatar
Lijiao committed
179
        return (
180
            <table className={`compare-modal-table ${scrollClass}`}>
Lijiao's avatar
Lijiao committed
181
                <tbody>
182
183
184
185
186
187
                    {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))
                    )}
188
189
190
191
192
193
194
                    {metricKeys !== undefined
                        ? metricKeys.map(k =>
                              this._renderRow(`metrics_${k}`, `Metric: ${k}`, 'value', items, item =>
                                  item.metrics.get(k)
                              )
                          )
                        : null}
Lijiao's avatar
Lijiao committed
195
196
197
198
199
                </tbody>
            </table>
        );
    }

200
201
202
203
204
205
206
207
208
    private closeCompareModal = (): void => {
        const { showDetails, changeSelectTrialIds, onHideDialog } = this.props;
        if (showDetails === true) {
            // eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
            changeSelectTrialIds!();
        }
        onHideDialog();
    };

209
    render(): React.ReactNode {
210
        const { trials, title, showDetails } = this.props;
211
212
213
214
215
216
217
218
219
220
221
222
        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())),
            intermediates: _parseIntermediates(trial)
        }));
Lijiao's avatar
Lijiao committed
223
224
225

        return (
            <Modal
226
227
                isOpen={true}
                containerClassName={contentStyles.container}
228
                className='compare-modal'
Lijiaoa's avatar
Lijiaoa committed
229
230
                allowTouchBodyScroll={true}
                dragOptions={dragOptions}
231
                onDismiss={this.closeCompareModal}
Lijiao's avatar
Lijiao committed
232
            >
233
234
                <div>
                    <div className={contentStyles.header}>
235
                        <span>{title}</span>
236
237
238
                        <IconButton
                            styles={iconButtonStyles}
                            iconProps={{ iconName: 'Cancel' }}
239
                            ariaLabel='Close popup modal'
240
                            onClick={this.closeCompareModal}
241
242
                        />
                    </div>
243
                    <Stack className='compare-modal-intermediate'>
244
                        {this._intermediates(items)}
245
                        <Stack className='compare-yAxis'># Intermediate result</Stack>
246
                    </Stack>
247
                    {showDetails && <Stack>{this._columns(items)}</Stack>}
248
                </div>
Lijiao's avatar
Lijiao committed
249
250
251
252
253
254
            </Modal>
        );
    }
}

export default Compare;