diff --git a/conda/js/src/components/ParameterCreate.jsx b/conda/js/src/components/ParameterCreate.jsx index 378557d5f07234c761a36cc2350c2344c48f5afc..7ae95a89cd5e91260a13e6b55b97e013f34aea1a 100644 --- a/conda/js/src/components/ParameterCreate.jsx +++ b/conda/js/src/components/ParameterCreate.jsx @@ -51,6 +51,19 @@ export default class ParameterCreate extends React.Component<Props, State> { const params = this.props.params; const updateParameter = this.props.updateParameter; + let new_param = false; + if (param.type == 'bool') + { + if(typeof(param.default) == 'string') + { + new_param = true; + } + else + { + new_param = false; + } + } + return ( <React.Fragment> <TypedField @@ -332,6 +345,12 @@ export default class ParameterCreate extends React.Component<Props, State> { <Input name={`default${ name }`} type='radio' + checked={param.default && !new_param} + value={true} + onChange={(e) => updateParameter(name, { + ...param, + default: JSON.parse(e.target.value) + })} /> True </Label> @@ -341,6 +360,12 @@ export default class ParameterCreate extends React.Component<Props, State> { <Input name={`default${ name }`} type='radio' + checked={!param.default && !new_param} + value={false} + onChange={(e) => updateParameter(name, { + ...param, + default: JSON.parse(e.target.value) + })} /> False </Label> diff --git a/conda/js/src/components/ParameterCreate.spec.jsx b/conda/js/src/components/ParameterCreate.spec.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8d2d6e251c5717db53bf152e9e63fdb328216db1 --- /dev/null +++ b/conda/js/src/components/ParameterCreate.spec.jsx @@ -0,0 +1,69 @@ +//// @flow +//import React from 'react'; +//import { expect } from 'chai'; +//import { mount } from 'enzyme'; +//import sinon from 'sinon'; +//import { spies } from '@test'; +// +//import C from './ParameterCreate.jsx'; +// +//describe.only('<ParameterCreate />', () => { +// let wrapper; +// const name = 'parameter'; +// // shortcut for a func to update just the parameter obj, not the name +// const _updateParameter = (name, p, oldName) => { +// wrapper.setProps({ +// param: p, +// }); +// }; +// const params = []; +// +// afterEach(() => { +// if(wrapper && wrapper.unmount) +// wrapper.unmount(); +// }); +// +// it(`saves the default value of a boolean parameter`, () => { +// let param = { +// type: '', +// default: '', +// description: '', +// }; +// const updateParameter = sinon.spy(_updateParameter); +// +// wrapper = mount( +// <C +// name={name} +// param={param} +// params={params} +// updateParameter={updateParameter} +// /> +// ); +// +// // sanity checks +// expect(wrapper.find('TypedField')).to.have.prop('name', name); +// +// wrapper.find('Input.custom-select').prop('onChange')( { target: { value: 'bool' }}); +// wrapper.update(); +// console.log(wrapper.props().param); +// expect(wrapper.props().param).to.deep.equal( +// { +// type: 'bool', +// default: '', +// description: '', +// } +// ); +// +// // wrapper.find('input[type="radio"]').at(0).simulate('change', { target: { checked: 'true' }});; +// wrapper.find('input[type="radio"]').at(0).simulate('change', { target: { value: 'true' }});; +// wrapper.update(); +// expect(updateParameter.args[0]).to.deep.equal([ +// name, +// { +// type: 'bool', +// default: 'true', +// description: '', +// } +// ]); +// }); +//}); diff --git a/conda/js/src/components/plotter/PlotterEditor.jsx b/conda/js/src/components/plotter/PlotterEditor.jsx index 910b35865af17caee52286934452cf7f1f339ec1..9095ced7458b68202d338e36c413b1979bfb3ce5 100644 --- a/conda/js/src/components/plotter/PlotterEditor.jsx +++ b/conda/js/src/components/plotter/PlotterEditor.jsx @@ -86,6 +86,81 @@ export class PlotterEditor extends React.Component<Props, State> { this.setContents({ parameters: params }); } + // helper to change a value in the "contents" subobject of a plotter + // (this is where the vast majority of change happens) + changeContentsVal = (field: string, val: any) => { + const newCache = { + ...this.props.data, + contents: { + ...this.props.data.contents, + [field]: val + } + }; + + this.props.updateFunc(newCache); + } + + + renderLibraries = () => ( + <TabPane tabId='2'> + <Row> + <Col sm='12'> + { + // loop through all the used libs + (Object.entries(this.props.data.contents.uses): [string, any][]) + .map(([name, lib], i, lEntries) => ( + <TypedField + key={i} + name={name} + type={lib} + types={this.props.libraries.map(l => l.name)} + existingFields={lEntries.map(([n, v]) => n)} + nameUpdateFunc={str => { + // update the alias + this.changeContentsVal('uses', + changeObjFieldName( + this.props.data.contents.uses, + name, + str + ) + ); + }} + typeUpdateFunc={str => { + // update the chosen library for the alias + const libs = { + ...this.props.data.contents.uses, + [name]: str + }; + this.changeContentsVal('uses', libs); + }} + deleteFunc={() => { + const libs = { ...this.props.data.contents.uses }; + delete libs[name]; + this.changeContentsVal('uses', libs); + }} + /> + )) + } + </Col> + </Row> + <Button outline block + id='newLibraryBtn' + onClick={() => { + const newKey = generateNewKey('library', Object.keys(this.props.data.contents.uses)); + this.changeContentsVal('uses', + { + ...this.props.data.contents.uses, + [newKey]: '' + } + ); + }} + > + New Library + </Button> + </TabPane> + ); + + render = () => { return ( <div> @@ -131,7 +206,11 @@ export class PlotterEditor extends React.Component<Props, State> { } </Input> </FormGroup> - <FormGroup> + <FormGroup id='usedLibs'> + <h5>Libraries</h5> + { this.renderLibraries() } + </FormGroup> + <FormGroup id='paramsForm'> <h5>Parameters</h5> { (Object.entries(this.props.data.contents.parameters): [string, any][]).map(([name, param], i, params) => ( @@ -178,6 +257,12 @@ const mapStateToProps = (state, ownProps) => { plotDfNames: Selectors.dataformatGet(state).map(df => df.name).filter(df => df.startsWith('plot/')), data: plotters[ownProps.index] || getValidObj(), }; + + if(obj.data.contents.dataformat === "") + { + obj.plotDfNames.unshift("-- select an option --") + }; + return obj; }; diff --git a/conda/js/src/components/plotter/PlotterEditor.spec.jsx b/conda/js/src/components/plotter/PlotterEditor.spec.jsx index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5aa3bd3b9649c8cdeb218805d826655059ede4b6 100644 --- a/conda/js/src/components/plotter/PlotterEditor.spec.jsx +++ b/conda/js/src/components/plotter/PlotterEditor.spec.jsx @@ -0,0 +1,236 @@ +// @flow +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import sinon from 'sinon'; +import { spies } from '@test'; + +import { PlotterEditor as C } from '.'; +import { getValidPlotterObj as getValidObj } from '@helpers/beat'; + +import testPlotters from '@test/test_plotters.json'; +import testDfs from '@test/test_dfs.json'; +import testLibs from '@test/test_libs.json'; + +const libs = []; + +describe('<PlotterEditor />', () => { + let wrapper; + + afterEach(() => { + if(wrapper && wrapper.unmount) + wrapper.unmount(); + }); + + describe('accepts', () => { + const plots = [ + { + name: 'test/myplot/1', + contents: { + description: 'A basic plotter as a sanity test for the PlotterEditor', + language: 'python', + parameters: { + 'parameter_1': { + type: 'string', + default: 'default', + description: 'description', + } + }, + dataformat: "plot/bar/1", + uses: { + 'library': 'plot/baselib/1', + } + } + } + ].concat(testPlotters.map(a => getValidObj(a))); + + plots.forEach(function(plot){ + const saveFunc = () => {}; + const updateFunc = () => {}; + const dfs = testDfs.filter(d => d.name.startsWith('plot')).map(testDfs => testDfs.name); + it(`${ plot.name }`, () => { + wrapper = mount( + <C + data={getValidObj(plot)} + plotters={plots} + dataformats={plot.contents.dataformat} + plotDfNames={dfs} + libraries={libs} + saveFunc={saveFunc} + updateFunc={updateFunc} + /> + ); + expect(wrapper).to.have.props( + ['data', 'plotters', 'dataformats', 'libraries', 'saveFunc', 'updateFunc'] + ) + }); + }); + }); + + describe('creates', () => { + it('test/createplot/1', () => { + const saveFunc = sinon.spy(); + const _updateFunc = (obj) => { + wrapper.setProps && wrapper.setProps({ data: obj }); + }; + const updateFunc = sinon.spy(_updateFunc); + const plotName = 'test/createplot/1'; + const dfs = testDfs.filter(d => d.name.startsWith('plot')).map(testDfs => testDfs.name); + wrapper = mount( + <C + data={getValidObj({name: plotName, contents: {}})} + plotters={[]} + dataformats={testDfs} + plotDfNames={dfs} + libraries={testLibs} + saveFunc={saveFunc} + updateFunc={updateFunc} + /> + ); + + expect(wrapper).to.have.props( + ['data', 'plotters', 'dataformats', 'plotDfNames', 'libraries', 'saveFunc', 'updateFunc'] + ); + + expect(wrapper.props().data).to.have.property('name', plotName); + + expect(wrapper.props().data.contents).to.have.property('dataformat', ''); + expect(wrapper.props().data.contents).to.have.property('dataformat').with.lengthOf(0); + expect(wrapper.props().data.contents).to.have.property('language').with.lengthOf(6); + expect(wrapper.props().data.contents).to.have.property('language', 'python'); + wrapper.find('#plDf select').simulate('change', { target: { value: 'user/anotherdataformat/1'}}); + expect(updateFunc.callCount).to.equal(1); + + expect(wrapper.props().data.contents).to.have.property('dataformat', 'user/anotherdataformat/1'); + expect(wrapper.props().data.contents).to.have.property('language', 'python'); + + expect(wrapper.props().data.contents).to.have.property('description').with.lengthOf(0); + expect(wrapper.props().data.contents).to.have.property('language').with.lengthOf(6); + expect(wrapper.props().data.contents).to.have.property('dataformat').with.lengthOf(24); + expect(wrapper.props().data.contents).to.have.property('parameters').that.deep.equals({}); + expect(wrapper.props().data.contents).to.have.property('uses').that.deep.equals({}); + expect(wrapper.props().data.contents).to.deep.equal({ + 'description': '', + 'uses': {}, + 'parameters': {}, + 'language': 'python', + 'dataformat': 'user/anotherdataformat/1' + }); + + }); + + it('test/plot_add_lib/1', () => { + const saveFunc = sinon.spy(); + const _updateFunc = (obj) => { + wrapper.setProps && wrapper.setProps({ data: obj }); + }; + const updateFunc = sinon.spy(_updateFunc); + const plotName = 'test/plot_other/1'; + const dfs = testDfs.filter(d => d.name.startsWith('plot')).map(testDfs => testDfs.name); + wrapper = mount( + <C + data={getValidObj({name: plotName, contents: {}})} + plotters={[]} + dataformats={testDfs} + plotDfNames={dfs} + libraries={testLibs} + saveFunc={saveFunc} + updateFunc={updateFunc} + /> + ); + + expect(wrapper).to.have.props( + ['data', 'plotters', 'dataformats', 'plotDfNames', 'libraries', 'saveFunc', 'updateFunc'] + ); + + expect(wrapper.props().data).to.have.property('name', plotName); + + expect(wrapper.props().data.contents).to.have.property('dataformat', ''); + expect(wrapper.props().data.contents).to.have.property('dataformat').with.lengthOf(0); + expect(wrapper.props().data.contents).to.have.property('language').with.lengthOf(6); + expect(wrapper.props().data.contents).to.have.property('language', 'python'); + expect(wrapper.props().data.contents).to.have.property('parameters').that.deep.equals({}); + expect(wrapper.props().data.contents).to.have.property('uses').that.deep.equals({}); + + wrapper.find('button#newLibraryBtn').simulate('click'); + wrapper.find('#usedLibs CacheInput[type="text"]').prop('onChange')({ target: { value: 'lib'}}); + expect(updateFunc.callCount).to.equal(2); + + wrapper.find('#usedLibs select').simulate('change', { target: { value: 'user/anotherlib/1'}}); + expect(updateFunc.callCount).to.equal(3); + + expect(wrapper.props().data.contents).to.deep.equal({ + 'description': '', + 'uses': { + lib: 'user/anotherlib/1' + }, + 'language': 'python', + 'parameters': {}, + 'dataformat': '' + }); + }); + + it('test/plot_add_param/1', () => { + const saveFunc = sinon.spy(); + const _updateFunc = (obj) => { + wrapper.setProps && wrapper.setProps({ data: obj }); + }; + const updateFunc = sinon.spy(_updateFunc); + const plotName = 'test/plot_param/1'; + const dfs = testDfs.filter(d => d.name.startsWith('plot')).map(testDfs => testDfs.name); + wrapper = mount( + <C + data={getValidObj({name: plotName, contents: {}})} + plotters={[]} + dataformats={testDfs} + plotDfNames={dfs} + libraries={testLibs} + saveFunc={saveFunc} + updateFunc={updateFunc} + /> + ); + + expect(wrapper).to.have.props( + ['data', 'plotters', 'dataformats', 'plotDfNames', 'libraries', 'saveFunc', 'updateFunc'] + ); + + expect(wrapper.props().data).to.have.property('name', plotName); + + expect(wrapper.props().data.contents).to.have.property('dataformat', ''); + expect(wrapper.props().data.contents).to.have.property('dataformat').with.lengthOf(0); + expect(wrapper.props().data.contents).to.have.property('language').with.lengthOf(6); + expect(wrapper.props().data.contents).to.have.property('language', 'python'); + expect(wrapper.props().data.contents).to.have.property('parameters').that.deep.equals({}); + expect(wrapper.props().data.contents).to.have.property('uses').that.deep.equals({}); + wrapper.find('button#newParameterBtn').simulate('click'); + expect(updateFunc.callCount).to.equal(1); + + wrapper.find('CacheInput[placeholder="Parameter name..."]').prop('onChange')( { target: { value: 'offset' }}); + expect(updateFunc.callCount).to.equal(2); + wrapper.find('#paramsForm select').prop('onChange')( { target: { value: 'int32' }}); + expect(updateFunc.callCount).to.equal(3); + wrapper.find('input[placeholder="Default"]').simulate('change', { target: { value: '1' }}); + expect(updateFunc.callCount).to.equal(4); + + expect(wrapper.props().data.contents.parameters).to.have.deep.property('offset', { + 'default': '1', + 'type': 'int32', + 'description': '', + }); + + expect(wrapper.props().data.contents).to.deep.equal({ + 'description': '', + 'uses': {}, + 'language': 'python', + 'parameters': { + 'offset': { + 'default': '1', + 'type': 'int32', + description: '', + } + }, + 'dataformat': '' + }); + }); + }); +}); diff --git a/conda/js/src/components/plotterparameter/PlotterParameterEditor.jsx b/conda/js/src/components/plotterparameter/PlotterParameterEditor.jsx index e93500cd3599cff42f855de33db3d0616f675875..9c90b2ffa4a41704514325ccd068cd3273362101 100644 --- a/conda/js/src/components/plotterparameter/PlotterParameterEditor.jsx +++ b/conda/js/src/components/plotterparameter/PlotterParameterEditor.jsx @@ -172,6 +172,13 @@ const mapStateToProps = (state, ownProps) => { plotters: Selectors.plotterGet(state), data: params[ownProps.index] || getValidObj(), }; + + if(!obj.data.contents.hasOwnProperty('plotter') || + obj.data.contents.plotters === "") + { + obj.plotters.unshift({name: "-- select an option --"}) + }; + return obj; }; diff --git a/conda/js/src/helpers/schema/plotterparameter.json b/conda/js/src/helpers/schema/plotterparameter.json index a6c1f3910937186d362cd6999e2b3fd8bfae8863..59760635cd341736e004d2856265cacf5c7abc18 100644 --- a/conda/js/src/helpers/schema/plotterparameter.json +++ b/conda/js/src/helpers/schema/plotterparameter.json @@ -16,5 +16,6 @@ "data": { "$ref": "experiment#/definitions/parameter_set" } - } + }, + "required": ["plotter"] } diff --git a/conda/js/test/test_plotters.json b/conda/js/test/test_plotters.json new file mode 100644 index 0000000000000000000000000000000000000000..e77da6e8f7cb37d84b4a0c6fa9bc30bc1a8c4f7a --- /dev/null +++ b/conda/js/test/test_plotters.json @@ -0,0 +1,341 @@ +[ + { + "name": "plot/bar/1", + "contents": { + "description": "Basic bar plotter for simple histograms", + "language": "python", + "parameters": { + "axis-fontsize": { + "default": 10, + "description": "Controls the axis font size (labels and values)", + "type": "uint16" + }, + "bar-alpha": { + "default": 0.75, + "description": "Value for the alpha effect in the bar plot", + "type": "float64" + }, + "bar-norm": { + "default": true, + "description": "If set to true will normalize the distribution between 0-1", + "type": "bool" + }, + "bar_attributes": { + "default": "", + "description": "Bar attributes passed directly to Matplotlib", + "type": "string" + }, + "content_type": { + "choice": [ + "image/png", + "image/jpeg", + "application/pdf" + ], + "default": "image/png", + "description": "The type of image returned", + "type": "string" + }, + "dpi": { + "default": 60, + "description": "Dots-per-inch in raster image formats", + "type": "uint16" + }, + "grid": { + "default": false, + "description": "If we should draw grid lines or not for the plot", + "type": "bool" + }, + "height": { + "default": 300, + "description": "Height of the resulting image in pixels", + "type": "uint16" + }, + "legend": { + "default": "", + "description": "Short description of the data, to be added to the plot", + "type": "string" + }, + "legend-bbox-to-anchor": { + "default": "1.0&1.0", + "description": "Specify any arbitrary location for the legend ", + "type": "string" + }, + "legend-fontsize": { + "default": 12, + "description": "Controls the font size of the legend", + "type": "uint16" + }, + "legend-loc": { + "default": "best", + "description": "The location of the legend", + "type": "string" + }, + "title": { + "default": "Bar plot", + "description": "The title for this plot", + "type": "string" + }, + "title-fontsize": { + "default": 10, + "description": "Controls the title font size", + "type": "uint16" + }, + "width": { + "default": 400, + "description": "Width of the resulting image in pixels", + "type": "uint16" + }, + "xaxis_multiplier": { + "default": 1, + "description": "The multiplication factor for the X-axis (horizontal)", + "type": "float64" + }, + "xlabel": { + "default": "", + "description": "The label of the X-axis (horizontal)", + "type": "string" + }, + "yaxis_log": { + "default": false, + "description": "If Y-axis (vertical) should be in log-scale", + "type": "bool" + }, + "yaxis_multiplier": { + "default": 1, + "description": "The multiplication factor for the Y-axis (vertical)", + "type": "float64" + }, + "ylabel": { + "default": "", + "description": "The label of the Y-axis (vertical)", + "type": "string" + } + }, + "dataformat": "plot/bar/1", + "uses": { + "baselib": "plot/baselib/1" + } + } + }, + { + "name": "plot/isoroc/1", + "contents": { + "description": "ROC/DET plotter following the ISO/IEC 19795-1:2006(E) standard", + "language": "python", + "parameters": { + "axis-fontsize": { + "default": 10, + "description": "Controls the axis font size (labels and values)", + "type": "uint16" + }, + "content_type": { + "choice": [ + "image/png", + "image/jpeg", + "application/pdf" + ], + "default": "image/png", + "description": "The type of image returned", + "type": "string" + }, + "det": { + "default": false, + "description": "If set, plot a DET curve instead of a ROC", + "type": "bool" + }, + "dpi": { + "default": 60, + "description": "Dots-per-inch in raster image formats", + "type": "uint16" + }, + "grid": { + "default": true, + "description": "If we should draw grid lines or not for the plot", + "type": "bool" + }, + "height": { + "default": 300, + "description": "Height of the resulting image in pixels", + "type": "uint16" + }, + "legend": { + "default": "", + "description": "Short description of the data, to be added to the plot", + "type": "string" + }, + "legend-fontsize": { + "default": 12, + "description": "Controls the font size of the legend", + "type": "uint16" + }, + "legend-loc": { + "default": "best", + "description": "The location of the legend", + "type": "string" + }, + "line_attributes": { + "default": "", + "description": "Scatter/Line attributes passed directly to Matplotlib", + "type": "string" + }, + "title": { + "default": "ISO/IEC 19795-1:2006(E) ROC", + "description": "The title for this plot", + "type": "string" + }, + "title-fontsize": { + "default": 10, + "description": "Controls the title font size", + "type": "uint16" + }, + "width": { + "default": 400, + "description": "Width of the resulting image in pixels", + "type": "uint16" + }, + "xaxis_log": { + "default": false, + "description": "If X-axis (horizontal) should be in log-scale", + "type": "bool" + }, + "xaxis_multiplier": { + "default": "100.0", + "description": "The multiplication factor for the X-axis (horizontal)", + "type": "float64" + }, + "xlabel": { + "default": "False Positives (False Match Rate), in %", + "description": "The label of the X-axis (horizontal)", + "type": "string" + }, + "xlim-left": { + "default": "0.0", + "description": "", + "type": "float64" + }, + "xlim-right": { + "default": "100.0", + "description": "", + "type": "float64" + }, + "yaxis_log": { + "default": false, + "description": "If Y-axis (vertical) should be in log-scale", + "type": "bool" + }, + "yaxis_multiplier": { + "default": "100.0", + "description": "The multiplication factor for the Y-axis (vertical)", + "type": "float64" + }, + "ylabel": { + "default": "True Positives (1 - False Non-Match Rate), in %", + "description": "The label of the Y-axis (vertical)", + "type": "string" + }, + "ylim-bottom": { + "default": "0.0", + "description": "", + "type": "float64" + }, + "ylim-top": { + "default": "100.0", + "description": "", + "type": "float64" + } + }, + "dataformat": "plot/isoroc/1", + "uses": { + "baselib": "plot/baselib/1" + } + } + }, + { + "name": "plot/scatter/1", + "contents": { + "description": "Basic scatter plotter for simple lines", + "language": "python", + "parameters": { + "content_type": { + "choice": [ + "image/png", + "image/jpeg", + "application/pdf" + ], + "default": "image/png", + "description": "The type of image returned", + "type": "string" + }, + "dpi": { + "default": 60, + "description": "Dots-per-inch in raster image formats", + "type": "uint16" + }, + "grid": { + "default": false, + "description": "If we should draw grid lines or not for the plot", + "type": "bool" + }, + "height": { + "default": 300, + "description": "Height of the resulting image in pixels", + "type": "uint16" + }, + "legend": { + "default": "", + "description": "Short description of the data, to be added to the plot", + "type": "string" + }, + "line_attributes": { + "default": "", + "description": "Scatter/Line attributes passed directly to Matplotlib", + "type": "string" + }, + "title": { + "default": "Scatter plot", + "description": "The title for this plot", + "type": "string" + }, + "width": { + "default": 400, + "description": "Width of the resulting image in pixels", + "type": "uint16" + }, + "xaxis_log": { + "default": false, + "description": "If X-axis (horizontal) should be in log-scale", + "type": "bool" + }, + "xaxis_multiplier": { + "default": "1.0", + "description": "The multiplication factor for the X-axis (horizontal)", + "type": "float64" + }, + "xlabel": { + "default": "X", + "description": "The label of the X-axis (horizontal)", + "type": "string" + }, + "yaxis_log": { + "default": false, + "description": "If Y-axis (vertical) should be in log-scale", + "type": "bool" + }, + "yaxis_multiplier": { + "default": "1.0", + "description": "The multiplication factor for the Y-axis (vertical)", + "type": "float64" + }, + "ylabel": { + "default": "Y", + "description": "The label of the Y-axis (vertical)", + "type": "string" + } + }, + "dataformat": "plot/scatter/1", + "uses": { + "baselib": "plot/baselib/1" + } + } + } +]