|
1 | 1 | import React, { useEffect, useState } from 'react' |
2 | 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
3 | | -import { faPalette, faExternalLinkAlt, faTimes, faSearch } from '@fortawesome/free-solid-svg-icons' |
| 3 | +import { |
| 4 | + faPalette, |
| 5 | + faExternalLinkAlt, |
| 6 | + faTimes, |
| 7 | + faSearch, |
| 8 | + faFileUpload, |
| 9 | + faGlobe, faFileDownload |
| 10 | +} from '@fortawesome/free-solid-svg-icons' |
4 | 11 | import Modal from 'react-bootstrap/lib/Modal' |
5 | 12 | import { HexColorPicker, HexColorInput } from 'react-colorful' |
6 | 13 | import Button from 'react-bootstrap/lib/Button' |
@@ -215,6 +222,9 @@ export default function ScatterPlotLegend({ |
215 | 222 | }) { |
216 | 223 | // is the user currently in color-editing mode |
217 | 224 | const [showColorControls, setShowColorControls] = useState(false) |
| 225 | + const [globalColorUpdate, setGlobalColorUpdate] = useState(false) |
| 226 | + const [toggleClassName, setToggleClassName] = useState('fa-toggle-off') |
| 227 | + |
218 | 228 | // whether a request to the server to save colors is pending |
219 | 229 | const labels = getLegendSortedLabels(countsByLabel) |
220 | 230 | const numLabels = labels.length |
@@ -246,18 +256,40 @@ export default function ScatterPlotLegend({ |
246 | 256 | /** resets any unsaved changes to user colors and clears custom colors */ |
247 | 257 | async function resetColors() { |
248 | 258 | setEditedCustomColors({}) |
249 | | - await saveCustomColors({}) |
| 259 | + await saveCustomColors({}, globalColorUpdate) |
250 | 260 | setShowColorControls(false) |
251 | 261 | } |
252 | 262 |
|
253 | 263 | /** save the colors to the server */ |
254 | 264 | async function saveColors() { |
255 | 265 | // merge the user picked colors with existing custom colors so previously saved values are preserved |
256 | 266 | const colorsToSave = Object.assign(customColors, editedCustomColors) |
257 | | - await saveCustomColors(colorsToSave) |
| 267 | + await saveCustomColors(colorsToSave, globalColorUpdate) |
258 | 268 | setShowColorControls(false) |
259 | 269 | } |
260 | 270 |
|
| 271 | + function exportColors() { |
| 272 | + const colorMap = Object.keys(customColors).length > 0 ? customColors : refColorMap |
| 273 | + const lines = Object.entries(colorMap).map(([label, color]) => { |
| 274 | + return `${label}\t${color}\n` |
| 275 | + }) |
| 276 | + |
| 277 | + // Create an element with an anchor link and connect this to the blob |
| 278 | + const element = document.createElement('a') |
| 279 | + const colorExport = new Blob(lines, { type: 'text/plain' }) |
| 280 | + element.href = URL.createObjectURL(colorExport) |
| 281 | + |
| 282 | + // name the file and indicate it should download |
| 283 | + element.download = `${name}_color_map.tsv` |
| 284 | + |
| 285 | + // Simulate clicking the link resulting in downloading the file |
| 286 | + document.body.appendChild(element) |
| 287 | + element.click() |
| 288 | + |
| 289 | + // Cleanup |
| 290 | + document.body.removeChild(element) |
| 291 | + } |
| 292 | + |
261 | 293 | /** collect general information when a user's mouse enters the legend */ |
262 | 294 | function logMouseEnter() { |
263 | 295 | log('hover:scatterlegend', { numLabels }) |
@@ -323,6 +355,40 @@ export default function ScatterPlotLegend({ |
323 | 355 | setLabelsToShow(labels) |
324 | 356 | } |
325 | 357 |
|
| 358 | + /** handle clicking global color update toggle */ |
| 359 | + function handleToggleGlobalColor() { |
| 360 | + const toggleClass = toggleClassName === 'fa-toggle-on' ? 'fa-toggle-off' : 'fa-toggle-on' |
| 361 | + setGlobalColorUpdate(!globalColorUpdate) |
| 362 | + setToggleClassName(toggleClass) |
| 363 | + } |
| 364 | + |
| 365 | + /** read uploaded manifest and apply colors to current scatter plot */ |
| 366 | + function readColorManifest(file) { |
| 367 | + const colorUpdate = {} |
| 368 | + const fileReader = new FileReader() |
| 369 | + fileReader.onloadend = () => { |
| 370 | + const lines = fileReader.result.trim().split(/\n/) |
| 371 | + lines.map((line, _) => { |
| 372 | + const entry = line.split(/[\t,]/).map((l, _) => {return l.trim()}) |
| 373 | + const label = entry[0] |
| 374 | + const color = entry[1] |
| 375 | + colorUpdate[label] = color |
| 376 | + }) |
| 377 | + saveCustomColors(colorUpdate, globalColorUpdate) |
| 378 | + } |
| 379 | + fileReader.readAsText(file) |
| 380 | + } |
| 381 | + |
| 382 | + const globalSwitch = |
| 383 | + <label htmlFor="global-color-update" |
| 384 | + data-toggle="tooltip" |
| 385 | + className="color-update-toggle" |
| 386 | + title="Apply color changes globally for this annotation"> |
| 387 | + <span className={globalColorUpdate ? 'text-info' : 'text-muted'} onClick={handleToggleGlobalColor}>Global |
| 388 | + <i className={`fa fa-fw ${toggleClassName}`}></i> |
| 389 | + </span> |
| 390 | + </label> |
| 391 | + |
326 | 392 | return ( |
327 | 393 | <div |
328 | 394 | className={`scatter-legend ${filteredClass}`} |
@@ -379,18 +445,41 @@ export default function ScatterPlotLegend({ |
379 | 445 | <a role="button" className="pull-right" data-analytics-name="legend-color-picker-cancel" onClick={cancelColors}> |
380 | 446 | Cancel |
381 | 447 | </a><br/> |
382 | | - |
| 448 | + {globalSwitch} |
383 | 449 | <a role="button" className="pull-right" data-analytics-name="legend-color-picker-reset" onClick={resetColors}> |
384 | 450 | Reset to defaults |
385 | 451 | </a> |
386 | 452 | </div> |
387 | 453 | </> |
388 | 454 | } |
389 | 455 | { !showColorControls && |
390 | | - <a role="button" data-analytics-name="legend-color-picker-show" onClick={() => setShowColorControls(true)}> |
391 | | - Customize colors <FontAwesomeIcon icon={faPalette}/> |
392 | | - </a> |
393 | | - } |
| 456 | + <> |
| 457 | + <a role="button" |
| 458 | + className='customize-color-palette' |
| 459 | + data-analytics-name="legend-color-picker-show" |
| 460 | + onClick={() => setShowColorControls(true)} |
| 461 | + > |
| 462 | + Customize <FontAwesomeIcon icon={faPalette}/> |
| 463 | + </a> |
| 464 | + {globalSwitch} |
| 465 | + <label htmlFor="color-manifest-upload" |
| 466 | + data-toggle="tooltip" |
| 467 | + className="icon-button" |
| 468 | + title="Upload a manifest of annotation labels to color hex codes"> |
| 469 | + <input id="color-manifest-upload" |
| 470 | + type="file" |
| 471 | + onChange={e => readColorManifest(e.target.files[0])}/> |
| 472 | + <FontAwesomeIcon className="action fa-lg" icon={faFileUpload} /> |
| 473 | + </label> |
| 474 | + <label htmlFor="color-manifest-export" |
| 475 | + data-toggle="tooltip" |
| 476 | + title="Export current color manifest" |
| 477 | + onClick={exportColors} |
| 478 | + > |
| 479 | + <FontAwesomeIcon className="action fa-lg" icon={faFileDownload} /> |
| 480 | + </label> |
| 481 | + </> |
| 482 | + } |
394 | 483 | </div> |
395 | 484 | } |
396 | 485 | <div> |
|
0 commit comments