Transform.react.js

  1. import React from 'react';
  2. import Base from './_sub/Base.react';
  3. import PropTypes from 'prop-types';
  4. import Alert from 'react-bootstrap/Alert';
  5. import Button from 'react-bootstrap/Button';
  6. import Modal from 'react-bootstrap/Modal';
  7. import Form from 'react-bootstrap/Form';
  8. import InputGroup from 'react-bootstrap/InputGroup';
  9. import Accordion from 'react-bootstrap/Accordion';
  10. import AggrTransform from './_sub/AggrTransform.react';
  11. import EvalTransform from './_sub/EvalTransform.react';
  12. import MeltTransform from './_sub/MeltTransform.react';
  13. import CombinecatTransform from './_sub/CombinecatTransform.react';
  14. import CategoryLookup from './_sub/CategoryLookup.react';
  15. import DropnaTransform from './_sub/DropnaTransform.react';
  16. import WideToLong from './_sub/WideToLong.react';
  17. import ZerosToNanTransform from './_sub/ZerosToNanTransform.react';
  18. import RenameTransform from './_sub/RenameTransform.react';
  19. import StrSplitTransform from './_sub/StrSplitTransform.react';
  20. import BinTransform from './_sub/BinTransform.react';
  21. import FilterIqrTransform from './_sub/FilterIqrTransform.react';
  22. import AsType from './_sub/AsType.react';
  23. import GroupedSample from './_sub/GroupedSample.react';
  24. import AggrSvg from 'react-svg-loader!./_svg/aggr.svg';
  25. import EvalSvg from 'react-svg-loader!./_svg/eval.svg';
  26. import MeltSvg from 'react-svg-loader!./_svg/melt.svg';
  27. import CombinecatSvg from 'react-svg-loader!./_svg/combinecat.svg';
  28. import CatlookupSvg from 'react-svg-loader!./_svg/catlookup.svg';
  29. import DropnaSvg from 'react-svg-loader!./_svg/dropna.svg';
  30. import WideToLongSvg from 'react-svg-loader!./_svg/wide_to_long.svg';
  31. import ZerostoNanSvg from 'react-svg-loader!./_svg/zerostonan.svg';
  32. import RenameSvg from 'react-svg-loader!./_svg/rename.svg';
  33. import StrSplitSvg from 'react-svg-loader!./_svg/str.split.svg';
  34. import BinSvg from 'react-svg-loader!./_svg/bin.svg';
  35. import FilterIqrSvg from 'react-svg-loader!./_svg/filteriqr.svg';
  36. import AsTypeSvg from 'react-svg-loader!./_svg/astype.svg';
  37. import GroupedSampleSvg from 'react-svg-loader!./_svg/groupedsample.svg';
  38. /**
  39. * <div style="width:450px; margin-left: 20px; float: right; margin-top: -150px;">
  40. * <img src="https://raw.githubusercontent.com/VK/dash-express-components/main/.media/transform.png"/>
  41. * <img src="https://raw.githubusercontent.com/VK/dash-express-components/main/.media/transform-modal.png"/>
  42. * <img src="https://raw.githubusercontent.com/VK/dash-express-components/main/.media/transform-types.png"/>
  43. * </div>
  44. *
  45. * The `Transform` component helps to create user defined data transformations.
  46. * Currently basic transformations are available, like:
  47. *
  48. * <ul style="margin-left: 20px;">
  49. * <li><b>eval</b></li>
  50. * <li><b>groupby([...]).aggr([...])</b></li>
  51. * <li><b>melt</b></li>
  52. * <li><b>wide_to_long</b></li>
  53. * <li><b>replace</b></li>
  54. * <li><b>rename</b></li>
  55. * </ul>
  56. * @hideconstructor
  57. *
  58. * @example
  59. * import dash_express_components as dxc
  60. * import plotly.express as px
  61. *
  62. * meta = dxc.get_meta(px.data.gapminder())
  63. *
  64. * dxc.Transform(
  65. * id="transform",
  66. * meta=meta
  67. * )
  68. * @public
  69. */
  70. class Transform extends Base {
  71. static trafo_groups = [
  72. { label: "New columns", value: "col" },
  73. { label: "Reshape data", value: "reshape" },
  74. { label: "Metadata", value: "meta" },
  75. { label: "Missing & filter data", value: "missing" }
  76. ]
  77. static known_trafos = [
  78. {
  79. group: "col", type: "eval", class: EvalTransform,
  80. "label": "Compute new column", svg: <EvalSvg />
  81. },
  82. {
  83. group: "col", type: "strsplit", class: StrSplitTransform,
  84. "label": "Compute a string split", svg: <StrSplitSvg />
  85. },
  86. {
  87. group: "reshape", type: "aggr", class: AggrTransform,
  88. "label": "Aggregate grouped dataset", svg: <AggrSvg />
  89. },
  90. {
  91. group: "col", type: "combinecat", class: CombinecatTransform,
  92. "label": "Combine multiple columns to new category", svg: <CombinecatSvg />
  93. },
  94. {
  95. group: "reshape", type: "melt", class: MeltTransform,
  96. "label": "Rearrange multiple colums to one", svg: <MeltSvg />
  97. },
  98. {
  99. group: "reshape", type: "wide_to_long", class: WideToLong,
  100. "label": "Rearrange columns based on naming", svg: <WideToLongSvg />
  101. },
  102. {
  103. group: "col", type: "catlookup", class: CategoryLookup,
  104. "label": "Apply a lookup on a categorical column", svg: <CatlookupSvg />
  105. },
  106. {
  107. group: "missing", type: "dropna", class: DropnaTransform,
  108. "label": "Remove rows with nan values", svg: <DropnaSvg />
  109. },
  110. {
  111. group: "missing", type: "zerostonan", class: ZerosToNanTransform,
  112. "label": "Replace zero values with nan values", svg: <ZerostoNanSvg />
  113. },
  114. {
  115. group: "missing", type: "filteriqr", class: FilterIqrTransform,
  116. "label": "Apply a grouped IQR filter", svg: <FilterIqrSvg />
  117. },
  118. {
  119. group: "missing", type: "groupby_sample", class: GroupedSample,
  120. "label": "Apply a grouped sampling", svg: <GroupedSampleSvg />
  121. },
  122. {
  123. group: "meta", type: "rename", class: RenameTransform,
  124. "label": "Rename multiple columns", svg: <RenameSvg />
  125. },
  126. {
  127. group: "meta", type: "as_type", class: AsType,
  128. "label": "Recast multiple columns", svg: <AsTypeSvg />
  129. },
  130. {
  131. group: "col", type: "bin", class: BinTransform,
  132. "label": "Compute a binned variable", svg: <BinSvg />
  133. },
  134. ]
  135. constructor(props) {
  136. super([], props);
  137. this.state =
  138. {
  139. ...this.state,
  140. /* state of the modal to add new filters */
  141. showAddModal: false,
  142. showChooseModal: false,
  143. transformIndex: undefined,
  144. transformType: "",
  145. sub_config: {}
  146. };
  147. this.update_config(this.state.config, true);
  148. }
  149. handleClose() {
  150. this.setState({ showAddModal: false });
  151. }
  152. handleShow() {
  153. this.setState({ showAddModal: true });
  154. }
  155. handleChooseClose() {
  156. this.setState({ showChooseModal: false });
  157. }
  158. handleChooseShow() {
  159. this.setState({ showChooseModal: true });
  160. }
  161. UNSAFE_componentWillReceiveProps(newProps) {
  162. const update_config_needed = (newProps.config !== this.props.config);
  163. super.UNSAFE_componentWillReceiveProps(newProps);
  164. if (update_config_needed && "config" in newProps) {
  165. this.update_config(newProps.config);
  166. }
  167. }
  168. update_config(new_config, constructor = false) {
  169. super.update_config(new_config, constructor);
  170. //let new_meta = JSON.parse(JSON.stringify(this.state.meta))
  171. let new_meta = { ...this.state.meta };
  172. let meta_stages = [];
  173. let stage_results = [];
  174. meta_stages.push(new_meta);
  175. if (new_config)
  176. new_config.forEach(el => {
  177. // if the transform is not known, skip it
  178. if (Transform.known_trafos.filter(t => t["type"] === el["type"]).length !== 0) {
  179. let transform_class = Transform.known_trafos.filter(t => t["type"] === el["type"])[0]["class"];
  180. let res = transform_class.eval(
  181. {
  182. ...el,
  183. meta: new_meta
  184. }
  185. );
  186. if (res["new_meta"] != undefined) {
  187. new_meta = res["new_meta"];
  188. meta_stages.push(new_meta);
  189. } else {
  190. meta_stages.push({});
  191. }
  192. stage_results.push(res);
  193. } else {
  194. // TODO add a server callback to get the result of the transform
  195. stage_results.push({ error: false, message: "Unknown transform type: " + el["type"] });
  196. meta_stages.push({});
  197. }
  198. });
  199. super.update_meta_out(new_meta, constructor);
  200. if (constructor) {
  201. this.state = {
  202. ...this.state,
  203. meta_stages: meta_stages,
  204. stage_results: stage_results,
  205. ...this.get_columns(new_meta)
  206. }
  207. } else {
  208. this.setState({
  209. meta_stages: meta_stages,
  210. stage_results: stage_results,
  211. ...this.get_columns(new_meta)
  212. });
  213. }
  214. return new_meta;
  215. }
  216. get_transform_blocks() {
  217. const { config, stage_results } = this.state;
  218. if (config) {
  219. return <div>
  220. {
  221. config.map((el, id) => {
  222. // if the transform is not known, we use a generic block
  223. if (Transform.known_trafos.filter(t => t["type"] === el["type"]).length === 0) {
  224. return <Alert variant='warning' key={id}>
  225. <pre className='mb-0'>{JSON.stringify(el, null, 2)}</pre>
  226. </Alert>
  227. }
  228. let transform_class = Transform.known_trafos.filter(t => t["type"] === el["type"])[0]["class"];
  229. let config_string = transform_class.config_to_string(el);
  230. let variant = (stage_results[id].error) ? 'secondary' : 'primary';
  231. let error_string = (stage_results[id].error) ? (<span className="text-danger"><br /><b>Error: </b>{stage_results[id].message}</span>) : '';
  232. return (<Alert dismissible variant={variant} key={id} onClose={() => {
  233. let new_config = config.filter((e, idx) => idx !== id);
  234. this.update_config(new_config)
  235. }}>
  236. {config_string}
  237. {error_string}
  238. <button className='btn-close btn-edit'
  239. onClick={() => {
  240. let update_state = {
  241. transformIndex: id,
  242. transformType: el.type,
  243. sub_config: el
  244. };
  245. this.setState(update_state, () => {
  246. this.handleShow();
  247. })
  248. }}
  249. ></button>
  250. </Alert>)
  251. }
  252. )
  253. }
  254. </div>
  255. }
  256. }
  257. get_modal_blocks() {
  258. const {
  259. allColOptions,
  260. catColOptions,
  261. numColOptions,
  262. allOptions,
  263. meta_out,
  264. showAddModal,
  265. transformType,
  266. sub_config,
  267. config,
  268. transformIndex,
  269. meta_stages
  270. } = this.state;
  271. const {
  272. id
  273. } = this.props;
  274. const stt = Transform.known_trafos.filter((el) => el.type === transformType);
  275. const tt = (stt.length === 1) ? stt[0] : undefined;
  276. return (<Modal
  277. centered
  278. backdrop="static"
  279. animation={false}
  280. show={showAddModal}
  281. onHide={() => this.handleClose()
  282. }
  283. >
  284. <Modal.Header closeButton>
  285. <Modal.Title>{(stt.length === 1) ? tt.label : "Add transform"}</Modal.Title>
  286. </Modal.Header>
  287. <Modal.Body><div style={{ minHeight: "15em" }} className="mb-3">
  288. <Button
  289. key={id + "change-transform-button"}
  290. variant="outline-secondary"
  291. className="d-flex align-items-center w-100 mb-2"
  292. onClick={() => this.handleChooseShow()}
  293. style={{ "height": "110px" }}
  294. >
  295. {(tt && "svg" in tt) && <div className="w-100">{tt.svg}</div>}
  296. {!(tt && "svg" in tt) && <div className="w-100 h3">Choose a transformation type</div>}
  297. </Button>
  298. {
  299. Transform.known_trafos.map(trafo_el => {
  300. let input_meta = (transformIndex === undefined) ? meta_out : meta_stages[transformIndex];
  301. let stage_options = this.get_columns(input_meta);
  302. return (
  303. transformType === trafo_el["type"] &&
  304. <trafo_el.class
  305. key={"config" + trafo_el["type"]}
  306. config={sub_config}
  307. meta={input_meta}
  308. allColOptions={stage_options.allColOptions}
  309. catColOptions={stage_options.catColOptions}
  310. numColOptions={stage_options.numColOptions}
  311. allOptions={stage_options.allOptions}
  312. setProps={e => { if ("config" in e) { this.setState({ sub_config: e.config }) } }}
  313. />
  314. )
  315. })
  316. }
  317. </div>
  318. </Modal.Body>
  319. <Modal.Footer>
  320. <Button variant="secondary" onClick={() => this.handleClose()}>
  321. Close
  322. </Button>
  323. <Button variant="primary" onClick={() => {
  324. if ("type" in sub_config) {
  325. if (transformIndex === undefined) {
  326. //add a new transform
  327. let transform_class = Transform.known_trafos.filter(el => el["type"] === sub_config["type"])[0]["class"];
  328. let res = transform_class.eval(
  329. {
  330. ...sub_config,
  331. meta: meta_out
  332. }
  333. );
  334. if (!res.error || window.confirm("Do you want to add the transform, even with errors?")) {
  335. let new_config = [
  336. ...config,
  337. sub_config
  338. ];
  339. this.update_config(new_config);
  340. this.handleClose();
  341. }
  342. } else {
  343. //update a transform
  344. let transform_class = Transform.known_trafos.filter(el => el["type"] === sub_config["type"])[0]["class"];
  345. let res = transform_class.eval(
  346. {
  347. ...sub_config,
  348. meta: meta_stages[transformIndex]
  349. }
  350. );
  351. if (!res.error || window.confirm("Do you want to add the transform, even with errors?")) {
  352. let new_config = JSON.parse(JSON.stringify(config));
  353. new_config[transformIndex] = sub_config;
  354. this.update_config(new_config);
  355. this.handleClose();
  356. }
  357. }
  358. }
  359. }}>
  360. {(transformIndex === undefined) ? "Add" : "Update"}
  361. </Button>
  362. </Modal.Footer>
  363. </Modal>)
  364. }
  365. get_choose_modal() {
  366. const {
  367. showChooseModal
  368. } = this.state;
  369. const {
  370. id
  371. } = this.props;
  372. return (<Modal
  373. size="xl"
  374. centered
  375. backdrop="static"
  376. animation={false}
  377. show={showChooseModal}
  378. onHide={() => this.handleChooseClose()}
  379. key={id + "-trafo-type-modal"}
  380. >
  381. <Modal.Header closeButton>
  382. <Modal.Title>Transformation Types</Modal.Title>
  383. </Modal.Header>
  384. <Modal.Body><div className="mt-2 dxc-container dxc-row" style={{ padding: 0 }}>
  385. {Transform.trafo_groups.map(gr => {
  386. return <div className='dxc-p-1 dxc-col-6'><h4>{gr.label}</h4> {
  387. Transform.known_trafos.filter(pt => pt.group == gr.value).map(pt => {
  388. return (
  389. <Button
  390. key={"set-plot-" + pt.type}
  391. variant="outline-secondary"
  392. className="d-flex align-items-center w-100 mb-2"
  393. onClick={(e) => {
  394. this.setState({
  395. transformType: pt.type
  396. });
  397. this.handleChooseClose()
  398. }}
  399. >
  400. <div style={{ "transform": "scale(.6)", "transformOrigin": "0 0", width: "180px", height: "60px" }}>{(pt && "svg" in pt) ? pt.svg : ""}</div>
  401. <div className="flex-grow-1 m-1 h5">
  402. {(pt && "label" in pt) ? pt.label : ""}
  403. </div>
  404. </Button>
  405. );
  406. })
  407. } </div>
  408. })
  409. }
  410. </div>
  411. </Modal.Body>
  412. <Modal.Footer>
  413. <Button variant="secondary" onClick={() => this.handleChooseClose()}>
  414. Close
  415. </Button>
  416. </Modal.Footer>
  417. </Modal>)
  418. }
  419. render() {
  420. return (
  421. <div>
  422. {this.get_transform_blocks()}
  423. <Button className='w-100' onClick={() => {
  424. this.setState({
  425. transformType: "",
  426. sub_config: {},
  427. transformIndex: undefined
  428. }, () => {
  429. this.handleShow();
  430. });
  431. }}>
  432. Add transformation
  433. </Button>
  434. {this.get_modal_blocks()}
  435. {this.get_choose_modal()}
  436. </div>
  437. )
  438. }
  439. }
  440. Transform.defaultProps = {};
  441. /**
  442. * @typedef
  443. * @public
  444. * @enum {}
  445. */
  446. Transform.propTypes = {
  447. /**
  448. * The ID used to identify this component in Dash callbacks.
  449. */
  450. id: PropTypes.string.isRequired,
  451. /**
  452. * The config the user sets in this component.
  453. */
  454. config: PropTypes.any,
  455. /**
  456. * The metadata this section is based on.
  457. */
  458. meta: PropTypes.any.isRequired,
  459. /**
  460. * The metadata section will create as output.
  461. */
  462. meta_out: PropTypes.any,
  463. /**
  464. * Dash-assigned callback that should be called to report property changes
  465. * to Dash, to make them available for callbacks.
  466. */
  467. setProps: PropTypes.func
  468. };
  469. /**
  470. * @private
  471. */
  472. export default Transform;
  473. JAVASCRIPT
    Copied!