Skip to content

Compare everything with everything else #840

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion js/Pane.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class Pane extends React.Component {
};
};

updateCompareViewSelection = value => {};

shouldComponentUpdate(nextProps) {
if (this.props.contentID !== nextProps.contentID) {
return true;
Expand All @@ -81,6 +83,8 @@ class Pane extends React.Component {
}

render() {
let widgets = [].concat(this.props.widgets);

let windowClassNames = classNames({
window: true,
focus: this.props.isFocused,
Expand All @@ -91,6 +95,27 @@ class Pane extends React.Component {
focus: this.props.isFocused,
});

// compare view selection
if (
this.props.has_compare &&
this.props.compare_content_info &&
this.props.compare_view_mode != 'merge'
) {
var select = this.props.compare_selection_i;
widgets.push(
<div key="compare_selection" className="widget compare_selection">
<span>Selected Env</span>
<select onChange={this.props.updateCompareViewSelection}>
{this.props.compare_content_info.map((info, id) => (
<option key={id} value={info.content_i}>
{info.plot_name}
</option>
))}
</select>
</div>
);
}

return (
<div
className={windowClassNames}
Expand All @@ -117,7 +142,7 @@ class Pane extends React.Component {
<div>{this.props.title}</div>
</div>
<div className="content">{this.props.children}</div>
<div className="widgets">{this.props.widgets}</div>
<div className="widgets">{widgets}</div>
</div>
);
}
Expand Down
42 changes: 36 additions & 6 deletions js/PlotPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,39 @@ class PlotPane extends React.Component {
}

newPlot = () => {
Plotly.newPlot(
this.props.contentID,
this.props.content.data,
this.props.content.layout,
{ showLink: true, linkText: 'Edit' }
);
// determine data based on window (compare) settings
if (
!this.props.has_compare ||
(this.props.compare_view_mode && this.props.compare_view_mode == 'select')
)
var content = this.props.content;
// merge mode (for scatter plots)
else {
var layout = this.props.compare_content[0].layout;
layout.showlegend = true;

// first merge list of data-lists into flat data-list
var data = this.props.compare_content
.map(function(content) {
return content.data;
})
.flat();

// use the modified compare_name as labels
data.forEach(function(val) {
val.name = val.compare_name;
});

var content = {
layout: layout,
data: data,
};
}

Plotly.newPlot(this.props.contentID, content.data, content.layout, {
showLink: true,
linkText: 'Edit',
});
};

handleDownload = () => {
Expand All @@ -83,11 +110,14 @@ class PlotPane extends React.Component {
};

render() {
let widgets = [];

return (
<Pane
{...this.props}
handleDownload={this.handleDownload}
ref={ref => (this._paneRef = ref)}
widgets={widgets}
>
<div
id={this.props.contentID}
Expand Down
10 changes: 10 additions & 0 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1572,9 +1572,16 @@ class App extends React.Component {
);
}

updateCompareViewSelection(evt) {
this.compare_selection_i = evt.target.value;
}

render() {
let panes = Object.keys(this.state.panes).map(id => {
let pane = this.state.panes[id];
if (pane.has_compare && pane.compare_content) {
pane.content = pane.compare_content[pane.compare_selection_i];
}

try {
let Comp = PANES[pane.type];
Expand Down Expand Up @@ -1610,6 +1617,9 @@ class App extends React.Component {
sendPaneMessage: this.sendPaneMessage,
sendEmbeddingPop: this.sendEmbeddingPop,
}}
updateCompareViewSelection={this.updateCompareViewSelection.bind(
pane
)}
/>
</ReactResizeDetector>
</div>
Expand Down
157 changes: 88 additions & 69 deletions py/visdom/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1455,92 +1455,110 @@ def gather_envs(state, env_path=DEFAULT_ENV_PATH):

def compare_envs(state, eids, socket, env_path=DEFAULT_ENV_PATH):
logging.info('comparing envs')
eidNums = {e: str(i) for i, e in enumerate(eids)}
env = {}
envs = {}

# queries from eids-list
# - envs: a list of all (eid -> env) pairs. (directly loads envs if not yet loaded)
# - res: a single env containing all windows with titles
# - title2Win: a dict of all (title -> win) pairs
# note: In case multiple windows share the same title, any window
# could be used. we use the first occurence as a compare view
envs, res, title2Win = [], {'jsons': {}, 'reload': {}}, {}
for eid in eids:

if eid in state:
envs[eid] = state.get(eid)
env = state.get(eid)
elif env_path is not None:
p = os.path.join(env_path, eid.strip(), '.json')
if os.path.exists(p):
with open(p, 'r') as fn:
env = tornado.escape.json_decode(fn.read())
state[eid] = env
envs[eid] = env

res = copy.deepcopy(envs[list(envs.keys())[0]])
name2Wid = {res['jsons'][wid].get('title', None): wid + '_compare'
for wid in res.get('jsons', {})
if 'title' in res['jsons'][wid]}
for wid in list(res['jsons'].keys()):
res['jsons'][wid + '_compare'] = res['jsons'][wid]
res['jsons'][wid] = None
res['jsons'].pop(wid)

for ix, eid in enumerate(envs.keys()):
env = envs[eid]
else:
continue

envs.append(env)
for winId, win in env['jsons'].items():
if "title" in win and win["title"] and win["title"] not in title2Win:
comparewinId = winId + "_compare"
title2Win[win["title"]] = comparewinId
res['jsons'][comparewinId] = copy.deepcopy(env['jsons'][winId])
if isinstance(res['jsons'][comparewinId]['content'], dict):
res['jsons'][comparewinId]['content']["data"] = []
else:
res['jsons'][comparewinId]['content'] = ""
res['jsons'][comparewinId]["compare_content"] = []
res['jsons'][comparewinId]["compare_selection_i"] = 0
res['jsons'][comparewinId]['has_compare'] = True
res['jsons'][comparewinId]['compare_view_mode'] = "select"
res['jsons'][comparewinId]['compare_content_info'] = []
res['jsons'][comparewinId]['contentID'] = get_rand_id()
logging.error("compare")

# TODO: next, merge
tableRows = []
for eidNum, env in enumerate(envs):

perEnvTitleCount = {}
for wid in env.get('jsons', {}).keys():
win = env['jsons'][wid]
if win.get('type', None) != 'plot':
continue
if 'content' not in win:
continue
if 'title' not in win:
continue
title = win['title']
if title not in name2Wid or title == '':
if 'title' not in win or not win["title"]:
continue

destWid = name2Wid[title]
destWidJson = res['jsons'][destWid]
# Combine plots with the same window title. If plot data source was
# labeled "name" in the legend, rename to "envId_legend" where
# envId is enumeration of the selected environments (not the long
# environment id string). This makes plot lines more readable.
if ix == 0:
if 'name' not in destWidJson['content']['data'][0]:
continue # Skip windows with unnamed data
destWidJson['has_compare'] = False
destWidJson['content']['layout']['showlegend'] = True
destWidJson['contentID'] = get_rand_id()
for dataIdx, data in enumerate(destWidJson['content']['data']):
if 'name' not in data:
break # stop working with this plot, not right format
destWidJson['content']['data'][dataIdx]['name'] = \
'{}_{}'.format(eidNums[eid], data['name'])
# set up the window to show compare data in
title = win["title"]
content_copy = copy.deepcopy(win['content'])
destwin = res['jsons'][title2Win[title]]
if title not in perEnvTitleCount:
perEnvTitleCount[title] = 0
else:
if 'name' not in destWidJson['content']['data'][0]:
continue # Skip windows with unnamed data
# has_compare will be set to True only if the window title is
# shared by at least 2 envs.
destWidJson['has_compare'] = True
for _dataIdx, data in enumerate(win['content']['data']):
data = copy.deepcopy(data)
if 'name' not in data:
destWidJson['has_compare'] = False
break # stop working with this plot, not right format
data['name'] = '{}_{}'.format(eidNums[eid], data['name'])
destWidJson['content']['data'].append(data)

# Make sure that only plots that are shared by at least two envs are shown.
# Check has_compare flag
for destWid in list(res['jsons'].keys()):
if ('has_compare' not in res['jsons'][destWid]) or \
(not res['jsons'][destWid]['has_compare']):
del res['jsons'][destWid]

# create legend mapping environment names to environment numbers so one can
# look it up for the new legend
tableRows = ["<tr> <td> {} </td> <td> {} </td> </tr>".format(v, eidNums[v])
for v in eidNums]
perEnvTitleCount[title] += 1
destwin['compare_content_info'].append({
"envId": eidNum,
"plot_name": str(eidNum)+"_"+str(perEnvTitleCount[title]),
"content_i": len(destwin['compare_content']),
})
destwin['compare_content'].append(content_copy)

# If plot data source was labeled "name" in the legend, rename to
# "envId_legend" where envId is enumeration of the selected
# environments (not the long environment id string). This makes plot
# lines more readable.
if isinstance(content_copy, dict) and "data" in content_copy:
for _dataIdx, data in enumerate(content_copy["data"]):
if 'name' in data:
data['compare_name'] = '{}_{}'.format(eidNum, data['name'])

# create legend mapping environment names to environment numbers so one can
# look it up for the new legend
tableRows.append("<tr> <td> {} </td> <td> {} </td> </tr>".format(eids[eidNum], eidNum))

# in case all plot types in a window are line plot, we can use merge-mode
for win in res['jsons'].values():
all_scatter = True
if isinstance(win['content'], dict) and 'layout' in win['content']:
for content_list in win['compare_content']:
for data in content_list["data"]:
if data['type'] != 'scatter':
all_scatter = False
break
if not all_scatter:
break
if all_scatter:
win['content']['layout']['showlegend'] = True
win['compare_view_mode'] = 'merge'
else:
if isinstance(win['content'], dict) and 'layout' in win['content'] and 'margin' in win['content']['layout']:
win['content']['layout']['margin']['b'] += 20

tbl = """"<style>
table, th, td {{
border: 1px solid black;
}}
</style>
<table> {} </table>""".format(' '.join(tableRows))
<table> {} </table>
""".format(' '.join(tableRows))

res['jsons']['window_compare_legend'] = {
"command": "window",
Expand All @@ -1556,10 +1574,11 @@ def compare_envs(state, eids, socket, env_path=DEFAULT_ENV_PATH):
"i": 1,
"has_compare": True,
}
if 'reload' in res:
socket.write_message(
json.dumps({'command': 'reload', 'data': res['reload']})
)
# TODO needed?
# if 'reload' in res:
# socket.write_message(
# json.dumps({'command': 'reload', 'data': res['reload']})
# )

jsons = list(res.get('jsons', {}).values())
windows = sorted(jsons, key=lambda k: ('i' not in k, k.get('i', None)))
Expand Down
9 changes: 9 additions & 0 deletions py/visdom/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,12 @@ doesn't override our desire to make them non-interactable */
.content-properties {
background-color: #ffffff;
}

.widget.compare_selection {
color: #333;
font-size: 11px;
}

.widget.compare_selection > span, .widget.compare_selection > select {
margin: 0 5px;
}
Loading