@webwriter/neural-network
Version:
Deep learning visualization for feed-forward networks with custom datasets, training and prediction.
610 lines (578 loc) • 21.5 kB
text/typescript
import { LitElementWw } from '@webwriter/lit'
import { CSSResult, TemplateResult, css, html, nothing } from 'lit'
import { customElement, property, query, state } from 'lit/decorators.js'
import { consume } from '@lit/context'
import { globalStyles } from '@/global_styles'
import type { DataSet } from '@/types/data_set'
import { availableDataSetsContext } from '@/contexts/available_data_sets_context'
import {
SlChangeEvent,
SlDialog,
SlButton,
SlInput,
SlTextarea,
SlTooltip,
SlRadioGroup,
SlRadioButton,
} from '@shoelace-style/shoelace'
import { serialize } from '@shoelace-style/shoelace/dist/utilities/form.js'
import { AlertUtils } from '@/utils/alert_utils'
import IconQuestionCircle from 'bootstrap-icons/icons/question-circle.svg'
import IconArrowLeftCircle from 'bootstrap-icons/icons/arrow-left-circle.svg'
import IconArrowRightCircle from 'bootstrap-icons/icons/arrow-right-circle.svg'
import { CCard } from '../reusables/c-card'
import { msg } from '@lit/localize'
export class CreateDataSetDialog extends LitElementWw {
static scopedElements = {
'sl-dialog': SlDialog,
'sl-textarea': SlTextarea,
'sl-tooltip': SlTooltip,
'sl-input': SlInput,
'c-card': CCard,
'sl-button': SlButton,
'sl-radio-group': SlRadioGroup,
'sl-radio-button': SlRadioButton,
}
({ context: availableDataSetsContext, subscribe: true })
accessor availableDataSets: DataSet[]
private emptyConfig: DataSet = {
name: '',
description: '',
type: 'regression',
featureDescs: [{ key: '', description: '' }],
labelDesc: {
key: '',
description: '',
classes: [
{ id: 0, description: '' },
{ id: 1, description: '' },
],
},
data: [],
}
()
accessor config: DataSet = <DataSet>(
JSON.parse(JSON.stringify(this.emptyConfig))
)
()
accessor step: number = 1
('sl-dialog')
accessor _dialog: SlDialog
('.dialog-form')
accessor _dialogForm: HTMLFormElement
// LIFECYCLE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
async connectedCallback(): Promise<void> {
super.connectedCallback()
await this.updateComplete
}
// METHODS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
async show() {
await this._dialog.show()
console.log(this._dialogForm)
console.log(serialize(this._dialogForm).data)
if (this.step != 1 || serialize(this._dialogForm).data) {
AlertUtils.spawn({
message: msg(
'The progress you made in creating your own data set was successfully restored!',
),
variant: 'success',
icon: 'check-circle',
})
}
}
nextStep(e: MouseEvent) {
const form: any = e.target
if (!form.checkValidity()) {
form.reportValidity()
return
}
if (this.step == 5) {
void this.validateAndCreate()
} else {
this.step++
}
e.preventDefault()
}
// step 3 (configuring the features (keys/descriptions))
addFeature() {
this.config.featureDescs.push({ key: '', description: '' })
this.config = { ...this.config }
}
removeFeature() {
this.config.featureDescs.pop()
this.config = { ...this.config }
}
// step 4 (configuring the label (key/description) and its classes
// (keys/descriptions))
addLabelClass() {
this.config.labelDesc.classes.push({ id: undefined, description: '' })
this.config = { ...this.config }
}
removeLabelClass() {
this.config.labelDesc.classes.pop()
this.config = { ...this.config }
}
// step 5 (paste data set form)
async validateAndCreate() {
// get data
const data: string = <string>serialize(this._dialogForm).data
if (
this.availableDataSets.find((dataSet) => dataSet.name == this.config.name)
) {
AlertUtils.spawn({
message: msg('A data set with the same name already exists!'),
variant: 'danger',
icon: 'x-circle',
})
return
}
// additional validation
const pattern = new RegExp(
`^(\\s*(?:(?:[-+]?\\d+(?:\\.\\d*)?)|(?:\\d*\\.\\d+))(?:\\s*,\\s*(?:(?:[-+]?\\d+(?:\\.\\d*)?)|(?:\\d*\\.\\d+))){${this.config.featureDescs.length}}\\s*)+$`,
)
const result = pattern.test(data)
if (!result) {
AlertUtils.spawn({
message: msg(
'The provided data does not match the required format! Please check again',
),
variant: 'danger',
icon: 'x-circle',
})
return
}
// checks all passed, we can proceed parsing data
const lines = data.split('\n')
for (const line of lines) {
// remove spaces in the beginning and end with trim and use split to
// convert into array of the values
const values: string[] = line.trim().split(',')
console.log(values)
// parse feature and label data (config.featureDescs.length * features and
// one label)
const features: number[] = []
let index = 0
for (const _feature of this.config.featureDescs) {
features.push(parseInt(values[index].trim()))
index += 1
}
const label: number = parseInt(values[index])
// add parsed data from this line to the data array
this.config.data.push({
features,
label,
})
}
const dataSet: DataSet = <DataSet>JSON.parse(JSON.stringify(this.config))
this.dispatchEvent(
new CustomEvent<DataSet>('add-data-set', {
detail: dataSet,
bubbles: true,
composed: true,
}),
)
this.dispatchEvent(
new CustomEvent<DataSet>('select-data-set', {
detail: dataSet,
bubbles: true,
composed: true,
}),
)
this.config = <DataSet>JSON.parse(JSON.stringify(this.emptyConfig))
this.step = 1
this._dialogForm.reset()
AlertUtils.spawn({
message: msg(
'A new data set was successfully created and automatically selected!',
),
variant: 'success',
icon: 'check-circle',
})
await this._dialog.hide()
}
// STYLES - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static styles: CSSResult[] = [
globalStyles,
css`
sl-dialog::part(base) {
position: absolute;
height: 100%;
width: 100%;
}
sl-dialog::part(overlay) {
position: absolute;
width: 100%;
}
sl-dialog::part(body) {
text-align: center;
}
.form-main {
margin: 15px 0;
}
p,
sl-input,
sl-textarea,
sl-button,
c-card {
margin-bottom: 10px;
}
form *[label] {
text-align: left;
}
.step-chooser {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
margin-bottom: 20px;
}
`,
]
// RENDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
render(): TemplateResult<1> {
return html`
<sl-dialog label=${msg('Create a new data set')}>
<div class="step-chooser">
<sl-button
circle
variant="${this.step == 1 ? 'primary' : 'default'}"
@click="${(_e: MouseEvent) => (this.step = 1)}"
>
1
</sl-button>
<sl-button
circle
variant="${this.step == 2 ? 'primary' : 'default'}"
@click="${(_e: MouseEvent) => (this.step = 2)}"
.disabled="${this.step < 2}"
>
2
</sl-button>
<sl-button
circle
variant="${this.step == 3 ? 'primary' : 'default'}"
@click="${(_e: MouseEvent) => (this.step = 3)}"
.disabled="${this.step < 3}"
>
3
</sl-button>
<sl-button
circle
variant="${this.step == 4 ? 'primary' : 'default'}"
@click="${(_e: MouseEvent) => (this.step = 4)}"
.disabled="${this.step < 4}"
>
4
</sl-button>
<sl-button
circle
variant="${this.step == 5 ? 'primary' : 'default'}"
@click="${(_e: MouseEvent) => (this.step = 5)}"
.disabled="${this.step < 5}"
>
5
</sl-button>
</div>
<form
class="dialog-form"
@submit=${(e) => {
this.nextStep(e)
}}
>
<div class="form-main">
<div ?hidden=${this.step !== 1} ?inert=${this.step !== 1}>
<h1>${msg('Welcome')}</h1>
<p>
${msg(
'This tour will guide you through creating your own data set in a few simple steps. Everything is stored automatically, so you can close this modal at any time and resume.',
)}
</p>
</div>
<div ?hidden=${this.step !== 2} ?inert=${this.step !== 2}>
<h1>${msg('General info about the data set')}</h1>
<p>
${msg(
'Choose a short but meaningful name for your data set and write a description.',
)}
</p>
<sl-input
name="name"
label=${msg('Name')}
placeholder=${msg('Boston House Pricing')}
?required=${this.step == 2}
minlength=${this.step == 2 ? 1 : nothing}
@sl-change=${(e: SlChangeEvent) => {
this.config.name = (<HTMLInputElement>e.target).value
this.config = { ...this.config }
}}
></sl-input>
<sl-textarea
rows="4"
name="description"
label=${msg('Description')}
placeholder=${msg(
'The Boston House Price data set involves the prediction of a house price in thousands of dollars given details of the house and its neighborhood.',
)}
?required=${this.step == 2}
minlength=${this.step == 2 ? 1 : nothing}
@sl-change=${(e: SlChangeEvent) => {
this.config.description = (<HTMLInputElement>e.target).value
this.config = { ...this.config }
}}
></sl-textarea>
<sl-tooltip>
<div slot="content">
<p>
${msg(
"Choose 'regression' if you want to predict continous values like house or gas prices",
)}
</p>
<p>
${msg(
"Choose 'classification' if you want information about the affiliation of the feature(s) to a class (e.g. what animal can be seen in this image? A dog, cat or horse?)",
)}
</p>
</div>
<p>
${msg('Choose a type')}
<sl-icon src=${IconQuestionCircle}></sl-icon>
</p>
</sl-tooltip>
<sl-radio-group
value="${this.config.type}"
@sl-change="${(e: SlChangeEvent) => {
this.config.type = <'regression' | 'classification'>(
(<HTMLInputElement>e.target).value
)
this.config = { ...this.config }
}}"
>
<sl-radio-button pill value="regression"
>${msg('Regression')}</sl-radio-button
>
<sl-radio-button pill value="classification"
>${msg('Classification')}</sl-radio-button
>
</sl-radio-group>
</div>
<div ?hidden=${this.step !== 3} ?inert=${this.step !== 3}>
<h1>${msg('Features')}</h1>
<p>
${msg(
'Which data will be put into the neural network? Create arbitrary many features!',
)}
</p>
${this.config.featureDescs.map(
(featureDesc, index) => html`
<c-card>
<div slot="content">
<sl-input
value=${featureDesc.key}
label=${msg('Key')}
placeholder="DIS"
help-text=${msg('1-6 capital letters')}
?required=${this.step == 3}
maxlength=${this.step == 3 ? 6 : nothing}
pattern=${this.step == 3 ? '[A-Z]+' : nothing}
@sl-change=${(e: SlChangeEvent) => {
this.config.featureDescs[index].key = (
e.target as HTMLInputElement
).value
this.config = { ...this.config }
}}
></sl-input>
<sl-textarea
rows="2"
value=${featureDesc.description}
label=${msg('Description')}
placeholder=${msg(
'Weighted distances to five Boston employment centers',
)}
?required=${this.step == 3}
minlength=${this.step == 3 ? 1 : nothing}
@sl-change=${(e: SlChangeEvent) => {
this.config.featureDescs[index].description = (
e.target as HTMLInputElement
).value
this.config = { ...this.config }
}}
></sl-textarea>
</div>
</c-card>
`,
)}
${this.config.featureDescs.length >= 2
? html`
<sl-button
@click="${(_e: MouseEvent) => this.removeFeature()}"
>${msg('Remove feature')}</sl-button
>
`
: html``}
<sl-button @click="${(_e: MouseEvent) => this.addFeature()}"
>${msg('Add feature')}</sl-button
>
</div>
<div ?hidden=${this.step !== 4} ?inert=${this.step !== 4}>
<h1>${msg('Label')}</h1>
<p>${msg('What shall be the output of the network?')}</p>
<sl-input
value=${this.config.labelDesc.key}
label=${msg('Key')}
placeholder="MEDV"
help-text=${msg('1-6 capital letters')}
?required=${this.step == 4}
maxlength=${this.step == 4 ? 6 : nothing}
pattern=${this.step == 4 ? '[A-Z]+' : nothing}
@sl-change=${(e: SlChangeEvent) => {
this.config.labelDesc.key = (<HTMLInputElement>e.target).value
this.config = { ...this.config }
}}
></sl-input>
<sl-textarea
rows="2"
value=${this.config.labelDesc.description}
label=${msg('Description')}
placeholder=${msg(
'Median value of owner-occupied homes in $1000s',
)}
?required=${this.step == 4}
minlength=${this.step == 4 ? 1 : nothing}
@sl-change=${(e: SlChangeEvent) => {
this.config.labelDesc.description = (<HTMLInputElement>(
e.target
)).value
this.config = { ...this.config }
}}
></sl-textarea>
${this.config.type == 'classification'
? html`
<h3>Classes</h3>
${this.config.labelDesc.classes?.map(
(clazz, index) => html`
<c-card>
<div slot="content">
<sl-input
type="number"
value=${clazz.id}
label=${msg('Key')}
placeholder="0"
help-text=${msg('an integer')}
?required=${this.step == 4}
maxlength=${this.step == 4 ? 6 : nothing}
pattern=${this.step == 4 ? '[A-Z]+' : nothing}
@sl-change=${(e: SlChangeEvent) => {
this.config.labelDesc.classes[index].id =
parseInt((e.target as HTMLInputElement).value)
this.config = { ...this.config }
}}
></sl-input>
<sl-textarea
rows="2"
value=${clazz.description}
label=${msg('Description')}
placeholder=${msg(
'Animal was detected as a horse',
)}
?required=${this.step == 4}
minlength=${this.step == 4 ? 1 : nothing}
@sl-change=${(e: SlChangeEvent) => {
this.config.labelDesc.classes[
index
].description = (<HTMLInputElement>(
e.target
)).value
this.config = { ...this.config }
}}
></sl-textarea>
</div>
</c-card>
`,
)}
${this.config.labelDesc.classes.length >= 3
? html`
<sl-button
@click="${(_e: MouseEvent) =>
this.removeLabelClass()}"
>${msg('Remove class')}</sl-button
>
`
: html``}
<sl-button
@click="${(_e: MouseEvent) => this.addLabelClass()}"
>${msg('Add class')}</sl-button
>
`
: html``}
</div>
<div ?hidden=${this.step !== 5} ?inert=${this.step !== 5}>
<h1>${msg('You are nearly done')}</h1>
<p>${msg('Now add your data in the following format')}*:</p>
<div
class="tag-group"
style="justify-content: center !important;"
>
${this.config.featureDescs.map(
(featureDesc) => html`
<c-data-info
type="feature"
.dataDesc="${featureDesc}"
.dataSet="${this.config}"
class="clickable"
></c-data-info
>,
`,
)}
<c-data-info
type="label"
.dataDesc="${this.config.labelDesc}"
.dataSet="${this.config}"
class="clickable"
></c-data-info>
</div>
<sl-textarea
id="dataTextarea"
rows="10"
name="data"
help-text="*${msg(
'Each row needs to represent one set containing the features and the label. Seperate items with a comma (spaces before and after the comma are okay). The single last item always represents the label while the items before it represent the features. Make sure to use only dots and no commas for floating point values. If you have data in CSV format, you can just paste it here but make sure to remove any comments.',
)}"
?required=${this.step == 5}
></sl-textarea>
</div>
</div>
<div class="button-group form-footer">
${this.step != 1
? html`
<sl-button
id="abortButton"
@click="${(_e: MouseEvent) => this.step--}"
>
<sl-icon slot="prefix" src=${IconArrowLeftCircle}></sl-icon>
${msg('Previous')}
</sl-button>
`
: html``}
<sl-button variant="primary" type="submit" id="nextStepButton">
${this.step < 5
? html`
${msg('Next')}
<sl-icon
slot="suffix"
src=${IconArrowRightCircle}
></sl-icon>
`
: html`
${msg('Validate and create')}
<sl-icon
slot="suffix"
src=${IconArrowRightCircle}
></sl-icon>
`}
</sl-button>
</div>
</form>
</sl-dialog>
`
}
}