Skip to content

Commit

Permalink
Allow downloading/copying data from the slice editor.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 500739910
  • Loading branch information
cjqian authored and LIT team committed Jan 9, 2023
1 parent 5ee03f6 commit 57fac3a
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 87 deletions.
14 changes: 6 additions & 8 deletions lit_nlp/client/core/slice_module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
:host {
--slice-selector-right-col-width: 140px;
--slice-selector-right-col-width: 200px;
}

.module-container {
Expand Down Expand Up @@ -80,25 +80,23 @@
overflow: auto;
}

.number-label {
.right-action-menu {
float: right;
color: #5f6368;
max-width: var(--slice-selector-right-col-width);
}

.number-label {
padding-right: 8px;
}

.selector-item .icon-button {
font-size: 18px;
vertical-align: middle;
margin-left: 3px;
margin-bottom: 5px;
}

.selector-item .icon-button.disabled {
pointer-events: none;
cursor: auto;
opacity: .2;
}

.hidden {
visibility: hidden;
}
45 changes: 36 additions & 9 deletions lit_nlp/client/core/slice_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* limitations under the License.
*/

import '../elements/export_controls';
// tslint:disable:no-new-decorators
import {customElement} from 'lit/decorators';
import {html} from 'lit';
Expand All @@ -23,17 +24,18 @@ import {computed, observable} from 'mobx';

import {app} from './app';
import {LitModule} from './lit_module';
import {ModelInfoMap, Spec} from '../lib/types';
import {SortableTableEntry} from '../elements/table';
import {IndexedInput, ModelInfoMap, Spec} from '../lib/types';
import {handleEnterKey} from '../lib/utils';
import {GroupService, NumericFeatureBins} from '../services/group_service';
import {SliceService} from '../services/services';
import {STARRED_SLICE_NAME} from '../services/slice_service';
import {FacetsChange} from '../core/faceting_control';


import {styles as sharedStyles} from '../lib/shared_styles.css';
import {styles} from './slice_module.css';


/**
* The slice controls module
*/
Expand Down Expand Up @@ -165,6 +167,20 @@ export class SliceModule extends LitModule {
// clang-format on
}

/** Returns data within this slice for exporting. */
getArrayData(sliceName: string): SortableTableEntry[][] {
const columnStrings = this.appState.currentInputDataKeys;
const rowData = (row : IndexedInput) => {
// Add data index.
return [this.appState.getIndexById(row.id)].concat(
columnStrings.map(c => row.data[c]));
};

const sliceData = this.sliceService.getSliceDataByName(sliceName);
return sliceData.map(d => rowData(d));
}


renderSliceRow(sliceName: string) {
const selectedSliceName = this.sliceService.selectedSliceName;
const itemClass = classMap(
Expand Down Expand Up @@ -195,10 +211,11 @@ export class SliceModule extends LitModule {
this.sliceService.deleteNamedSlice(sliceName);
};

const shouldDisableIcons = numDatapoints <= 0;
const clearIconClass = classMap({
'icon-button': true,
'mdi-outlined': true,
'disabled': numDatapoints <= 0
'disabled': shouldDisableIcons
});
const clearClicked = (e: Event) => {
e.stopPropagation(); /* don't select row */
Expand All @@ -208,23 +225,33 @@ export class SliceModule extends LitModule {

// clang-format off
return html`
<div class=${itemClass} @click=${itemClicked}>
<span class='slice-name'>${sliceName}</span>
<span class="number-label">
${numDatapoints} ${numDatapoints === 1 ? 'datapoint' : 'datapoints'}
<div class=${itemClass}>
<span class='slice-name' @click=${itemClicked}>${sliceName}</span>
<span class="right-action-menu">
<span class="number-label" @click=${itemClicked}>
${numDatapoints} ${numDatapoints === 1 ? 'datapoint' : 'datapoints'}
</span>
<mwc-icon class=${appendIconClass} @click=${appendClicked}
title="Add selected to this slice">
add_circle_outline
</mwc-icon>
${sliceName === STARRED_SLICE_NAME ?
html`<mwc-icon class=${clearIconClass} @click=${clearClicked}
title="Reset this slice">
clear
</mwc-icon>` :
html`<mwc-icon class='icon-button' @click=${deleteClicked}
title="Delete this slice">
html`<mwc-icon class='icon-button selector-item-icon-button'
@click=${deleteClicked} title="Delete this slice">
delete_outline
</mwc-icon>`}
<export-controls ?disabled=${shouldDisableIcons}
.data=${this.getArrayData(sliceName)}
.downloadFilename="${
this.appState.currentDataset}-${sliceName}.csv"
.columnNames=${this.appState.currentInputDataKeys}>
</export-controls>
</span>
</div>`;
// clang-format on
Expand Down
28 changes: 28 additions & 0 deletions lit_nlp/client/elements/export_controls.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#export-controls {
display: inline-block;
}

.above {
--popup-bottom: 28px;
}

.icon-button {
font-size: 18px;
vertical-align: middle;
margin-left: 3px;
margin-bottom: 5px;
}

.download-button {
margin: 4px 0 0 0;
width: 100%;
}

popup-container.download-popup {
display: inline-block;
--popup-right: 0;
}

.download-popup-controls {
align-items: center;
}
142 changes: 142 additions & 0 deletions lit_nlp/client/elements/export_controls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* @fileoverview A resusable component for downloading data in LIT
* @license
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// tslint:disable:no-new-decorators
import './popup_container';

import {html} from 'lit';
import {classMap} from 'lit/directives/class-map';
import {customElement, property} from 'lit/decorators';
import {observable} from 'mobx';
import * as papa from 'papaparse';

import {ReactiveElement} from '../lib/elements';
import {styles as sharedStyles} from '../lib/shared_styles.css';

import {styles} from './export_controls.css';
import {PopupContainer} from './popup_container';
import {SortableTableEntry} from './table';

/**
* An element that handles logic for downloading data.
*/
@customElement('export-controls')
export class ExportControls extends ReactiveElement {
static override get styles() {
return [sharedStyles, styles];
}

/** The default file download name. */
@observable @property({type: String}) downloadFilename: string = 'data.csv';
/** A list of rows of data to download. */
@property({type: Object}) data: SortableTableEntry[][] = [];
/** Column names. */
@observable @property({type: Object}) columnNames: string[] = [];
/** Download popup position defaults to below the download icon. */
@property({type: String}) popupPosition: string = 'below';
/** If true, disable controls. */
@property({type: Boolean}) disabled = false;

getCSVContent(): string {
return papa.unparse(
{fields: this.columnNames, data: this.data},
{newline: '\r\n'});
}

getPopupClasses() {
return classMap({
'hidden': this.disabled,
'download-popup': true,
'above': this.popupPosition === 'above'
});
}

/**
* Renders the copy and download buttons to download data.
*/
override render() {
const copyCSV = () => {
const csvContent = this.getCSVContent();
navigator.clipboard.writeText(csvContent);
};

const downloadCSV = () => {
const csvContent = this.getCSVContent();
const blob = new Blob([csvContent], {type: 'text/csv'});
const a = window.document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = this.downloadFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
const controls: PopupContainer =
this.shadowRoot!.querySelector('popup-container.download-popup')!;
controls.expanded = false;
};

const updateFilename = (e: Event) => {
// tslint:disable-next-line:no-any
this.downloadFilename = (e as any).target.value as string;
};

function onEnter(e: KeyboardEvent) {
if (e.key === 'Enter') downloadCSV();
}

const iconClass = classMap({
'icon-button': true,
'mdi-outlined': true,
'disabled': this.disabled,
});

// clang-format off
return html`
<div id='export-controls'>
<mwc-icon class=${iconClass}
title="Copy ${this.data.length} rows as CSV"
@click=${copyCSV}>
file_copy
</mwc-icon>
<popup-container class='${this.getPopupClasses()}'>
<mwc-icon class=${iconClass} slot='toggle-anchor'
title="Download ${this.data.length} rows as CSV">
file_download
</mwc-icon>
<div class='download-popup-controls'>
<label for="filename">Filename</label>
<input type="text" name="filename" value=${this.downloadFilename}
@input=${updateFilename} @keydown=${onEnter}>
<button class='download-button filled-button nowrap'
@click=${downloadCSV}
?disabled=${!this.downloadFilename}>
Download ${this.data.length} rows
</button>
</div>
</popup-container>
</div>`;
// clang-format on
}
}

declare global {
interface HTMLElementTagNameMap {
'export-controls': ExportControls;
}
}
11 changes: 0 additions & 11 deletions lit_nlp/client/elements/table.css
Original file line number Diff line number Diff line change
Expand Up @@ -235,17 +235,6 @@ tbody td {
flex: 1;
}

popup-container.download-popup {
--popup-bottom: 28px;
--popup-right: 0;
}

.download-popup-controls {
display: flex;
align-items: center;
column-gap: 4px;
}

.current-page-num {
display: inline-block;
text-align: center;
Expand Down

0 comments on commit 57fac3a

Please sign in to comment.