Compare.tsx 8.39 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';
Lijiao's avatar
Lijiao committed
7
import '../../static/style/compare.scss';
8
9
10
11
12
13
import { convertDuration, parseMetrics } from '../../static/function';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';

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;
Lijiao's avatar
Lijiao committed
54
55
56
57
58
59
60
}

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

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

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

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

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

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

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

199
    render(): React.ReactNode {
200
201
202
203
204
205
206
207
208
209
210
211
212
        const { onHideDialog, trials, title, showDetails } = this.props;
        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
213
214
215

        return (
            <Modal
216
217
                isOpen={true}
                containerClassName={contentStyles.container}
218
                className='compare-modal'
Lijiaoa's avatar
Lijiaoa committed
219
220
                allowTouchBodyScroll={true}
                dragOptions={dragOptions}
221
                onDismiss={onHideDialog}
Lijiao's avatar
Lijiao committed
222
            >
223
224
                <div>
                    <div className={contentStyles.header}>
225
                        <span>{title}</span>
226
227
228
                        <IconButton
                            styles={iconButtonStyles}
                            iconProps={{ iconName: 'Cancel' }}
229
                            ariaLabel='Close popup modal'
230
                            onClick={onHideDialog}
231
232
                        />
                    </div>
233
                    <Stack className='compare-modal-intermediate'>
234
                        {this._intermediates(items)}
235
                        <Stack className='compare-yAxis'># Intermediate result</Stack>
236
                    </Stack>
237
                    {showDetails && <Stack>{this._columns(items)}</Stack>}
238
                </div>
Lijiao's avatar
Lijiao committed
239
240
241
242
243
244
            </Modal>
        );
    }
}

export default Compare;