diff --git a/.eslintrc.json b/.eslintrc.json index 723a1d5..bf061d3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,6 +20,15 @@ "no-plusplus": 0, "no-param-reassign": ["error", { "props": false }], "prefer-destructuring": ["error", { "object": true, "array": false }], + "react/no-unused-class-component-methods": 0, + "react/prop-types": 0, + "eqeqeq": 0, + "react/no-unused-state": 0, + "jsx-a11y/control-has-associated-label": 0, // don't like omitting this... + "jsx-a11y/click-events-have-key-events": 0, // don't like omitting this either... + "no-underscore-dangle":0, + "no-unused-vars":0, // for now... + "no-console": 0, "jsx-a11y/label-has-associated-control": [ "error", { diff --git a/.github/workflows/buildah.yml b/.github/workflows/buildah.yml deleted file mode 100644 index a5259d5..0000000 --- a/.github/workflows/buildah.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Buildah via Dockerfile -on: [push] - -jobs: - build: - name: Build image - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Buildah Action - uses: redhat-actions/buildah-build@v2.2 - with: - image: eaglescope-edge - tags: v1 ${{ github.sha }} - dockerfiles: | - ./Dockerfile diff --git a/.github/workflows/draft-paper.yml b/.github/workflows/draft-paper.yml new file mode 100644 index 0000000..f62d314 --- /dev/null +++ b/.github/workflows/draft-paper.yml @@ -0,0 +1,26 @@ +on: [push] + +name: Build Paper Draft + + +jobs: + paper: + runs-on: ubuntu-latest + name: JOSS Paper Draft + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build draft PDF + uses: openjournals/openjournals-draft-action@master + with: + journal: joss + # This should be the path to the paper within your repo. + paper-path: paper.md + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: paper + # This is the output path where Pandoc will write the compiled + # PDF. Note, this should be the same directory as the input + # paper.md + path: paper.pdf \ No newline at end of file diff --git a/.github/workflows/lint_test.yml b/.github/workflows/lint_test.yml new file mode 100644 index 0000000..920e30d --- /dev/null +++ b/.github/workflows/lint_test.yml @@ -0,0 +1,24 @@ +name: Code Quality Checks (Lint) + +on: + push: + +jobs: + lint: + name: Run npm lint + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Install dependencies + run: npm install --legacy-peer-deps + + - name: Run lint + run: npm run lint diff --git a/.github/workflows/smoke_test.yml b/.github/workflows/smoke_test.yml new file mode 100644 index 0000000..21ab3f3 --- /dev/null +++ b/.github/workflows/smoke_test.yml @@ -0,0 +1,23 @@ +name: Code Test (Smoke) + +on: [push] + +jobs: + lint: + name: Run npm lint + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Install dependencies + run: npm install --legacy-peer-deps + + - name: Run test + run: npm test diff --git a/ContextualVis.png b/ContextualVis.png new file mode 100644 index 0000000..a3b0a2c Binary files /dev/null and b/ContextualVis.png differ diff --git a/Dockerfile b/Dockerfile index cbfa252..0a0f4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ COPY ./ /source/ WORKDIR /source/ RUN rm -rf ./.git/ -RUN npm install +RUN npm install --legacy-peer-deps RUN npm run-script build RUN mkdir -p /var/www/html/ RUN mv /source/dist/* /var/www/html diff --git a/package-lock.json b/package-lock.json index b11ca96..04dfa0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "BSD-3-Clause", "dependencies": { "@fortawesome/fontawesome": "^1.1.8", + "@fortawesome/fontawesome-free": "^6.5.2", "@fortawesome/fontawesome-free-solid": "^5.0.13", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-regular-svg-icons": "^6.4.2", @@ -18,6 +19,7 @@ "@types/sortablejs": "^1.15.4", "array-move": "^3.0.1", "bootstrap": "^5.3.2", + "bootstrap-css-only": "^4.4.1", "crossfilter2": "^1.5.4", "d3": "^5.16.0", "jquery": "^3.7.1", @@ -1903,9 +1905,9 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "5.15.4", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", - "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz", + "integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==", "hasInstallScript": true, "engines": { "node": ">=6" @@ -8367,6 +8369,15 @@ "react-dom": "*" } }, + "node_modules/mdbreact/node_modules/@fortawesome/fontawesome-free": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", + "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/mdbreact/node_modules/prop-types": { "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", diff --git a/package.json b/package.json index e1ba3f0..944afb9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "lint": "eslint source", - "lint:fix": "eslint --fix", + "lint:fix": "eslint source --fix", "test": "npm run-script build", "build": "parcel build source/index.html --public-url ./", "dev": "parcel source/index.html" @@ -24,6 +24,7 @@ "license": "BSD-3-Clause", "dependencies": { "@fortawesome/fontawesome": "^1.1.8", + "@fortawesome/fontawesome-free": "^6.5.2", "@fortawesome/fontawesome-free-solid": "^5.0.13", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-regular-svg-icons": "^6.4.2", @@ -32,6 +33,7 @@ "@types/sortablejs": "^1.15.4", "array-move": "^3.0.1", "bootstrap": "^5.3.2", + "bootstrap-css-only": "^4.4.1", "crossfilter2": "^1.5.4", "d3": "^5.16.0", "jquery": "^3.7.1", diff --git a/paper.bib b/paper.bib new file mode 100644 index 0000000..b5ea0c3 --- /dev/null +++ b/paper.bib @@ -0,0 +1,50 @@ +@misc{bokeh, + author = {Bokeh Contributors}, + title = {Bokeh: an interactive visualization library for modern web browsers}, + year = {2024}, + url = {https://github.com/bokeh/bokeh} +} + +@article{cbioportal2013, + author = {Gao, Jianjiong and Aksoy, Bulent Arman and Dogrusoz, Ugur and Dresdner, Gideon and Gross, Benjamin and Sumer, S Onur and Sun, Yichao and Jacobsen, Anders and Sinha, Rileen and Larsson, Erik and Cerami, Ethan and Sander, Chris and Schultz, Nikolaus}, + title = {Integrative analysis of complex cancer genomics and clinical profiles using the cBioPortal}, + journal = {Sci Signal}, + year = {2013}, + volume = {6}, + number = {269} +} + +@inproceedings{datascope2017, + author = {Iyer, Ganesh and DuttaDuwarah, Sapoonjyoti and Sharma, Ashish}, + title = {DataScope: Interactive visual exploratory dashboards for large multidimensional data}, + booktitle = {2017 IEEE Workshop on Visual Analytics in Healthcare (VAHC)}, + year = {2017}, + pages = {17-23} +} + +@article{nbia2020, + author = {Nguyen, Tin and Shafi, Adib and Nguyen, Tuan-Minh and Schissler, A. Grant and Draghici, Sorin}, + title = {NBIA: a network-based integrative analysis framework - applied to pathway analysis}, + journal = {Scientific Reports}, + year = {2020}, + volume = {10}, + number = {1} +} + +@article{tica2013, + author = {Clark, Kenneth and Vendt, Bruce and Smith, Kirk and Freymann, John and Kirby, Justin and Moore, Stephen and Phillips, Stanley and Maffit, David and Pringle, Michael and Tarbox, Lawrence and Prior, Fred}, + title = {The Cancer Imaging Archive (TCIA): Maintaining and Operating a Public Information Repository}, + journal = {J Digit Imaging}, + year = {2013}, + volume = {26}, + number = {6}, + pages = {1045-1057} +} + +@article{prism2020, + author = {Sharma, Ashish and Tarbox, Lawrence and Kurc, Tahsin and Bona, Jonathan and Smith, Kirk and Kathiravelu, Pradeeban and Bremer, Erich and Saltz, Joel H. and Prior, Fred}, + title = {PRISM: A Platform for Imaging in Precision Medicine}, + journal = {JCO Clinical Cancer Informatics}, + year = {2020}, + volume = {4} +} diff --git a/paper.md b/paper.md new file mode 100644 index 0000000..59bb8ee --- /dev/null +++ b/paper.md @@ -0,0 +1,51 @@ +--- +title: 'Eaglescope: an interactive visualization and cohort selection tool for biomedical data exploration.' +tags: + - javascript + - interactive visualization + - data exploration + - biomedical research + - data analysis +authors: + - name: Nan Li + equal-contrib: true + affiliation: 1 + orcid: 0000-0002-3975-4809 + - name: Ryan Birmingham + orcid: 0000-0002-7943-6346 + equal-contrib: true + affiliation: 1 + - name: Tony Pan + affiliation: 1 + - name: Yahia Zakaria + corresponding: true + affiliation: 2 +affiliations: + - name: Emory Univeristy, USA + index: 1 + - name: Independent Researcher, Egypt + index: 2 +date: 23 April 2024 +bibliography: paper.bib + +--- + +# Summary + +Eaglescope is a configurable code-free interactive visualization and cohort selection tool designed for biomedical data exploration. It is designed to be hosted flexibly without the need for a dedicated server, and creates an interactive dashboard based upon a configuration file and either an API or data file. It uses visualizations of sets of features to describe and enable contextual filtering of the data. This allows for users to understand deeper patterns or anomalies within the data, and to create datasets specifically tuned to their requirements effortlessly. +Eaglescope is typically utilized either as a tool to create refined datasets tailored for training and validating machine learning AI models, or as a central hub for further exploration, allowing users to seamlessly navigate to biomedical viewers such as DICOM or whole slide imaging (WSI) platforms. +![Interactive Contextual Visualizations](./ContextualVis.png) +To create a dashboard, users simply need to create a file specifying the data source, configurations for each visualization, and any further desired customizations to the platform. Hosting is as straightforward as copying the static files, along with the configuration and data files if applicable, to any location capable of hosting static files. This streamlined process was intentionally designed to support the visualization of multiple datasets without added complexity or specialized requirements. Additionally, the flexibility of hosting allows for seamless scalability with demand, eliminating the need for modifications to Eaglescope itself. + +# Statement of Need + +Eaglescope was initially developed as a successor to abother tool [@datascope2017] to enhance the usability of interactively exploring large biomedical datasets. To achieve this, we created a versatile tool capable of supporting multiple datasets, easily reconfigurable without coding, and deployable in a serverless manner. Moreover, Eaglescope facilitates hierarchical usage, allowing dashboards to represent and link to other dashboards. Recognizing the value of visually contextualized filtering operations, we introduced a set of visualizations that display filtered data within its broader context. This approach enables users to uncover patterns in the data that might otherwise go unnoticed, fostering deeper insights and more informed decision making in biomedical research. +Eaglescope takes inspiration from Bokeh [@bokeh], cBioPortal [@cbioportal2013], and NBIA [@nbia2020] for features and user experience. +The Cancer Imaging Archive (TCIA) [@tica2013] and the National Cancer Institute use Eaglescope to enable exploration and export of the large amount of data across collections and modalities and the PRISM [@prism2020] project includes Eaglescope to facilitate dataset creation and visualization. + +# Acknowledgements + +We acknowledge all contibutors to the Eaglescope project, as well as grant support subawarded by the University of Arkansas Medical School and both financial and logistical support from the Emory Univeristy Department of Biomedical Informatics. + +# References + diff --git a/source/common/DataTranform.js b/source/common/DataTranform.js deleted file mode 100644 index 0c5221f..0000000 --- a/source/common/DataTranform.js +++ /dev/null @@ -1,22 +0,0 @@ -function GroupByField(data, feild) { - return d3 - .nest() - .key((d) => d[feild]) - .entries(data); -} - -function groupByFieldAndCountElements(data, field) { - return d3 - .nest() - .key((d) => d[field]) - .rollup((v) => v.length) - .entries(data); -} - -function groupByFieldAndSumElements(data, keyField, valueFeild) { - return d3 - .nest() - .key((d) => d[keyField]) - .rollup((v) => ({ length: v.length, total: d3.sum(v, (d) => parseFloat(d[valueFeild])) })) - .entries(data); -} diff --git a/source/common/utils.js b/source/common/utils.js index 1e4fdb5..fffee15 100644 --- a/source/common/utils.js +++ b/source/common/utils.js @@ -113,7 +113,7 @@ export function getLayoutConfig(chartsConfig, cols, resiziable = false) { const layout = []; const matrix = createMatrix(cols); // sort charts by priority - chartsConfig = chartsConfig.sort( + const chartsConfigSorted = chartsConfig.sort( (a, b) => b.priority - a.priority || a.title.localeCompare(b.displayName), ); @@ -124,7 +124,7 @@ export function getLayoutConfig(chartsConfig, cols, resiziable = false) { // filter out the solid chart before compute the position of the rest of charts // make an arrangement for the rest of charts - chartsConfig.forEach((chart) => { + chartsConfigSorted.forEach((chart) => { // get the size of a chart; default size is [1,1] (w,h) const size = chart.size || [1, 1]; const pos = matrix.length === 0 ? [0, 0] : getPosition(matrix, size); @@ -150,15 +150,3 @@ export function getLayoutConfig(chartsConfig, cols, resiziable = false) { return { layout, rows: matrix[0].length }; } - -// Grid includes 10px margin -export function getSizeOfGridContent(gridSize, margin) { - return [ - STUDY_VIEW_CONFIG.layout.grid.w * gridSize[0] - + (chartDimension.w - 1) * STUDY_VIEW_CONFIG.layout.gridMargin.x - - borderWidth * 2, - STUDY_VIEW_CONFIG.layout.grid.h * gridSize[1] - + (chartDimension.h - 1) * STUDY_VIEW_CONFIG.layout.gridMargin.y - - chartHeight, - ]; -} diff --git a/source/components/Eaglescope/Eaglescope.js b/source/components/Eaglescope/Eaglescope.js index e67804d..ef4fe51 100644 --- a/source/components/Eaglescope/Eaglescope.js +++ b/source/components/Eaglescope/Eaglescope.js @@ -39,12 +39,12 @@ function Eaglescope() { if (filters.length > 0) { setProgressAttrs({ now: filteredData.length, - label: `${filteredData.length}/${data.length}, ${Math.floor((filteredData.length/data.length)*100)}\%`, + label: `${filteredData.length}/${data.length}, ${Math.floor((filteredData.length / data.length) * 100)}%`, }); } else { setProgressAttrs({ now: data.length, - label: `${data.length}/${data.length}, ${Math.floor((data.length/data.length)*100)}\%`, + label: `${data.length}/${data.length}, ${Math.floor((data.length / data.length) * 100)}%`, }); } }, [filters, filteredData]); diff --git a/source/components/Layout/VisGridView/VisGridItem/VisGridItem.js b/source/components/Layout/VisGridView/VisGridItem/VisGridItem.js index fabf086..abf37be 100644 --- a/source/components/Layout/VisGridView/VisGridItem/VisGridItem.js +++ b/source/components/Layout/VisGridView/VisGridItem/VisGridItem.js @@ -1,9 +1,9 @@ import React, { useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import VisGridItemContent from './VisGridItemContent/VisGridItemContent'; import VisGridItemHeader from './VisGridItemHeader/VisGridItemHeader'; import { DataContext } from '../../../../contexts/DataContext'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; // css class import './VisGridItem.css'; @@ -29,7 +29,8 @@ function VisGridItem(props) { onMouseEnter={onMouseEnterHandle} onMouseLeave={onMouseLeaveHandle} - > + - - {props.isResizing?
- -
: - } + + {props.isResizing ? ( +
+ +
+ ) : ( + + )} ); } diff --git a/source/components/Layout/VisGridView/VisGridView.js b/source/components/Layout/VisGridView/VisGridView.js index 69befaf..1be2df8 100644 --- a/source/components/Layout/VisGridView/VisGridView.js +++ b/source/components/Layout/VisGridView/VisGridView.js @@ -25,7 +25,7 @@ function VisGridView({ fullVisScreenHandler, fullScreened }) { const draggableHandle = config.GRAGGABLE ? '.draggable' : ''; const isDraggable = config.DRAGGABLE || false; const isResizable = config.RESIZABLE || false; - + const [isResizing, SetIsResizing] = useState(false); const [resizingItemId, SetResizingItemId] = useState(null); const [appLayout, setAppLayout] = useState({ diff --git a/source/components/Settings/Settings.js b/source/components/Settings/Settings.js index df4a0b7..b62fa59 100644 --- a/source/components/Settings/Settings.js +++ b/source/components/Settings/Settings.js @@ -90,8 +90,9 @@ function Settings() { return ( <> {showNewVis && } - - {config.HAS_SETTINGS&&()} + + )} scaleRef.current.y(d[fields.y]) + scaleRef.current.y.bandwidth() / 2 + 4) .text((d) => d.key) - .on('click', x=>{ + .on('click', (x) => { const filter = { id: props.id, title: props.title, @@ -84,7 +84,7 @@ function HorizontalBarChart(props) { operation: 'eq', values: x.key, }; - props.filterAdded([filter]) + props.filterAdded([filter]); }); }; diff --git a/source/components/VisualTools/Chart/ScatterChart.js b/source/components/VisualTools/Chart/ScatterChart.js index 7662a13..371ca82 100644 --- a/source/components/VisualTools/Chart/ScatterChart.js +++ b/source/components/VisualTools/Chart/ScatterChart.js @@ -99,7 +99,7 @@ export default class ScatterChart extends PureComponent { svg.selectAll('rect').remove('rect'); const startX = Math.min(this.startPosition[0], this.endPosition[0]); const startY = Math.min(this.startPosition[1], this.endPosition[1]); - const selectedArea=svg.append('rect') + const selectedArea = svg.append('rect') .attr('position', 'absolute') .attr('x', startX + this.state.margin.left) .attr('y', startY) diff --git a/source/components/VisualTools/VisGridCard/MasonryComponent/MasonryComponent.js b/source/components/VisualTools/VisGridCard/MasonryComponent/MasonryComponent.js index 30b2912..34e685c 100644 --- a/source/components/VisualTools/VisGridCard/MasonryComponent/MasonryComponent.js +++ b/source/components/VisualTools/VisGridCard/MasonryComponent/MasonryComponent.js @@ -46,7 +46,12 @@ export default class MasonryComponent extends Component { if (style.top !== undefined && Number.isInteger(style.top)) style.top += 10; if (style.left !== undefined && Number.isInteger(style.left)) style.left += 10; return ( - +
{item[fields.image] && ( { - // send event with new data - window.__data = data; - const ev = new CustomEvent('filterOut', { detail: { data, filter: {} } }); - window.dispatchEvent(ev); - console.info('filterOut event: ', ev); - }); - } else if (JSON.stringify(filter) == JSON.stringify(this.filter)) { - console.info('no change'); - } else { - this.filter = filter; - // get filtered data - this.dataSource.data(this.filter).then((data) => { - // send event with new data - window.__data = data; - const ev = new CustomEvent('filterOut', { detail: { data, filter } }); - window.dispatchEvent(ev); - console.info('filterOut event: ', ev); - }); - } - } - - initialize(e) { - this.dataSource = e.detail.dataSource; - this.dataSource.data({}).then((z) => { - // send init event with data - const ev = new CustomEvent('initData', { detail: { data: z } }); - window.__data = z; - window.__baseData = z; - window.dispatchEvent(ev); - console.info('init event: ', ev); - }); - } -} - -export default DataManager; diff --git a/source/experimental/FieldSelectionPanel/Content/ContentPanel.js b/source/experimental/FieldSelectionPanel/Content/ContentPanel.js deleted file mode 100644 index 7d1ebff..0000000 --- a/source/experimental/FieldSelectionPanel/Content/ContentPanel.js +++ /dev/null @@ -1,30 +0,0 @@ -import React, { PureComponent } from 'react'; -import Item from './Item'; -import Footer from './Footer'; - -class ContentPanel extends PureComponent { - constructor(props) { - super(props); - } - - render() { - const items = this.props.itemsExpanded ? this.props.items : this.props.items.slice(0, 4); - - let __content = items.map((item, key) => ( - - )); - - if (items.length == 0) __content = No Matching Items; - return ( -
- {__content} -
-
- ); - } -} -export default ContentPanel; diff --git a/source/experimental/FieldSelectionPanel/Content/Footer.js b/source/experimental/FieldSelectionPanel/Content/Footer.js deleted file mode 100644 index 5a5df21..0000000 --- a/source/experimental/FieldSelectionPanel/Content/Footer.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -function Footer(props) { - if (props.num <= 4) return null; - // else { - const _text = props.expanded ? 'Less...' : `${props.num - 4} More...`; - return ( -
- {_text} -
- ); - // } -} -export default Footer; diff --git a/source/experimental/FieldSelectionPanel/Content/Item.js b/source/experimental/FieldSelectionPanel/Content/Item.js deleted file mode 100644 index db1ccce..0000000 --- a/source/experimental/FieldSelectionPanel/Content/Item.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Component } from 'react'; - -function Item(props) { -// const _id = `${props.name}`; - return ( - - ); -} - -export default Item; diff --git a/source/experimental/FieldSelectionPanel/FieldSelectionPanel.css b/source/experimental/FieldSelectionPanel/FieldSelectionPanel.css deleted file mode 100644 index f8b2e0d..0000000 --- a/source/experimental/FieldSelectionPanel/FieldSelectionPanel.css +++ /dev/null @@ -1,277 +0,0 @@ -/*facet-terms-aggregation.css*/ - - -.field-selection-panel { - z-index: 100; - position: relative; - border-bottom: 2px solid #84817a; -} - -.flex-row { - display: flex; - flex-direction: row; - box-sizing: border-box; - position: relative; - outline: none; -} - -.flex-column { - display: flex; - flex-direction: column; - box-sizing: border-box; - position: relative; - outline: none; -} - -/* Header START */ -.field-selection-panel .header { - display: flex; - flex-direction: row; - box-sizing: border-box; - position: relative; - outline: none; - -webkit-box-align: center; - -webkit-box-pack: justify; - color: #005083; - font-size: 1.7rem; - font-weight: bold; - /* cursor: pointer; */ - align-items: center; - justify-content: space-between; - padding: 1rem 0; -} - -.field-selection-panel .header .title { - cursor: pointer; -} - -.field-selection-panel .header span i { - width:1.5rem; - text-align:center; -} - -.field-selection-panel .header div.tail i, -.field-selection-panel .operation i { - text-align: center; - padding: .3rem; -} - - -.field-selection-panel .header i.fa.fa-search, -.field-selection-panel .header i.fa.fa-ellipsis-h, -.field-selection-panel .header i.fa.fa-undo, -.field-selection-panel .header i.fa.fa-times, -.field-selection-panel .search i.fa.fa-times, -.field-selection-panel .operation i.fa { - color:#6B6260; -} - -.field-selection-panel .header i.fa.fa-search:hover, -.field-selection-panel .header i.fa.fa-ellipsis-h:hover, -.field-selection-panel .header i.fa.fa-undo:hover, -.field-selection-panel .header i.fa.fa-times:hover, -.field-selection-panel .search i.fa.fa-times:hover, -.field-selection-panel .operation i.fa:hover { - cursor: pointer; - color:#337ab7; -} - -.field-selection-panel .header i.fa.fa-ellipsis-h:hover::before, -.field-selection-panel .header i.fa.fa-search:hover::before, -.field-selection-panel .header i.fa.fa-undo:hover::before, -.field-selection-panel .header i.fa.fa-times:hover::before, -.field-selection-panel .search i.fa.fa-times:hover::before, -.field-selection-panel .operation i.fa:hover::before { - text-shadow: rgba(144, 144, 144, 0.62) 0px 0px 7px; -} -/* Header END */ - -/* search panel START */ -.field-selection-panel .search, -.field-selection-panel .operation { - padding: 0 0 .5rem 1.5rem; - display: flex; - flex-direction: row; - box-sizing: border-box; - position: relative; - outline: none; -} -.field-selection-panel .operation { - font-size: 1.3rem; - line-height: 1.5; -} -.field-selection-panel .operation i.fa { - font-size: 1.5rem; -} - -.field-selection-panel .search .search-input { - width: 100%; - min-width: 0px; - height: 2.5rem; - padding: .6rem 1.2rem; - font-size: 1.4rem; - line-height: 1.42857; - color: #555555; - background-color: #FFFFFF; - border: 1px solid #CCCCCC; - box-shadow: rgba(0, 0, 0, 0.075) 0px 1px 1px inset; - transition: border-color 0.15s ease-in-out 0s, box-shadow 0.15s ease-in-out 0s; - border-radius: 4px; - margin-bottom: 6px; - outline: none; -} -.field-selection-panel .search .search-input:focus, -.field-selection-panel .search .search-input:active { - border: 2px solid #337ab7; -} - -.field-selection-panel .search .close { - font-size: 1.5rem; - position: absolute; - right: 0; - padding: .6rem; - transition: all 0.3s ease 0s; - outline: 0; - cursor: pointer; -} -/* search panel END */ - - -/* operation panel START */ -.field-selection-panel .operation span { - font-weight: bold; -} -.field-selection-panel .operation label { - cursor: pointer; -} -/* operation panel END */ - - - -.field-selection-panel .footer a { - float: right; - text-decoration: none; - font-size: 1.5rem; - color:#337ab7; - cursor: pointer; -} - -.field-selection-panel .footer a:hover, -.field-selection-panel .footer a:focus { - color: #23527c; -} - -.facet-header .operators { - display: flex; - flex-direction: row; - box-sizing: border-box; - position: relative; - outline: none; - - /* */ - color: #6B6262; - line-height: 1.48px; - font-size: 1.2em; -} - -.terms-aggregation { - display: flex; - flex-direction: column; - box-sizing: border-box; - position: relative; - outline: none; - text-align: center; -} - - - - -.terms-aggregation span.tip { - font-size: 1.3rem; - color: #ff0000; - font-weight: bold; - padding: 0.5rem; -} - - -/* .terms-list { - display: flex; - flex-direction: column; - box-sizing: border-box; - position: relative; - outline: none; -} */ - -.term { - font-size: 1.4rem; - display: flex; - flex-direction: row; - box-sizing: border-box; - position: relative; - outline: none; - /* style */ - padding: 0.3rem 0px; -} - -.term-head { - color: rgb(36, 36, 36); - text-decoration: none; - -webkit-box-align: center; - min-width: 0px; - display: flex; - align-items: center; - margin-bottom: 0.5rem; -} -.term-head label { - white-space: nowrap; - text-overflow: ellipsis; - min-width: 0; - overflow: hidden; - padding: 0 0.25rem; - margin-left: 0.3rem; - vertical-align: middle; -} -.term-tail { - margin-left: auto; - background-color: #5B5151; - font-size: 1rem; - color: white; - padding: 0.2em 0.6em 0.3em; - border-radius: 0.25em; - font-weight: bold; - height: 2rem; - cursor: pointer; -} - -.term-list-toggle { - display: flex; - flex-direction: row; - box-sizing: border-box; - position: relative; - outline: none; - - padding: 0.5rem; -} - -.term-toggle-text { - margin-left: auto; - color: #6B6245; - font-size: 1.2rem; - cursor: pointer; -} - -.content-footer a { - float: right; - text-decoration: none; - font-size: 1.5rem; - color:#337ab7; - cursor: pointer; - } - .content-footer a:hover, - .content-footer a:focus { - color: #23527c; - } - - - - diff --git a/source/experimental/FieldSelectionPanel/FieldSelectionPanel.js b/source/experimental/FieldSelectionPanel/FieldSelectionPanel.js deleted file mode 100644 index 27ba182..0000000 --- a/source/experimental/FieldSelectionPanel/FieldSelectionPanel.js +++ /dev/null @@ -1,171 +0,0 @@ -import React, { PureComponent } from 'react'; - -import PropTypes from 'prop-types'; -import Header from './Header/Header'; -import SearchPanel from './SearchPanel'; -import OperationPanel from './OperationPanel'; -import ContentPanel from './Content/ContentPanel'; - -const findMatches = (wordToMatch, list) => list.filter((item) => { - const itemName = item.name; - // here we need to figure out if word matches what was searched - const regex = new RegExp(wordToMatch, 'gi'); - return itemName.match(regex); -}); - -class FieldeSlectionPanel extends PureComponent { - constructor(props) { - super(props); - this.state = { - // for async call - hasError: null, - isLoaded: true, // - - /* UI status */ - panelExpanded: true, // Is the entire items panel expanded - itemsExpanded: false, // - isShowSearch: false, - isShowOperation: false, - /* sort status */ - - sort: 'alpha', // none, alpha, num, - isAscending: true, // asc - true, desc - false - searchText: '', - items: props.items.map((elt) => { - elt.checked = false; - return elt; - }), - /* filter status */ - // selectedChanged: this.props.selectedChanged, - // name: this.props.name || "", - // title: this.props.title || "", - // items: this.props.items || [] - }; - - // bind this - this.togglePanelHandler = this.togglePanelHandler.bind(this); - this.toggleItemsListHandler = this.toggleItemsListHandler.bind(this); - this.toggleSearch = this.toggleSearch.bind(this); - this.toggleOperation = this.toggleOperation.bind(this); - - this.setSortBy = this.setSortBy.bind(this); - this.toggleOrder = this.toggleOrder.bind(this); - - this.sort = this.sort.bind(this); - this.searchChangedHandler = this.searchChangedHandler.bind(this); - this.selectedChangeHandler = this.selectedChangeHandler.bind(this); - this.clearSelectedHandler = this.clearSelectedHandler.bind(this); - } - - setSortBy(value) { - this.setState({ sort: value }); - } - - togglePanelHandler() { - this.setState((prevState) => ({ panelExpanded: !prevState.panelExpanded })); - } - - toggleItemsListHandler() { - this.setState((prevState) => ({ itemsExpanded: !prevState.itemsExpanded })); - } - - toggleSearch() { - this.setState((prevState) => ({ isShowSearch: !prevState.isShowSearch })); - } - - toggleOperation() { - this.setState((prevState) => ({ isShowOperation: !prevState.isShowOperation })); - } - - toggleOrder() { - this.setState((prevState) => ({ isAscending: !prevState.isAscending })); - } - - sort(a, b) { - const v1 = this.state.sort === 'alpha' ? a.name : a.sum; - const v2 = this.state.sort === 'alpha' ? b.name : b.sum; - - if (v1 > v2) { - return this.state.isAscending ? 1 : -1; - } - - if (v1 < v2) { - return this.state.isAscending ? -1 : 1; - } - - return 0; - } - - searchChangedHandler(text) { - this.setState({ searchText: text }); - } - - selectedChangeHandler(e) { - const item = e.target; - this.state.items.find((i) => i.name === item.value).checked = item.checked; - this.setState((prevState) => ({ items: [...prevState.items] })); - } - - clearSelectedHandler() { - this.state.items.forEach((item) => { - item.checked = false; - }); - this.setState((prevState) => ({ items: [...prevState.items] })); - } - - render() { - // - const { hasError, isLoaded } = this.state; - - const items = this.state.searchText - ? findMatches(this.state.searchText, this.state.items.sort(this.sort)) - : this.state.items.sort(this.sort); - - // has a error - if (hasError) { - return
Something is wrong!!!
; - } - if (!isLoaded) { - return
Loading...
; - } - return ( -
-
- {this.state.isShowOperation && ( - - )} - {this.state.isShowSearch && ( - - )} - {this.state.panelExpanded && ( - - )} -
- ); - } -} - -export default FieldeSlectionPanel; - -FieldeSlectionPanel.propTypes = { - title: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.shape({})).isRequired, -}; diff --git a/source/experimental/FieldSelectionPanel/Header/Header.js b/source/experimental/FieldSelectionPanel/Header/Header.js deleted file mode 100644 index a1804d6..0000000 --- a/source/experimental/FieldSelectionPanel/Header/Header.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import Title from './Title'; -import Tail from './Tail'; - -function Head(props) { - return ( -
- - <Tail - searchClicked={props.searchClicked} - operationClicked={props.operationClicked} - clearClicked={props.clearClicked} - items={props.items} - /> - </div> - ); -} - -export default Head; diff --git a/source/experimental/FieldSelectionPanel/Header/Tail.js b/source/experimental/FieldSelectionPanel/Header/Tail.js deleted file mode 100644 index 92a7332..0000000 --- a/source/experimental/FieldSelectionPanel/Header/Tail.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -function Tail(props) { - const result = props.items.filter((item) => item.checked == true); - return ( - <div className="tail"> - {props.operationClicked && <i className="fa fa-ellipsis-h" onClick={props.operationClicked} />} - {props.searchClicked && <i className="fa fa-search" onClick={props.searchClicked} />} - {result.length > 0 && props.clearClicked && <i className="fa fa-undo" onClick={props.clearClicked} />} - </div> - ); -} - -export default Tail; diff --git a/source/experimental/FieldSelectionPanel/Header/Title.js b/source/experimental/FieldSelectionPanel/Header/Title.js deleted file mode 100644 index 00bd4c5..0000000 --- a/source/experimental/FieldSelectionPanel/Header/Title.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; - -function Title(props) { - // _icon - const _icon = props.expanded ? 'fa fa-angle-down' : 'fa fa-angle-right'; - // style={'cursor: pointer;'} - // style={'width:1.5rem;text-align:center;'} - return ( - <span onClick={props.clicked}> - <i className={_icon} /> - {props.title} - </span> - ); -} - -export default Title; diff --git a/source/experimental/FieldSelectionPanel/OperationPanel.js b/source/experimental/FieldSelectionPanel/OperationPanel.js deleted file mode 100644 index 286a3e4..0000000 --- a/source/experimental/FieldSelectionPanel/OperationPanel.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; - -function OperationPanel(props) { - const selection = 'alpha'; - // fa-sort-up - // fa-sort-down - - // fa-sort-alpha-up - // fa-sort-alpha-down - - // fa-sort-numeric-up - // fa-sort-numeric-down - // <i className='fa fa-sort-down'></i> - const changedHandler = (e) => { - props.sortChanged(e.currentTarget.value); - }; - - const _icon = props.isAscending ? 'fa fa-sort-up' : 'fa fa-sort-down'; - return ( - <div className="operation"> - <span>Sort By:</span> -   - <label htmlFor="sortAlphaChecked"> - Alpha - <input - id="sortAlphaChecked" - name="radioGroupCollectionSort" - type="radio" - value="alpha" - defaultChecked={selection === 'alpha'} - onChange={changedHandler} - /> - </label> -   - <label htmlFor="sortNumChecked"> - Num - <input - id="sortNumChecked" - name="radioGroupCollectionSort" - type="radio" - value="num" - defaultChecked={selection === 'num'} - onChange={changedHandler} - /> - </label> -   - <i className={_icon} onClick={props.orderClicked} /> - </div> - ); -} -export default OperationPanel; diff --git a/source/experimental/FieldSelectionPanel/SearchPanel.js b/source/experimental/FieldSelectionPanel/SearchPanel.js deleted file mode 100644 index 65cc86f..0000000 --- a/source/experimental/FieldSelectionPanel/SearchPanel.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -function SearchPanel(props) { - const changedHandler = (e) => { - const text = e.currentTarget.value; - props.changed(text); - }; - return ( - <div className="search"> - <input - type="text" - className="search-input" - onChange={changedHandler} - onKeyUp={changedHandler} - value={props.text} - /> - {props.text && ( - <i - className="fa fa-times close" - onClick={() => props.changed('')} - role="button" - onFocus - /> - )} - </div> - ); -} -export default SearchPanel; - -SearchPanel.propTypes = { - text: PropTypes.string.isRequired, - changed: PropTypes.func.isRequired, -}; diff --git a/source/experimental/ImageGrid.js b/source/experimental/ImageGrid.js deleted file mode 100644 index d731ffb..0000000 --- a/source/experimental/ImageGrid.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; -import BaseVisualization from './BaseVisualization'; -import ImageGridItem from './ImageGridItem'; - -// should only have to worry about rendering -class ImageGrid extends BaseVisualization { - constructor(props, ctx) { - super(props, ctx); - this.onPageButton = this.onPageButton.bind(this); - this.state.currentData = {}; - this.state.page = 0; - this.state.perPage = props.perPage || 10; - this.width = this.props.w * 100 || 100; - this.height = this.props.h * 100 || 100; - this.imSize = this.props.imSize || 4; - this.style = { width: this.width, height: this.height }; - } - - onPageButton(e) { - if (e.target && e.target.innerText) { - const next_page = parseInt(e.target.innerText); - this.setState((prevState, props) => { - prevState.page = next_page; - }); - this.forceUpdate(); - } - } - - render() { - const images = []; - const thispage = this.state.filteredData.slice( - this.state.page * this.state.perPage, - (this.state.page + 1) * this.state.perPage, - ); - for (const i in thispage) { - const v = thispage[i]; - const im_url = v[this.props.urlField] || 'https://ppaas.herokuapp.com/partyparrot'; - const im_label = v[this.props.labelField] || ''; - if (im_url) { - images.push( - <ImageGridItem - url={im_url} - label={im_label} - id={`${this.id}-im-${i}`} - key={`${this.id}-im-${i}`} - w={this.imSize} - h={this.imSize} - />, - ); - } - } - - const pageBtns = []; - // min page - const _minp = Math.max(this.state.page - 5, 0); - const _maxp = Math.min( - this.state.page + 5, - this.state.filteredData.length / this.state.perPage, - ); - for (let j = _minp; j < _maxp; j++) { - if (j == this.state.page) { - pageBtns.push( - <li className="page-item" key={`${this.id}-pg-${j}`} id={`${this.id}-pg-${j}`}> - {' '} - <a className="page-link" value={j} onClick={this.onPageButton}> - <b>{j}</b> - </a> - </li>, - ); - } else { - pageBtns.push( - <li className="page-item" key={`${this.id}-pg-${j}`} id={`${this.id}-pg-${j}`}> - {' '} - <a className="page-link" value={j} onClick={this.onPageButton}> - {j} - </a> - </li>, - ); - } - } - if (this.state.ready) { - return ( - <div id={this.id} key={this.id} style={this.style}> - {images} - <ul className="pagination" id={`${this.id}-pages`}> - {pageBtns} - </ul> - </div> - ); - } - return ( - <div id={this.id} key={this.id} className="vis-loading" style={this.style}> - <p> waiting...</p> - </div> - ); - } -} - -export default ImageGrid; diff --git a/source/experimental/ImageGridItem.js b/source/experimental/ImageGridItem.js deleted file mode 100644 index 5a65c2a..0000000 --- a/source/experimental/ImageGridItem.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -// should only have to worry about rendering -class ImageGridItem extends React.Component { - constructor(props, ctx) { - super(props, ctx); - this.width = this.props.w * 10 || 10; - this.height = this.props.h * 10 || 10; - this.style = { width: this.width, height: this.height }; - // this.props.img is the url - // this.props.label is the label text - } - - render() { - return ( - <div id={this.props.id}> - <img src={this.props.url} style={this.style} /> - <span>{this.props.label}</span> - </div> - ); - } -} - -export default ImageGridItem; diff --git a/source/experimental/LoadData.js b/source/experimental/LoadData.js deleted file mode 100644 index 1b0141e..0000000 --- a/source/experimental/LoadData.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; - -class LoadData extends React.Component { - constructor(props) { - super(props); - this.state = { - error: null, - isLoaded: false, - items: [], - }; - } - - componentDidMount() { - fetch('http://144.30.2.201/locations', { - mode: 'cors', - }) - .then((res) => res.json()) - .then( - (result) => { - this.setState({ - isLoaded: true, - items: result.length, - }); - }, - // Note: it's important to handle errors here - // instead of a catch() block so that we don't swallow - // exceptions from actual bugs in components. - (error) => { - this.setState({ - isLoaded: true, - error, - }); - }, - ); - } - - render() { - const { error, isLoaded, items } = this.state; - if (error) { - return ( - <div> - Error: - {error.message} - </div> - ); - } - if (!isLoaded) { - return <div>Loading...</div>; - } - return <div>{items}</div>; - } -} - -export default LoadData; diff --git a/source/experimental/RestDataSource.js b/source/experimental/RestDataSource.js deleted file mode 100644 index b0a748d..0000000 --- a/source/experimental/RestDataSource.js +++ /dev/null @@ -1,19 +0,0 @@ -import filterTools from './filterTools.js'; - -class RestDataSource { - constructor(url) { - this._records = fetch(url).then((x) => x.json()); - - const ev = new CustomEvent('dataSourceReady', { detail: { dataSource: this } }); - window.dispatchEvent(ev); - console.info('dataSourceReady event: ', ev); - } - - data(filter) { - return new Promise((resolve, reject) => { - this._records.then((data) => { resolve(filterTools.filterData(data, filter)); }); - }); - } -} - -export default RestDataSource; diff --git a/source/experimental/filterTools.js b/source/experimental/filterTools.js deleted file mode 100644 index 8024e77..0000000 --- a/source/experimental/filterTools.js +++ /dev/null @@ -1,50 +0,0 @@ -function filterData(data, rules) { - return data.filter((y) => { - for (const rule in rules) { - var test_val; - if (rule === '__all' || rule === '__ALL') { - // we only care about VALUES, not keys - test_val = JSON.stringify(Object.values(y)); - } else { - test_val = y[rule]; - } - let broken = false; - const oprs = Object.keys(rules[rule]); - if (!broken && oprs.includes('less')) { - broken = broken || test_val >= rules[rule].less; - } - if (!broken && oprs.includes('greater')) { - broken = broken || test_val <= rules[rule].greater; - } - if (!broken && oprs.includes('match')) { - broken = broken || test_val != rules[rule].match; - } - if (!broken && oprs.includes('regex')) { - const re = new RegExp(rules[rule].regex); - broken = broken || !re.test(test_val); - } - if (broken) { - return false; - } - } - // all rules met - return true; - }); -} - -function filterMerge(filter, additions, mergeMethod) { - filter = filter || {}; - additions = additions || {}; - if (additions.hasOwnProperty('__RESET') || filter.hasOwnProperty('__RESET')) { - return { __RESET: 'true' }; - } - const outFilter = JSON.parse(JSON.stringify(filter)); - for (const rule in additions) { - outFilter[rule] = additions[rule]; - } - return outFilter; -} - -const filterTools = { filterData, filterMerge }; - -export default filterTools; diff --git a/source/experimental/vegaSpecs.js b/source/experimental/vegaSpecs.js deleted file mode 100644 index 39e4a3e..0000000 --- a/source/experimental/vegaSpecs.js +++ /dev/null @@ -1,175 +0,0 @@ -const vegaSpecs = {}; -vegaSpecs.dotPlotSpec = JSON.stringify({ - $schema: 'https://vega.github.io/schema/vega-lite/v3.json', - mark: 'tick', - selection: { - brush: { - encodings: ['x'], - type: 'interval', - }, - }, - encoding: { - x: { field: 'i0', type: 'quantitative' }, - }, -}); - -vegaSpecs.barChartSpec = JSON.stringify({ - $schema: 'https://vega.github.io/schema/vega-lite/v3.json', - mark: 'bar', - encoding: { - y: { - field: 'i0', - type: 'ordinal', - }, - x: { - aggregate: 'sum', - field: 'i1', - type: 'quantitative', - }, - }, -}); - -vegaSpecs.histSpec = JSON.stringify({ - $schema: 'https://vega.github.io/schema/vega-lite/v3.json', - mark: 'bar', - selection: { - brush: { - encodings: ['x'], - type: 'interval', - }, - }, - encoding: { - x: { - bin: true, - field: 'i0', - type: 'quantitative', - }, - y: { - aggregate: 'count', - type: 'quantitative', - }, - }, -}); - -vegaSpecs.scatterSpec = JSON.stringify({ - $schema: 'https://vega.github.io/schema/vega-lite/v3.json', - mark: 'bar', - selection: { - brush: { - encodings: ['x', 'y'], - type: 'interval', - }, - }, - mark: 'point', - encoding: { - x: { field: 'i0', type: 'quantitative' }, - y: { field: 'i1', type: 'quantitative' }, - color: { field: 'c0', type: 'nominal' }, - shape: { field: 'c1', type: 'nominal' }, - }, -}); - -vegaSpecs.parallelCoordSpec = JSON.stringify({ - $schema: 'https://vega.github.io/schema/vega-lite/v3.json', - height: 300, - transform: [ - { window: [{ op: 'count', as: 'index' }] }, - { fold: ['i1', 'i0'] }, - { - joinaggregate: [ - { op: 'min', field: 'value', as: 'min' }, - { op: 'max', field: 'value', as: 'max' }, - ], - groupby: ['key'], - }, - { - calculate: '(datum.value - datum.min) / (datum.max-datum.min)', - as: 'norm_val', - }, - { - calculate: '(datum.min + datum.max) / 2', - as: 'mi1', - }, - ], - layer: [{ - mark: { type: 'rule', color: '#ccc', tooltip: null }, - encoding: { - detail: { aggregate: 'count', type: 'quantitative' }, - x: { type: 'nominal', field: 'key' }, - }, - }, { - mark: 'line', - encoding: { - color: { type: 'nominal', field: 'f0' }, - detail: { type: 'nominal', field: 'index' }, - opacity: { value: 0.3 }, - x: { type: 'nominal', field: 'key' }, - y: { type: 'quantitative', field: 'norm_val', axis: null }, - tooltip: [{ - field: 'i1', - }, { - field: 'i0', - }], - }, - }, { - encoding: { - x: { type: 'nominal', field: 'key' }, - y: { value: 0 }, - }, - layer: [{ - mark: { type: 'text', style: 'label' }, - encoding: { - text: { aggregate: 'max', field: 'max', type: 'quantitative' }, - }, - }, { - mark: { - type: 'tick', style: 'tick', size: 8, color: '#ccc', - }, - }], - }, { - - encoding: { - x: { type: 'nominal', field: 'key' }, - y: { value: 150 }, - }, - layer: [{ - mark: { type: 'text', style: 'label' }, - encoding: { - text: { aggregate: 'min', field: 'mi1', type: 'quantitative' }, - }, - }, { - mark: { - type: 'tick', style: 'tick', size: 8, color: '#ccc', - }, - }], - }, { - encoding: { - x: { type: 'nominal', field: 'key' }, - y: { value: 300 }, - }, - layer: [{ - mark: { type: 'text', style: 'label' }, - encoding: { - text: { aggregate: 'min', field: 'min', type: 'quantitative' }, - }, - }, { - mark: { - type: 'tick', style: 'tick', size: 8, color: '#ccc', - }, - }], - }], - config: { - axisX: { - domain: false, labelAngle: 0, tickColor: '#ccc', title: null, - }, - view: { stroke: null }, - style: { - label: { - baseline: 'mi1dle', align: 'right', dx: -5, tooltip: null, - }, - tick: { orient: 'horizontal', tooltip: null }, - }, - }, -}); - -export default vegaSpecs; diff --git a/source/experimental/xFilterTools.js b/source/experimental/xFilterTools.js deleted file mode 100644 index b645645..0000000 --- a/source/experimental/xFilterTools.js +++ /dev/null @@ -1,44 +0,0 @@ -// in this mode greater and less should always be used together. -function filterData(dataObj, rules) { - if (JSON.stringify(rules) == '{}') { - return dataObj.xf.all(); - } - for (const rule in rules) { - if (dataObj.dims[rule]) { - const oprs = Object.keys(rules[rule]); - if (oprs.includes('clear')) { - dataObj.dims[rule].filter(null); - } - if (oprs.includes('match')) { - dataObj.dims[rule].filter(rules[rule].match); - } else if (oprs.includes('less') && oprs.includes('greater')) { - dataObj.dims[rule].filter([rules[rule].greater, rules[rule].less]); - } else if (oprs.includes('regex')) { - dataObj.dims[rule].filterFunction((y) => { - const re = new RegExp(rules[rule].regex); - return re.test(y); - }); - } - } else { - console.warn(`no dimension matching ${rule}`); - } - } - return dataObj.xf.allFiltered() || []; -} - -function filterMerge(filter, additions, mergeMethod) { - filter = filter || {}; - additions = additions || {}; - if (additions.hasOwnProperty('__RESET') || filter.hasOwnProperty('__RESET')) { - return { __RESET: 'true' }; - } - const outFilter = JSON.parse(JSON.stringify(filter)); - for (const rule in additions) { - outFilter[rule] = additions[rule]; - } - return outFilter; -} - -const filterTools = { filterData, filterMerge }; - -export default filterTools; diff --git a/source/experimental/xfRestDataSource.js b/source/experimental/xfRestDataSource.js deleted file mode 100644 index be9e09a..0000000 --- a/source/experimental/xfRestDataSource.js +++ /dev/null @@ -1,28 +0,0 @@ -import crossfilter from 'crossfilter2'; -import filterTools from './xFilterTools.js'; - -class RestDataSource { - constructor(url) { - this._records = fetch(url).then((x) => x.json()).then((x) => { - const xf = crossfilter(x); - const dims = {}; - for (const i in Object.keys(x[0])) { - var k = Object.keys(x[0])[i]; - dims[k] = xf.dimension((d) => d[k]); - } - dims.__ALL = xf.dimension((d) => JSON.stringify(d)); - return { xf, raw: x, dims }; - }); - const ev = new CustomEvent('dataSourceReady', { detail: { dataSource: this } }); - window.dispatchEvent(ev); - console.info('dataSourceReady event: ', ev); - } - - data(filter) { - return new Promise((resolve, reject) => { - this._records.then((data) => { resolve(filterTools.filterData(data, filter)); }); - }); - } -} - -export default RestDataSource; diff --git a/source/hooks/useFetch.js b/source/hooks/useFetch.js index 0676aa4..8b7106f 100644 --- a/source/hooks/useFetch.js +++ b/source/hooks/useFetch.js @@ -4,7 +4,7 @@ import * as d3 from 'd3'; function isNumeric(str) { if (typeof str !== 'string') return false; // we only process strings! return ( - !isNaN(str) + !Number.isNaN(str) // use type coercion to parse the _entirety_ of the string // (`parseFloat` alone does not do this)... && !Number.isNaN(parseFloat(str)) diff --git a/source/index.js b/source/index.js index c59cb26..d0cd877 100644 --- a/source/index.js +++ b/source/index.js @@ -1,6 +1,6 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import Application from './Application.js'; +import Application from './Application'; // style import './components/VisualTools/Chart/style.css';