mobx#toJS JavaScript Examples

The following examples show how to use mobx#toJS. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: SyncStore.js    From lens-extension-cc with MIT License 6 votes vote down vote up
toJSON() {
    // throw-away: just to get keys we care about on this
    const defaults = SyncStore.getDefaults();

    const observableThis = Object.keys(defaults).reduce((obj, key) => {
      obj[key] = this[key];
      return obj;
    }, {});

    // return a deep-clone that is no longer observable
    // NOTE: it's not "pure JSON" void of Mobx proxy objects, however; the sole purpose of
    //  this is to let Lens serialize the store to the related JSON file on disk, however
    //  it does that
    return toJS(observableThis);
  }
Example #2
Source File: CloudStore.js    From lens-extension-cc with MIT License 6 votes vote down vote up
toJSON() {
    // throw-away: just to get keys we care about on this
    const defaults = CloudStore.getDefaults();

    const observableThis = Object.keys(defaults).reduce((obj, key) => {
      if (key === 'clouds') {
        // store from map of cloudUrl to Cloud instance -> into a map of cloudUrl
        //  to JSON object
        obj[key] = Object.keys(this[key]).reduce((jsonMap, cloudUrl) => {
          jsonMap[cloudUrl] = this[key][cloudUrl].toJSON();
          return jsonMap;
        }, {});
      } else {
        obj[key] = this[key];
      }
      return obj;
    }, {});

    // return a deep-clone that is no longer observable
    // NOTE: it's not "pure JSON" void of Mobx proxy objects, however; the sole purpose of
    //  this is to let Lens serialize the store to the related JSON file on disk, however
    //  it does that
    return toJS(observableThis);
  }
Example #3
Source File: capture-central-live-events.js    From content-components with Apache License 2.0 6 votes vote down vote up
observeQueryParams() {
		observe(
			rootStore.routingStore,
			'queryParams',
			async change => {
				if (this.loading) {
					return;
				}

				const { searchQuery = '' } = toJS(change.newValue);

				if (rootStore.routingStore.getPage() === pageNames.manageLiveEvents) {
					await this.reload({ searchQuery });
				}
			}
		);
	}
Example #4
Source File: content-filter-dropdown.js    From content-components with Apache License 2.0 6 votes vote down vote up
observeQueryParams() {
		observe(
			rootStore.routingStore,
			'queryParams',
			change => {
				if (this.loading) {
					return;
				}

				const { dateModified = '', dateCreated = '' } =
					toJS(change.newValue);

				if (dateModified === this.selectedFilterParams.dateModified &&
					dateCreated === this.selectedFilterParams.dateCreated
				) {
					return;
				}

				this.selectedFilterParams = { dateModified, dateCreated };
			}
		);
	}
Example #5
Source File: content-list.js    From content-components with Apache License 2.0 6 votes vote down vote up
observeQueryParams() {
		observe(
			rootStore.routingStore,
			'queryParams',
			change => {
				if (this.loading) {
					return;
				}

				const {
					searchQuery: updatedSearchQuery = '',
					sortQuery: updatedSortQuery = 'updatedAt:desc',
					dateCreated: updatedDateCreated = '',
					dateModified: updatedDateModified = ''
				} = toJS(change.newValue);

				const { searchQuery, sortQuery, dateCreated, dateModified } =
					this.queryParams;

				if (updatedSearchQuery === searchQuery && updatedSortQuery === sortQuery &&
					updatedDateCreated === dateCreated && updatedDateModified === dateModified
				) {
					return;
				}

				this.queryParams = {
					searchQuery: updatedSearchQuery,
					sortQuery: updatedSortQuery,
					dateCreated: updatedDateCreated,
					dateModified: updatedDateModified
				};
				this.reloadPage();
			}
		);
	}
Example #6
Source File: content-list.js    From content-components with Apache License 2.0 6 votes vote down vote up
observeSuccessfulUpload() {
		observe(
			this.uploader,
			'successfulUpload',
			async change => {
				if (change.newValue &&
					change.newValue.content &&
					!this.areAnyFiltersActive()) {
					return this.addNewItemIntoContentItems(toJS(change.newValue));
				}
			}
		);
	}
Example #7
Source File: dataStore.js    From covidcg with MIT License 6 votes vote down vote up
downloadAggLocationGroupDate() {
    let locationData;
    if (rootStoreInstance.configStore.groupKey === GROUP_MUTATION) {
      locationData = toJS(this.aggLocationSingleMutationDate);
      // Get mutation data
      locationData.forEach((record) => {
        let mutation = rootStoreInstance.mutationDataStore.intToMutation(
          rootStoreInstance.configStore.dnaOrAa,
          rootStoreInstance.configStore.coordinateMode,
          record.group_id
        );
        record.group_name = mutation.name;
        record.group = mutation.mutation_str;
      });
    } else {
      locationData = toJS(this.aggLocationGroupDate);
      locationData.forEach((record) => {
        record.group_name = record.group_id;
        record.group = record.group_id;
      });
    }
    let csvString = `location,collection_date,${rootStoreInstance.configStore.getGroupLabel()},${rootStoreInstance.configStore.getGroupLabel()} Name,count\n`;
    locationData.forEach((row) => {
      csvString += `${row.location},${intToISO(row.collection_date)},${
        row.group
      },${row.group_name},${row.counts}\n`;
    });
    const blob = new Blob([csvString]);
    const url = URL.createObjectURL(blob);
    downloadBlobURL(url, 'data_agg_location_group_date.csv');
  }
Example #8
Source File: configStore.js    From covidcg with MIT License 6 votes vote down vote up
getSelectedMetadataFields() {
    const selectedMetadataFields = toJS(this.selectedMetadataFields);
    Object.keys(selectedMetadataFields).forEach((metadataField) => {
      selectedMetadataFields[metadataField] = selectedMetadataFields[
        metadataField
      ].map((item) => {
        return parseInt(item.value);
      });
    });
    return selectedMetadataFields;
  }
Example #9
Source File: content-filter-dropdown.js    From content-components with Apache License 2.0 6 votes vote down vote up
observeQueryParams() {
		observe(
			rootStore.routingStore,
			'queryParams',
			change => {
				if (this.loading) {
					return;
				}

				const { contentType = '', dateModified = '', dateCreated = '' } =
					toJS(change.newValue);

				if (contentType === this.selectedFilterParams.contentType &&
					dateModified === this.selectedFilterParams.dateModified &&
					dateCreated === this.selectedFilterParams.dateCreated
				) {
					return;
				}

				this.selectedFilterParams = { contentType, dateModified, dateCreated };
			}
		);
	}
Example #10
Source File: content-list.js    From content-components with Apache License 2.0 6 votes vote down vote up
observeQueryParams() {
		observe(
			rootStore.routingStore,
			'queryParams',
			change => {
				if (this.loading) {
					return;
				}

				const {
					searchQuery: updatedSearchQuery = '',
					sortQuery: updatedSortQuery = 'updatedAt:desc',
					contentType: updatedContentType = '',
					dateCreated: updatedDateCreated = '',
					dateModified: updatedDateModified = ''
				} = toJS(change.newValue);

				const { searchQuery, sortQuery, contentType, dateCreated, dateModified } =
					this.queryParams;

				if (updatedSearchQuery === searchQuery && updatedSortQuery === sortQuery &&
					updatedContentType === contentType && updatedDateCreated === dateCreated &&
					updatedDateModified === dateModified
				) {
					return;
				}

				this.queryParams = {
					searchQuery: updatedSearchQuery,
					sortQuery: updatedSortQuery,
					contentType: updatedContentType,
					dateCreated: updatedDateCreated,
					dateModified: updatedDateModified
				};
				this.reloadPage();
			}
		);
	}
Example #11
Source File: content-list.js    From content-components with Apache License 2.0 6 votes vote down vote up
observeSuccessfulUpload() {
		observe(
			this.uploader,
			'successfulUpload',
			async change => {
				if (change.newValue &&
					change.newValue.content &&
					!this.areAnyFiltersActive()) {
					return this.addNewItemIntoContentItems(toJS(change.newValue));
				}
			}
		);
	}
Example #12
Source File: metadataStore.js    From covidcg with MIT License 5 votes vote down vote up
@action
  async fetchMetadataFields() {
    rootStoreInstance.UIStore.onMetadataFieldStarted();

    fetch(hostname + '/metadata', {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        start_date: toJS(rootStoreInstance.configStore.startDate),
        end_date: toJS(rootStoreInstance.configStore.endDate),
        subm_start_date: toJS(rootStoreInstance.configStore.submStartDate),
        subm_end_date: toJS(rootStoreInstance.configStore.submEndDate),
        ...rootStoreInstance.configStore.getSelectedLocations(),
        selected_metadata_fields:
          rootStoreInstance.configStore.getSelectedMetadataFields(),
      }),
    })
      .then((res) => {
        if (!res.ok) {
          throw res;
        }
        return res.json();
      })
      .then((metadataRecords) => {
        // Each row in this array of records is structured as:
        // { "field", "val_id", "count", "val_str" }
        metadataRecords.forEach((record) => {
          this.metadataMap.get(record.field).set(record.val_id, record.val_str);
          this.metadataCounts
            .get(record.field)
            .set(record.val_id, record.count);
        });

        rootStoreInstance.UIStore.onMetadataFieldFinished();
      })
      .catch((err) => {
        if (!(typeof err.text === 'function')) {
          console.error(err);
        } else {
          err.text().then((errMsg) => {
            console.error(errMsg);
          });
        }
        rootStoreInstance.UIStore.onMetadataFieldErr();
      });
  }
Example #13
Source File: dataStore.js    From covidcg with MIT License 5 votes vote down vote up
downloadVariantTable({ selectedFields, mutationFormat, selectedReference }) {
    rootStoreInstance.UIStore.onDownloadStarted();

    fetch(hostname + '/variant_table', {
      method: 'POST',
      headers: {
        Accept: 'application/octet-stream',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        group_key: toJS(rootStoreInstance.configStore.groupKey),
        dna_or_aa: toJS(rootStoreInstance.configStore.dnaOrAa),
        coordinate_mode: toJS(rootStoreInstance.configStore.coordinateMode),
        coordinate_ranges: rootStoreInstance.configStore.getCoordinateRanges(),
        selected_gene: toJS(rootStoreInstance.configStore.selectedGene).name,
        selected_protein: toJS(rootStoreInstance.configStore.selectedProtein)
          .name,
        ...rootStoreInstance.configStore.getSelectedLocations(),
        selected_reference: selectedReference,
        selected_group_fields: toJS(
          rootStoreInstance.configStore.selectedGroupFields
        ),
        selected_metadata_fields:
          rootStoreInstance.configStore.getSelectedMetadataFields(),
        start_date: toJS(rootStoreInstance.configStore.startDate),
        end_date: toJS(rootStoreInstance.configStore.endDate),
        subm_start_date: toJS(rootStoreInstance.configStore.submStartDate),
        subm_end_date: toJS(rootStoreInstance.configStore.submEndDate),
        // Pass an array of only the fields that were selected
        selected_fields: Object.keys(selectedFields).filter(
          (field) => selectedFields[field]
        ),
        mutation_format: mutationFormat,
      }),
    })
      .then((res) => {
        if (!res.ok) {
          throw res;
        }
        return res.blob();
      })
      .then((blob) => {
        const url = URL.createObjectURL(blob);
        downloadBlobURL(url, 'variant_table.xlsx');
        rootStoreInstance.UIStore.onDownloadFinished();
      })
      .catch((err) => {
        let prefix = 'Error downloading variant table';
        if (!(typeof err.text === 'function')) {
          console.error(prefix, err);
        } else {
          err.text().then((errMsg) => {
            console.error(prefix, errMsg);
          });
        }
        rootStoreInstance.UIStore.onDownloadErr();
      });
  }
Example #14
Source File: MainQna.js    From front-app with MIT License 5 votes vote down vote up
MainQna = observer(() => {
  const { chatStore, userStore } = useStores()
  const { chatList: storeChatList, qnaList, setQnaList } = chatStore
  const { socket } = userStore
  usePageTracking('qna')

  const removeMsg = async (id) => {
    const { isConfirmed } = await Swal.fire({
      title: '정말 삭제 하시겠습니까?',
      showCancelButton: true,
    })
    if (isConfirmed) socket.emit('delete message', { msgId: id })
  }

  // 웨비나 중간에 채팅 초기화 버튼을 누르면 qna 리스트는 초기화 안됨.
  useEffect(() => {
    getAllChat().then(({ chatList }) => {
      if (toJS(qnaList).length !== chatList.filter((chatData) => chatData.question === true).length) {
        // console.log('update qna', toJS(qnaList), chatList.filter((chatData) => chatData.question === true))
        setQnaList(chatList)
      }
    })
  }, [])

  return (
    <div className={'qnaContainer'}>
      {qnaList &&
        qnaList.map((qna, idx) => (
          <div key={`qna_${idx}`} className={'qnaBox'}>
            <div className={'qnaBox__header'}>
              {getTokenVerification().length > 0 && (
                <img
                  src={xIcon}
                  alt={'xIcon'}
                  onClick={() => {
                    removeMsg(qna.msg_id)
                  }}
                />
              )}
            </div>
            <div className={'qnaBox__contents'}>Q. {qna.text}</div>
          </div>
        ))}
    </div>
  )
})
Example #15
Source File: routing-store.test.js    From content-components with Apache License 2.0 5 votes vote down vote up
describe('Routing Store', () => {
	let store;

	beforeEach(() => {
		store = new RoutingStore();
	});

	it('routing ctx is saved', () => {
		const testCtx = {
			pathname: '/d2l/contentstore/main',
			querystring: ''
		};
		store.setRouteCtx(testCtx);

		assert.deepEqual(toJS(store.routeCtx), testCtx);
	});

	it('with only page', () => {
		store.setRouteCtx({
			pathname: '/d2l/contentstore/404',
			querystring: ''
		});

		assert.equal(store.page, '404');
		assert.equal(store.subView, '');
		assert.deepEqual(store.queryParams, {});
	});

	it('with query params', () => {
		store.setRouteCtx({
			pathname: '/d2l/contentstore/123/files',
			querystring: 'foo=bar&number=5'
		});

		assert.equal(store.page, '123');
		assert.equal(store.subView, 'files');
		assert.deepEqual(toJS(store.queryParams), { foo: 'bar', number: '5' });
	});

	it('without query params', () => {
		store.setRouteCtx({
			pathname: '/d2l/contentstore/manage/files',
			querystring: ''
		});

		assert.equal(store.page, 'manage');
		assert.equal(store.subView, 'files');
		assert.deepEqual(store.queryParams, {});
	});
});
Example #16
Source File: routing-store.js    From content-components with Apache License 2.0 5 votes vote down vote up
getQueryParams() {
		return toJS(this.queryParams);
	}
Example #17
Source File: uploader.js    From content-components with Apache License 2.0 5 votes vote down vote up
getUploads() {
		return toJS(this.uploads);
	}
Example #18
Source File: uploader.js    From content-components with Apache License 2.0 5 votes vote down vote up
getSuccessfulUpload() {
		return toJS(this.successfulUpload);
	}
Example #19
Source File: routing-store.js    From content-components with Apache License 2.0 5 votes vote down vote up
getQueryParams() {
		return toJS(this.queryParams);
	}
Example #20
Source File: permission-store.js    From content-components with Apache License 2.0 5 votes vote down vote up
getPermissions() {
		return toJS(this.permissions);
	}
Example #21
Source File: LegendContainer.js    From covidcg with MIT License 4 votes vote down vote up
LegendContainer = observer(() => {
  const { dataStore, UIStore, configStore, groupDataStore, mutationDataStore } =
    useStores();

  const [state, setState] = useState({
    legendItems: [],
    sortColumn: COLUMN_NAMES.COUNTS,
    sortDir: SORT_DIRECTIONS.SORT_DESC,
  });

  const updateHoverGroup = debounce((group) => {
    configStore.updateHoverGroup(group);
  }, 10);

  const onClickColumnHeader = ({ columnName }) => {
    if (state.sortColumn === columnName) {
      if (state.sortDir === SORT_DIRECTIONS.SORT_ASC) {
        setState({ ...state, sortDir: SORT_DIRECTIONS.SORT_DESC });
      } else {
        setState({ ...state, sortDir: SORT_DIRECTIONS.SORT_ASC });
      }
    } else {
      setState({
        ...state,
        sortColumn: columnName,
        sortDir: SORT_DIRECTIONS.SORT_DESC,
      });
    }
  };

  const onItemSelect = (e) => {
    const selectedGroup = e.target.getAttribute('data-group');
    let newGroups;

    // If the click was not on an item, then unset the selection
    if (selectedGroup === null) {
      newGroups = [];
    }
    // If the item is already selected, then deselect it
    else if (
      configStore.selectedGroups.find(
        (group) => group.group === selectedGroup
      ) !== undefined
    ) {
      newGroups = configStore.selectedGroups.filter(
        (group) => !(group.group === selectedGroup)
      );
    } else {
      // Otherwise, add it
      newGroups = [{ group: selectedGroup }];
      // If shift is pressed, then add it to the existing selected groups
      if (UIStore.isKeyPressed(16)) {
        newGroups = newGroups.concat(configStore.selectedGroups);
      }
    }

    configStore.updateSelectedGroups(newGroups);
  };

  const getLegendKeys = () => {
    // Make own copy of the elements, and sort by group
    let legendItems = toJS(dataStore.groupCounts);

    // groupCounts is structured as:
    // [{ group_id, counts }]
    // Where group_id is either a mutation ID (in mutation mode)
    // or a string representing e.g. a lineage

    // Get some additional data:
    // 1) Group Name (get mutation name from mutation ID if in mutation mode)
    // 2) Color
    // 3) Calculate percent based off counts and total sequences
    // 4) mutation gene/protein (for sorting mutations - AA mutation mode only)
    // 5) mutation position (for sorting mutations - mutation mode only)
    if (configStore.groupKey === GROUP_MUTATION) {
      legendItems.forEach((record) => {
        let mut = mutationDataStore.intToMutation(
          configStore.dnaOrAa,
          configStore.coordinateMode,
          record.group_id
        );
        record.group = mut.mutation_str;
        record.group_name = mut.name;
        record.color = mut.color;
        record.percent =
          record.counts / dataStore.numSequencesAfterAllFiltering;
        record.pos = mut.pos;
        // If we're in DNA mode, then leave this null
        // Otherwise, get the gene or protein, depending on our AA mode
        record.gene_or_protein =
          configStore.dnaOrAa === DNA_OR_AA.DNA
            ? null
            : configStore.coordinateMode === COORDINATE_MODES.COORD_GENE
            ? mut.gene
            : mut.protein;
      });
    } else {
      legendItems.forEach((record) => {
        // For non-mutation groups, the name is the same as the ID
        record.group = record.group_id;
        record.group_name = record.group_id;
        record.color = groupDataStore.getGroupColor(
          configStore.groupKey,
          record.group_id
        );
        record.percent =
          record.counts / dataStore.numSequencesAfterAllFiltering;
      });
    }

    // Set aside the reference, and remove it from the rows list
    // Also set aside the "Other" group, if it exists
    // Sort the list, then add the reference back to the beginning
    // and add the other group back to the end
    const refItem = legendItems.find(
      (item) => item.group === GROUPS.REFERENCE_GROUP
    );
    const otherItem = legendItems.find(
      (item) => item.group === GROUPS.OTHER_GROUP
    );
    legendItems = legendItems.filter(
      (item) =>
        !(
          item.group === GROUPS.REFERENCE_GROUP ||
          item.group === GROUPS.OTHER_GROUP
        )
    );
    legendItems = legendItems.sort(
      comparer({
        sortColumn: state.sortColumn,
        sortDirection: state.sortDir,
        groupKey: configStore.groupKey,
        dnaOrAa: configStore.dnaOrAa,
        coordinateMode: configStore.coordinateMode,
      })
    );
    if (refItem !== undefined) {
      legendItems.unshift(refItem);
    }
    if (otherItem !== undefined) {
      legendItems.push(otherItem);
    }

    return legendItems;
  };

  useEffect(() => {
    let _arr = [...state.legendItems];
    _arr = _arr.sort(
      comparer({
        sortColumn: state.sortColumn,
        sortDirection: state.sortDir,
        groupKey: configStore.groupKey,
        dnaOrAa: configStore.dnaOrAa,
        coordinateMode: configStore.coordinateMode,
      })
    );
    setState({ ...state, legendItems: _arr });
  }, [state.sortColumn, state.sortDir]);

  useEffect(() => {
    if (UIStore.caseDataState !== ASYNC_STATES.SUCCEEDED) {
      return;
    }

    setState({
      ...state,
      legendItems: getLegendKeys().sort(
        comparer({
          sortColumn: state.sortColumn,
          sortDirection: state.sortDir,
          groupKey: configStore.groupKey,
          dnaOrAa: configStore.dnaOrAa,
          coordinateMode: configStore.coordinateMode,
        })
      ),
    });
  }, [UIStore.caseDataState]);

  if (UIStore.caseDataState === ASYNC_STATES.STARTED) {
    return (
      <div
        style={{
          height: '100%',
        }}
      >
        {Array.from({ length: 50 }, (_, i) => (
          <SkeletonElement
            key={`legend-loading-bar-${i}`}
            delay={5 + i + (i % 2) * 12.5}
            height={25}
          />
        ))}
      </div>
    );
  }

  return (
    <TableLegendContainer>
      <TableLegend
        legendItems={state.legendItems}
        updateHoverGroup={updateHoverGroup}
        updateSelectGroup={onItemSelect}
        sortColumn={state.sortColumn}
        sortDir={state.sortDir}
        onClickColumnHeader={onClickColumnHeader}
      />
    </TableLegendContainer>
  );
})
Example #22
Source File: CooccurrencePlot.js    From covidcg with MIT License 4 votes vote down vote up
CooccurrencePlot = observer(({ width }) => {
  const vegaRef = useRef();
  const { configStore, dataStore, plotSettingsStore, UIStore } = useStores();

  const handleDownloadSelect = (option) => {
    // console.log(option);
    // TODO: use the plot options and configStore options to build a more descriptive filename
    //       something like new_lineages_by_day_S_2020-05-03-2020-05-15_NYC.png...
    if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_DATA) {
      dataStore.downloadMutationCooccurrence();
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 1);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_2X) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 2);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_4X) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 4);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_SVG) {
      vegaRef.current.downloadImage('svg', 'vega-export.svg');
    }
  };

  const onChangeNormMode = (event) =>
    plotSettingsStore.setCooccurrenceNormMode(event.target.value);

  const handleHoverGroup = (...args) => {
    configStore.updateHoverGroup(args[1] === null ? null : args[1]['group']);
  };

  const handleSelectedGroups = (...args) => {
    const curSelectedGroups = toJS(configStore.selectedGroups).map(
      (item) => item.group
    );
    // console.log(curSelectedGroups);

    // Some of the groups might be multiple mutations, where the
    // group string is the two mutations separated by ' + '
    // So break those up and then combine into one big list
    // Also make sure we don't process "Reference" or "Other"
    // Array -> Set -> Array to remove duplicates
    const newSelectedGroups = Array.from(
      new Set(
        args[1]
          .map((item) => item.group.split(' + '))
          .reduce((memo, arr) => memo.concat(arr), [])
          .filter((item) => {
            return (
              item !== GROUPS.REFERENCE_GROUP && item !== GROUPS.OTHER_GROUP
            );
          })
      )
    );
    // console.log(newSelectedGroups);

    const groupsInCommon = newSelectedGroups
      .slice()
      .filter((group) => curSelectedGroups.includes(group));

    // The new selection to push to the store is
    // (cur u new) - (cur n new)
    // i.e., the union minus the intersection (XOR)
    // Array -> Set -> Array to remove duplicates
    // Then wrap in an object of { group: group }
    const pushSelectedGroups = Array.from(
      new Set(
        curSelectedGroups
          .concat(newSelectedGroups)
          .filter((group) => !groupsInCommon.includes(group))
      )
    ).map((group) => {
      return { group };
    });
    // console.log(pushSelectedGroups);

    configStore.updateSelectedGroups(pushSelectedGroups);
  };

  const getCooccurrenceData = () => {
    // console.log('GET COOCCURRENCE DATA');
    let newCooccurrenceData = toJS(dataStore.mutationCooccurrence);
    newCooccurrenceData = aggregate({
      data: newCooccurrenceData,
      groupby: ['combi', 'mutation', 'mutationName'],
      fields: ['combiName', 'mutationName', 'color', 'count'],
      ops: ['max', 'max', 'max', 'sum'],
      as: ['combiName', 'mutationName', 'color', 'count'],
    });

    return newCooccurrenceData;
  };

  const [state, setState] = useState({
    data: {},
    hoverGroup: null,
    signalListeners: {
      hoverGroup: throttle(handleHoverGroup, 50),
    },
    dataListeners: {
      selectedGroups: handleSelectedGroups,
    },
  });

  useEffect(() => {
    setState({
      ...state,
      hoverGroup: { group: configStore.hoverGroup },
    });
  }, [configStore.hoverGroup]);

  const refreshData = () => {
    // Only update once the mutation data finished processing
    if (UIStore.cooccurrenceDataState !== ASYNC_STATES.SUCCEEDED) {
      return;
    }

    setState({
      ...state,
      data: {
        //selectedGroups: toJS(configStore.selectedGroups),
        selectedGroups: [],
        cooccurrence_data: getCooccurrenceData(),
      },
    });
  };

  // Refresh data on mount (i.e., tab change) or when data state changes
  useEffect(refreshData, [UIStore.cooccurrenceDataState]);
  useEffect(refreshData, []);

  if (UIStore.cooccurrenceDataState === ASYNC_STATES.STARTED) {
    return (
      <div
        style={{
          paddingTop: '12px',
          paddingRight: '24px',
          paddingLeft: '12px',
          paddingBottom: '24px',
        }}
      >
        <SkeletonElement delay={2} height={70} />
      </div>
    );
  }

  if (configStore.selectedGroups.length === 0) {
    return (
      <EmptyPlot height={70}>
        <p>
          No mutations selected. Please select one or more mutations from the
          legend, frequency plot, or table.
        </p>
      </EmptyPlot>
    );
  } else if (dataStore.mutationCooccurrence.length === 0) {
    return (
      <EmptyPlot height={70}>
        <p>
          No mutations that co-occur with selected mutations{' '}
          {configStore.selectedGroups
            .map((item) => formatMutation(item.group, configStore.dnaOrAa))
            .join(', ')}
        </p>
      </EmptyPlot>
    );
  }

  // Signals
  let stackOffset, xLabel, xFormat;
  if (plotSettingsStore.cooccurrenceNormMode === NORM_MODES.NORM_COUNTS) {
    stackOffset = 'zero';
    xLabel = 'Mutation Frequency';
    xFormat = 's';
  } else if (
    plotSettingsStore.cooccurrenceNormMode === NORM_MODES.NORM_PERCENTAGES
  ) {
    stackOffset = 'normalize';
    xLabel = 'Mutation Frequency (Normalized)';
    xFormat = 's';
  }

  // Subtitle text
  const maxShownMutations = 4;
  let subtitle = configStore.selectedGroups
    .slice(0, maxShownMutations)
    .map((item) => formatMutation(item.group, configStore.dnaOrAa))
    .join(', ');
  if (configStore.selectedGroups.length > maxShownMutations) {
    subtitle += ', ...';
  } else {
    subtitle += '';
  }

  return (
    <PlotContainer>
      <PlotOptions>
        <PlotTitle>
          <span className="title">Co-occurring mutations of {subtitle}</span>
        </PlotTitle>
        Show mutations as{' '}
        <OptionSelectContainer>
          <label>
            <select
              value={plotSettingsStore.cooccurrenceNormMode}
              onChange={onChangeNormMode}
            >
              <option value={NORM_MODES.NORM_COUNTS}>Counts</option>
              <option value={NORM_MODES.NORM_PERCENTAGES}>
                Normalized Counts
              </option>
            </select>
          </label>
        </OptionSelectContainer>
        <div className="spacer"></div>
        <DropdownButton
          text={'Download'}
          options={[
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_DATA,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_2X,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_4X,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_SVG,
          ]}
          onSelect={handleDownloadSelect}
        />
      </PlotOptions>
      <VegaEmbed
        ref={vegaRef}
        width={width}
        spec={initialSpec}
        data={state.data}
        signalListeners={state.signalListeners}
        dataListeners={state.dataListeners}
        signals={{
          dna: configStore.dnaOrAa === DNA_OR_AA.DNA,
          hoverGroup: state.hoverGroup,
          stackOffset,
          xLabel,
          xFormat,
        }}
        actions={false}
      />
    </PlotContainer>
  );
})
Example #23
Source File: EntropyPlot.js    From covidcg with MIT License 4 votes vote down vote up
EntropyPlot = observer(({ width }) => {
  const vegaRef = useRef();
  const {
    configStore,
    dataStore,
    UIStore,
    mutationDataStore,
    plotSettingsStore,
  } = useStores();

  const onDismissWarning = () => {
    setState({
      ...state,
      showWarning: false,
    });
  };

  const handleDownloadSelect = (option) => {
    // console.log(option);
    // TODO: use the plot options and configStore options to build a more descriptive filename
    //       something like new_lineages_by_day_S_2020-05-03-2020-05-15_NYC.png...
    if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_DATA) {
      dataStore.downloadMutationFrequencies();
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 1);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_2X) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 2);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_4X) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 4);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_SVG) {
      vegaRef.current.downloadImage('svg', 'vega-export.svg');
    }
  };

  const processData = () => {
    let mutationCounts = toJS(dataStore.groupCounts);

    return mutationCounts.filter((record) => {
      return (
        record.group !== GROUPS.OTHER_GROUP &&
        record.group !== GROUPS.REFERENCE_GROUP
      );
    });
  };

  const handleHoverGroup = (...args) => {
    // Don't fire the action if there's no change
    let hoverGroup = args[1] === null ? null : args[1]['group'];
    if (hoverGroup === configStore.hoverGroup) {
      return;
    }
    configStore.updateHoverGroup(hoverGroup);
  };

  const handleSelected = (...args) => {
    // console.log(args[1], toJS(configStore.selectedGroups));
    const curSelectedGroups = args[1].map((item) => {
      return { group: item.group };
    });
    configStore.updateSelectedGroups(curSelectedGroups);
  };

  const onChangeEntropyYMode = (event) => {
    plotSettingsStore.setEntropyYMode(event.target.value);
  };
  const onChangeEntropyYPow = (event) => {
    plotSettingsStore.setEntropyYPow(event.target.value);
  };

  // Domain Plot height is calculated as the number of rows times a constant
  const domainPlotRowHeight = 15;
  const getDomainPlotHeight = () => {
    // There will always be at least 1 row (nullDomain displays when no rows)
    let numRows = 1;

    // Logic for Primer track
    if (configStore.coordinateMode === COORDINATE_MODES.COORD_PRIMER) {
      if (configStore.selectedPrimers.length > 0) {
        const primerObj = configStore.selectedPrimers;
        primerObj.forEach((primer) => {
          if (primer.row + 1 > numRows) {
            numRows = primer.row + 1;
          }
        });

        return numRows * domainPlotRowHeight;
      } else {
        return domainPlotRowHeight;
      }
    } else if (
      configStore.coordinateMode === COORDINATE_MODES.COORD_GENE ||
      configStore.coordinateMode === COORDINATE_MODES.COORD_PROTEIN
    ) {
      // Logic for Gene/Protein track
      let geneProteinObj = null;
      if (configStore.coordinateMode === COORDINATE_MODES.COORD_GENE) {
        geneProteinObj = configStore.selectedGene;
      } else if (
        configStore.coordinateMode === COORDINATE_MODES.COORD_PROTEIN
      ) {
        geneProteinObj = configStore.selectedProtein;
      }

      // Greedily get the number of rows
      if (geneProteinObj && geneProteinObj.domains.length > 0) {
        geneProteinObj.domains.forEach((domain) => {
          // geneProtein[row] is zero-indexed so add 1 to get total number of rows
          if (domain['row'] + 1 > numRows) {
            numRows = domain['row'] + 1;
          }
        });
      }
    }

    return numRows * domainPlotRowHeight;
  };

  const getXRange = () => {
    // Apply xRange
    let xRange;
    if (configStore.residueCoordinates.length === 0) {
      // If the residue coordinates are empty, then either "All Genes" or
      // "All Proteins" is selected -- so show everything
      xRange = [1, 30000];
    } else if (configStore.dnaOrAa === DNA_OR_AA.DNA) {
      const coordRanges = toJS(configStore.getCoordinateRanges());
      xRange = [
        coordRanges.reduce((memo, rng) => Math.min(...rng, memo), 30000),
        coordRanges.reduce((memo, rng) => Math.max(...rng, memo), 0),
      ];
    } else if (configStore.dnaOrAa === DNA_OR_AA.AA) {
      // Get the extent of the selected gene/protein
      let geneOrProteinObj;
      let residueCoordinates = toJS(configStore.residueCoordinates);
      if (configStore.coordinateMode === COORDINATE_MODES.COORD_GENE) {
        geneOrProteinObj = configStore.selectedGene;
      } else if (
        configStore.coordinateMode === COORDINATE_MODES.COORD_PROTEIN
      ) {
        geneOrProteinObj = configStore.selectedProtein;
      }

      // Find the smallest and largest residue index
      // from the selected residue coordinates
      const minResidueIndex = residueCoordinates.reduce(
        (minIndex, rng) => Math.min(...rng, minIndex),
        geneOrProteinObj.len_aa
      );
      const maxResidueIndex = residueCoordinates.reduce(
        (minIndex, rng) => Math.max(...rng, minIndex),
        1
      );

      if (configStore.dnaOrAa === DNA_OR_AA.DNA) {
        // Find the AA range that the minimum and maximum AA index fall in,
        // and then get the NT coordinates (from the start of the codon)
        let startNTInd, endNTInd;
        geneOrProteinObj.aa_ranges.forEach((aaRange, ind) => {
          if (minResidueIndex >= aaRange[0] && minResidueIndex <= aaRange[1]) {
            // Get the matching NT range, add residues * 3
            startNTInd =
              geneOrProteinObj.ranges[ind][0] +
              (minResidueIndex - aaRange[0]) * 3;
          }
          if (maxResidueIndex >= aaRange[0] && maxResidueIndex <= aaRange[1]) {
            // Get the matching NT range, add residues * 3 (to end of codon)
            endNTInd =
              geneOrProteinObj.ranges[ind][0] +
              2 +
              (maxResidueIndex - aaRange[0]) * 3;
          }
        });
        xRange = [startNTInd, endNTInd];
      } else if (configStore.dnaOrAa === DNA_OR_AA.AA) {
        xRange = [minResidueIndex, maxResidueIndex];
      }
    }
    return xRange;
  };

  const getDomains = () => {
    // Apply domains
    const xRange = getXRange();
    const nullDomain = [
      {
        name: 'No Domains Available',
        abbr: 'No Domains Available',
        ranges: [xRange],
        row: 0,
      },
    ];
    if (configStore.coordinateMode === COORDINATE_MODES.COORD_GENE) {
      return configStore.selectedGene.domains.length > 0
        ? configStore.selectedGene.domains
        : nullDomain;
    } else if (configStore.coordinateMode === COORDINATE_MODES.COORD_PROTEIN) {
      return configStore.selectedProtein.domains.length > 0
        ? configStore.selectedProtein.domains
        : nullDomain;
    } else {
      return nullDomain;
    }
  };

  const checkIfPrimersOverlap = (primer1, primer2) => {
    return (
      (primer1.Start < primer2.End && primer1.End > primer2.End) ||
      (primer1.Start < primer2.Start && primer1.End > primer2.Start)
    );
  };

  const getPrimers = () => {
    const selectedPrimers = configStore.selectedPrimers;

    if (selectedPrimers.length) {
      selectedPrimers[0].row = 0;
      for (let i = 0; i < selectedPrimers.length; i++) {
        let overlaps = true;
        let curRow = 0;
        const primerToPlace = selectedPrimers[i];

        while (overlaps) {
          const primersInRow = selectedPrimers.filter(
            (primer) => primer.row && primer.row === curRow
          );

          if (primersInRow.length) {
            for (const primer of primersInRow) {
              overlaps = checkIfPrimersOverlap(primer, primerToPlace);
              if (!overlaps) break;
            }
          } else {
            overlaps = false;
          }

          if (overlaps) curRow += 1;
        }

        primerToPlace.row = curRow;
        primerToPlace.ranges = [[primerToPlace.Start, primerToPlace.End]];
        primerToPlace.name = primerToPlace.Name;
        selectedPrimers[i] = primerToPlace;
      }
      return selectedPrimers;
    } else {
      const nullDomain = [
        {
          Institution: 'None',
          Name: 'No Primers Selected',
          ranges: [[0, 30000]],
          row: 0,
          Start: 0,
          End: 30000,
        },
      ];
      configStore.selectedPrimers = nullDomain;
      return nullDomain;
    }
  };

  const domainsToShow = () => {
    return configStore.coordinateMode === COORDINATE_MODES.COORD_PRIMER
      ? getPrimers()
      : getDomains();
  };

  const [state, setState] = useState({
    showWarning: true,
    xRange: getXRange(),
    hoverGroup: null,
    data: { domains: domainsToShow() },
    domainPlotHeight: getDomainPlotHeight(),
    signalListeners: {
      hoverGroup: throttle(handleHoverGroup, 100),
    },
    dataListeners: {
      selected: handleSelected,
    },
  });

  useEffect(() => {
    setState({
      ...state,
      hoverGroup: { group: configStore.hoverGroup },
    });
  }, [configStore.hoverGroup]);

  // Update internal selected groups copy
  useEffect(() => {
    setState({
      ...state,
      data: {
        ...state.data,
        selected: toJS(configStore.selectedGroups),
      },
    });
  }, [configStore.selectedGroups]);

  const refreshData = () => {
    if (UIStore.caseDataState !== ASYNC_STATES.SUCCEEDED) {
      return;
    }

    setState({
      ...state,
      xRange: getXRange(),
      domainPlotHeight: getDomainPlotHeight(),
      data: {
        ...state.data,
        domains: domainsToShow(),
        table: processData(),
        coverage: mutationDataStore.coverage,
      },
    });
  };

  // Refresh data on mount (i.e., tab change) or when data state changes
  useEffect(refreshData, [UIStore.caseDataState]);
  useEffect(refreshData, []);

  // Generate x-axis title
  let xLabel = '';
  if (configStore.dnaOrAa === DNA_OR_AA.DNA) {
    xLabel += 'NT';
  } else if (configStore.dnaOrAa === DNA_OR_AA.AA) {
    xLabel += 'AA residue';
  }
  xLabel += ' (' + configStore.selectedReference;
  if (configStore.coordinateMode === COORDINATE_MODES.COORD_GENE) {
    xLabel += ', ' + configStore.selectedGene.name + ' Gene';
  } else if (configStore.coordinateMode === COORDINATE_MODES.COORD_PROTEIN) {
    xLabel += ', ' + configStore.selectedProtein.name + ' Protein';
  }
  xLabel += ')';

  if (UIStore.caseDataState === ASYNC_STATES.STARTED) {
    return (
      <div
        style={{
          paddingTop: '12px',
          paddingRight: '24px',
          paddingLeft: '12px',
          paddingBottom: '24px',
        }}
      >
        <SkeletonElement delay={2} height={150} />
      </div>
    );
  }

  // If we have no rows, then return an empty element
  // We'll always have the "reference" row, so no rows = 1 row
  if (dataStore.numSequencesAfterAllFiltering === 0) {
    return (
      <EmptyPlot height={150}>
        <p>No sequences selected</p>
      </EmptyPlot>
    );
  }

  const entropyPlotHeight = 120;
  const coveragePlotHeight = 40;
  const padding = 40;

  return (
    <PlotContainer>
      {config['virus'] === 'sars2' && (
        <WarningBox show={state.showWarning} onDismiss={onDismissWarning}>
          {config.site_title} plots reflect data contributed to GISAID and are
          therefore impacted by the sequence coverage in each country. For
          example, systematic errors are sometimes observed specific to
          particular labs or methods (
          <ExternalLink href="https://virological.org/t/issues-with-sars-cov-2-sequencing-data/473/14">
            https://virological.org/t/issues-with-sars-cov-2-sequencing-data/473/14
          </ExternalLink>
          ,{' '}
          <ExternalLink href="https://doi.org/10.1371/journal.pgen.1009175">
            https://doi.org/10.1371/journal.pgen.1009175
          </ExternalLink>
          ). Users are advised to consider these errors in their high resolution
          analyses.
        </WarningBox>
      )}
      <PlotOptions>
        <OptionSelectContainer>
          <label>
            Show:
            <select
              value={plotSettingsStore.entropyYMode}
              onChange={onChangeEntropyYMode}
            >
              <option value={NORM_MODES.NORM_COUNTS}>Counts</option>
              <option value={NORM_MODES.NORM_PERCENTAGES}>Percents</option>
              <option value={NORM_MODES.NORM_COVERAGE_ADJUSTED}>
                Percents (coverage adjusted)
              </option>
            </select>
          </label>
          <QuestionButton
            data-tip={`
          <ul>
            <li>
              Counts: show raw mutation counts
            </li>
            <li>
              Percents: show mutation counts as a percentage of 
              the total number of sequences selected
            </li>
            <li>
              Percents (coverage adjusted): show mutation counts as a 
              percentage of sequences with coverage at the mutation position
            </li>
          </ul>
          `}
            data-html="true"
            data-for="main-tooltip"
          />
        </OptionSelectContainer>

        <OptionInputContainer style={{ marginLeft: '10px' }}>
          <label>
            Y-scale:
            <input
              value={plotSettingsStore.entropyYPow}
              type="number"
              step={0.1}
              min={0}
              onChange={onChangeEntropyYPow}
            ></input>
          </label>
          <QuestionButton
            data-tip={`
          <ul>
            <li>
              Y-scale: adjust the power of the y-axis. 
            </li>
            <li>
              For Y-scale = 1, the y-axis scale is linear.
            </li>
            <li>
              For Y-scale < 1, lower values are more visible.
            </li>
            <li>
              For Y-scale > 1, lower values are less visible.
            </li>
          </ul>
          `}
            data-html="true"
            data-for="main-tooltip"
          />
        </OptionInputContainer>
        <div className="spacer"></div>
        <DropdownButton
          text={'Download'}
          options={[
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_DATA,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_2X,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_4X,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_SVG,
          ]}
          onSelect={handleDownloadSelect}
        />
      </PlotOptions>
      <VegaEmbed
        ref={vegaRef}
        spec={initialSpec}
        data={state.data}
        width={width}
        height={
          entropyPlotHeight +
          padding +
          state.domainPlotHeight +
          padding +
          coveragePlotHeight
        }
        signals={{
          yMode: plotSettingsStore.entropyYMode,
          yScaleExponent: plotSettingsStore.entropyYPow,
          totalSequences: dataStore.numSequencesAfterAllFiltering,
          xLabel,
          xRange: state.xRange,
          hoverGroup: state.hoverGroup,
          numDomainRows: state.domainPlotHeight / domainPlotRowHeight,
          domainPlotHeight: state.domainPlotHeight,
          posField:
            configStore.dnaOrAa === DNA_OR_AA.DNA &&
            configStore.residueCoordinates.length !== 0 &&
            configStore.coordinateMode !== COORDINATE_MODES.COORD_PRIMER
              ? 0
              : 1,
        }}
        signalListeners={state.signalListeners}
        dataListeners={state.dataListeners}
        actions={false}
      />
    </PlotContainer>
  );
})
Example #24
Source File: GroupStackPlot.js    From covidcg with MIT License 4 votes vote down vote up
GroupStackPlot = observer(({ width }) => {
  const vegaRef = useRef();
  const { dataStore, UIStore, configStore, plotSettingsStore, groupDataStore } =
    useStores();

  const handleHoverGroup = (...args) => {
    // Don't fire the action if there's no change
    configStore.updateHoverGroup(args[1] === null ? null : args[1]['group']);
  };

  const handleSelected = (...args) => {
    // Ignore selections in mutation mode
    if (configStore.groupKey === GROUP_MUTATION) {
      return;
    }
    configStore.updateSelectedGroups(args[1]);
  };

  const handleDownloadSelect = (option) => {
    // console.log(option);
    // TODO: use the plot options and configStore options to build a more descriptive filename
    //       something like new_lineages_by_day_S_2020-05-03-2020-05-15_NYC.png...
    if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_DATA) {
      dataStore.downloadAggGroupDate();
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 1);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_2X) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 2);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_4X) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 4);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_SVG) {
      vegaRef.current.downloadImage('svg', 'vega-export.svg');
    }
  };

  const processData = () => {
    // console.log('GROUP STACK PROCESS DATA');

    if (configStore.groupKey === GROUP_MUTATION) {
      let data = toJS(dataStore.aggLocationSelectedMutationsDate);

      // Filter focused locations
      const focusedLocations = state.focusLocationTree
        .filter((node) => node.checked)
        .map((node) => node.value);
      data = data.filter((record) =>
        focusedLocations.includes(record.location)
      );

      // Re-aggregate
      data = aggregate({
        data,
        groupby: ['group', 'collection_date'],
        fields: ['counts', 'color', 'group_name'],
        ops: ['sum', 'first', 'first'],
        as: ['counts', 'color', 'group_name'],
      });

      // console.log(JSON.stringify(data));

      return data;
    }

    // For non-mutation mode, we'll need some additional fields:
    // 1) color of group
    // 2) name of group (same as group id)
    // Also collapse low-frequency groups based on settings
    const validGroups = getValidGroups({
      records: dataStore.groupCounts,
      lowFreqFilterType: plotSettingsStore.groupStackLowFreqFilter,
      lowFreqFilterValue: plotSettingsStore.groupStackLowFreqValue,
    });
    let data = toJS(dataStore.aggLocationGroupDate);

    // Filter focused locations
    const focusedLocations = state.focusLocationTree
      .filter((node) => node.checked)
      .map((node) => node.value);
    data = data.filter((record) => focusedLocations.includes(record.location));

    data = data.map((record) => {
      if (!validGroups.includes(record.group_id)) {
        record.group = GROUPS.OTHER_GROUP;
      } else {
        record.group = record.group_id;
      }

      record.group_name = record.group;
      record.color = groupDataStore.getGroupColor(
        configStore.groupKey,
        record.group
      );

      return record;
    });

    data = aggregate({
      data,
      groupby: ['group', 'collection_date'],
      fields: ['counts', 'color', 'group_name'],
      ops: ['sum', 'first', 'first'],
      as: ['counts', 'color', 'group_name'],
    });

    // console.log(JSON.stringify(data));

    return data;
  };

  const [state, setState] = useState({
    showWarning: true,
    // data: {},
    hoverGroup: null,
    focusLocationTree: [],
    signalListeners: {
      // detailDomain: debounce(handleBrush, 500),
      hoverBar: throttle(handleHoverGroup, 100),
    },
    dataListeners: {
      selected: handleSelected,
    },
  });

  // Update state based on the focused location dropdown select
  const focusLocationSelectOnChange = (event) => {
    const focusLocationTree = state.focusLocationTree.slice();
    focusLocationTree.forEach((node) => {
      if (node.value === event.value) {
        node.checked = event.checked;
      }
    });

    setState({
      ...state,
      focusLocationTree,
    });
  };
  // Deselect all focused locations
  const deselectAllFocusedLocations = () => {
    const focusLocationTree = state.focusLocationTree.slice().map((node) => {
      node.checked = false;
      return node;
    });
    setState({
      ...state,
      focusLocationTree,
    });
  };

  const onDismissWarning = () => {
    setState({
      ...state,
      showWarning: false,
    });
  };

  const onChangeNormMode = (event) =>
    plotSettingsStore.setGroupStackNormMode(event.target.value);
  const onChangeCountMode = (event) =>
    plotSettingsStore.setGroupStackCountMode(event.target.value);
  const onChangeDateBin = (event) =>
    plotSettingsStore.setGroupStackDateBin(event.target.value);

  useEffect(() => {
    setState({
      ...state,
      hoverGroup: { group: configStore.hoverGroup },
    });
  }, [configStore.hoverGroup]);

  // Update internal selected groups copy
  useEffect(() => {
    // console.log('SELECTED GROUPS');
    // Skip this update if we're in mutation mode
    if (configStore.groupKey === GROUP_MUTATION) {
      return;
    }

    setState({
      ...state,
      data: {
        ...state.data,
        selected: toJS(configStore.selectedGroups),
      },
    });
  }, [configStore.selectedGroups]);

  const refreshData = () => {
    // Skip unless the mutation data finished processing
    if (
      configStore.groupKey === GROUP_MUTATION &&
      UIStore.mutationDataState !== ASYNC_STATES.SUCCEEDED
    ) {
      return;
    }

    if (UIStore.caseDataState !== ASYNC_STATES.SUCCEEDED) {
      return;
    }

    // console.log('REFRESH DATA FROM STORE');

    // Update focus location tree with new locations
    const focusLocationTree = [];
    Object.keys(dataStore.countsPerLocationMap).forEach((loc) => {
      focusLocationTree.push({
        label: loc + ' (' + dataStore.countsPerLocationMap[loc] + ')',
        value: loc,
        checked: true,
      });
    });

    const newState = {
      ...state,
      data: {
        cases_by_date_and_group: processData(),
        selected: toJS(configStore.selectedGroups),
      },
    };

    // Only update location tree if the location list changed
    // This is to prevent firing the processData event twice when we change selected groups
    const prevLocs = state.focusLocationTree.map((node) => node.value);
    if (
      focusLocationTree.length !== state.focusLocationTree.length ||
      !focusLocationTree.every((node) => prevLocs.includes(node.value))
    ) {
      newState['focusLocationTree'] = focusLocationTree;
    }

    setState(newState);
  };

  useEffect(() => {
    // console.log('UPDATE DATA FROM FOCUSED LOCATIONS');

    setState({
      ...state,
      data: {
        ...state.data,
        cases_by_date_and_group: processData(),
      },
    });
  }, [state.focusLocationTree]);

  const focusLocationDropdownContainer = (
    <StyledDropdownTreeSelect
      mode={'multiSelect'}
      data={state.focusLocationTree}
      className="geo-dropdown-tree-select"
      clearSearchOnChange={false}
      keepTreeOnSearch={true}
      keepChildrenOnSearch={true}
      showPartiallySelected={false}
      inlineSearchInput={false}
      texts={{
        placeholder: 'Search...',
        noMatches: 'No matches found',
      }}
      onChange={focusLocationSelectOnChange}
      // onAction={treeSelectOnAction}
      // onNodeToggle={treeSelectOnNodeToggleCurrentNode}
    />
  );

  // Refresh data on mount (i.e., tab change) or when data state changes
  useEffect(refreshData, [
    UIStore.caseDataState,
    UIStore.mutationDataState,
    plotSettingsStore.groupStackLowFreqFilter,
    plotSettingsStore.groupStackLowFreqValue,
  ]);
  useEffect(refreshData, []);

  // For development in Vega Editor
  // console.log(JSON.stringify(caseData));

  if (UIStore.caseDataState === ASYNC_STATES.STARTED) {
    return (
      <div
        style={{
          paddingTop: '12px',
          paddingRight: '24px',
          paddingLeft: '12px',
          paddingBottom: '24px',
        }}
      >
        <SkeletonElement delay={2} height={400} />
      </div>
    );
  }

  if (configStore.selectedLocationNodes.length === 0) {
    return (
      <EmptyPlot height={250}>
        <p>
          No locations selected. Please select one or more locations from the
          sidebar, under &quot;Selected Locations&quot;, to compare counts of{' '}
          <b>{configStore.getGroupLabel()}</b> between them.
        </p>
      </EmptyPlot>
    );
  }

  let plotTitle = '';
  if (plotSettingsStore.groupStackCountMode === COUNT_MODES.COUNT_CUMULATIVE) {
    plotTitle += 'Cumulative ';
  } else if (plotSettingsStore.groupStackCountMode === COUNT_MODES.COUNT_NEW) {
    plotTitle += 'New ';
  }
  plotTitle += configStore.getGroupLabel();
  plotTitle +=
    plotSettingsStore.groupStackNormMode === NORM_MODES.NORM_PERCENTAGES
      ? ' Percentages'
      : ' Counts';

  if (plotSettingsStore.groupStackDateBin === DATE_BINS.DATE_BIN_DAY) {
    plotTitle += ' by Day';
  } else if (plotSettingsStore.groupStackDateBin === DATE_BINS.DATE_BIN_WEEK) {
    plotTitle += ' by Week';
  } else if (plotSettingsStore.groupStackDateBin === DATE_BINS.DATE_BIN_MONTH) {
    plotTitle += ' by Month';
  } else if (plotSettingsStore.groupStackDateBin === DATE_BINS.DATE_BIN_YEAR) {
    plotTitle += ' by Year';
  }

  const maxShownLocations = 4;
  let selectedLocationsText = '';
  if (!state.focusLocationTree.every((node) => node.checked === false)) {
    selectedLocationsText +=
      '(' +
      state.focusLocationTree
        .filter((node) => node.checked)
        .slice(0, maxShownLocations)
        .map((node) => node.value)
        .join(', ');
    if (state.focusLocationTree.length > maxShownLocations) {
      selectedLocationsText += ', ...)';
    } else {
      selectedLocationsText += ')';
    }
  }

  // Set the stack offset mode
  const stackOffset =
    plotSettingsStore.groupStackNormMode === NORM_MODES.NORM_PERCENTAGES
      ? 'normalize'
      : 'zero';
  // Set the date bin
  let dateBin;
  if (plotSettingsStore.groupStackDateBin === DATE_BINS.DATE_BIN_DAY) {
    dateBin = 1000 * 60 * 60 * 24;
  } else if (plotSettingsStore.groupStackDateBin === DATE_BINS.DATE_BIN_WEEK) {
    dateBin = 1000 * 60 * 60 * 24 * 7;
  } else if (plotSettingsStore.groupStackDateBin === DATE_BINS.DATE_BIN_MONTH) {
    dateBin = 1000 * 60 * 60 * 24 * 30;
  } else if (plotSettingsStore.groupStackDateBin === DATE_BINS.DATE_BIN_YEAR) {
    dateBin = 1000 * 60 * 60 * 24 * 365;
  }

  // If running in cumulative mode, add the vega transformation
  // By default the cumulative transformation is dumped into a column
  // "counts_cumulative", so if active, just overwrite the "counts"
  // column with this cumulative count
  const cumulativeWindow =
    plotSettingsStore.groupStackCountMode === COUNT_MODES.COUNT_CUMULATIVE
      ? [null, 0]
      : [0, 0];

  // Adapt labels to groupings
  let detailYLabel = '';
  if (plotSettingsStore.groupStackCountMode === COUNT_MODES.COUNT_CUMULATIVE) {
    detailYLabel += 'Cumulative ';
  }
  if (plotSettingsStore.groupStackNormMode === NORM_MODES.NORM_PERCENTAGES) {
    detailYLabel += '% ';
  }
  detailYLabel += 'Sequences by ' + configStore.getGroupLabel();

  // Hide the detail view in mutation mode when there's no selections
  // Also disable the plot options when the detail panel is hidden
  const hideDetail =
    configStore.groupKey === GROUP_MUTATION &&
    configStore.selectedGroups.length === 0;
  const detailHeight = hideDetail ? 0 : 280;
  const height = hideDetail ? 60 : 380;

  return (
    <div>
      <WarningBox show={state.showWarning} onDismiss={onDismissWarning}>
        {config.site_title} plots reflect data contributed to GISAID and are
        therefore impacted by the sequence coverage in each country.
      </WarningBox>
      {hideDetail && (
        <EmptyPlot height={100}>
          <p>
            No {configStore.getGroupLabel()}s selected. Please select one or
            more {configStore.getGroupLabel()}s from the legend, frequency plot,
            or table.
          </p>
        </EmptyPlot>
      )}
      {!hideDetail && (
        <PlotHeader>
          <PlotTitle style={{ gridRow: '1/-1' }}>
            <span className="title">{plotTitle}</span>
            <span className="subtitle">{selectedLocationsText}</span>
          </PlotTitle>
          <PlotOptionsRow>
            <OptionSelectContainer>
              <label>
                <select
                  value={plotSettingsStore.groupStackCountMode}
                  onChange={onChangeCountMode}
                >
                  <option value={COUNT_MODES.COUNT_NEW}>New</option>
                  <option value={COUNT_MODES.COUNT_CUMULATIVE}>
                    Cumulative
                  </option>
                </select>
              </label>
            </OptionSelectContainer>
            sequences, shown as{' '}
            <OptionSelectContainer>
              <label>
                <select
                  value={plotSettingsStore.groupStackNormMode}
                  onChange={onChangeNormMode}
                >
                  <option value={NORM_MODES.NORM_COUNTS}>Counts</option>
                  <option value={NORM_MODES.NORM_PERCENTAGES}>
                    Percentages
                  </option>
                </select>
              </label>
            </OptionSelectContainer>
            grouped by{' '}
            <OptionSelectContainer>
              <label>
                <select
                  value={plotSettingsStore.groupStackDateBin}
                  onChange={onChangeDateBin}
                >
                  <option value={DATE_BINS.DATE_BIN_DAY}>Day</option>
                  <option value={DATE_BINS.DATE_BIN_WEEK}>Week</option>
                  <option value={DATE_BINS.DATE_BIN_MONTH}>Month</option>
                  <option value={DATE_BINS.DATE_BIN_YEAR}>Year</option>
                </select>
              </label>
            </OptionSelectContainer>
          </PlotOptionsRow>
          {configStore.groupKey !== GROUP_MUTATION &&
            config.group_cols[configStore.groupKey].show_collapse_options && (
              <PlotOptionsRow>
                <LowFreqFilter
                  lowFreqFilterType={plotSettingsStore.groupStackLowFreqFilter}
                  lowFreqFilterValue={plotSettingsStore.groupStackLowFreqValue}
                  updateLowFreqFilterType={
                    plotSettingsStore.setGroupStackLowFreqFilter
                  }
                  updateLowFreqFilterValue={
                    plotSettingsStore.setGroupStackLowFreqValue
                  }
                ></LowFreqFilter>
              </PlotOptionsRow>
            )}
          <PlotOptionsRow>
            Only show locations:&nbsp;{focusLocationDropdownContainer}
            {'  '}
            <button
              disabled={state.focusLocationTree.every(
                (node) => node.checked === false
              )}
              onClick={deselectAllFocusedLocations}
              style={{ marginLeft: 10 }}
            >
              Deselect all
            </button>
          </PlotOptionsRow>
          <PlotOptionsRow style={{ justifyContent: 'flex-end' }}>
            <DropdownButton
              text={'Download'}
              options={[
                PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_DATA,
                PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG,
                PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_2X,
                PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_4X,
                PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_SVG,
              ]}
              style={{ minWidth: '90px' }}
              onSelect={handleDownloadSelect}
            />
          </PlotOptionsRow>
        </PlotHeader>
      )}

      <div style={{ width: `${width}px` }}>
        <VegaEmbed
          ref={vegaRef}
          data={state.data}
          spec={initialSpec}
          signalListeners={state.signalListeners}
          dataListeners={state.dataListeners}
          signals={{
            dateRangeStart: new Date(config.min_date).getTime(),
            disableSelectionColoring: configStore.groupKey === GROUP_MUTATION,
            detailHeight,
            hoverBar: state.hoverGroup,
            stackOffset,
            dateBin,
            cumulativeWindow,
            detailYLabel,
            yFormat:
              plotSettingsStore.groupStackNormMode === NORM_MODES.NORM_COUNTS
                ? 's'
                : '%',
          }}
          cheapSignals={['hoverBar']}
          width={width}
          height={height}
          actions={false}
        />
      </div>
    </div>
  );
})
Example #25
Source File: LocationDatePlot.js    From covidcg with MIT License 4 votes vote down vote up
LocationDatePlot = observer(({ width }) => {
  const vegaRef = useRef();
  const { dataStore, configStore, UIStore, plotSettingsStore, groupDataStore } =
    useStores();

  const handleHoverLocation = (...args) => {
    // Don't fire the action if there's no change
    let hoverLocation = args[1] === null ? null : args[1]['location'];
    if (hoverLocation === configStore.hoverLocation) {
      return;
    }
    configStore.updateHoverLocation(hoverLocation);
  };

  const handleSelected = (...args) => {
    configStore.updateFocusedLocations(args[1]);
  };

  const processLocationData = () => {
    //console.log('PROCESS LOCATION DATE DATA');

    let locationData;
    if (configStore.groupKey === GROUP_MUTATION) {
      if (dataStore.aggLocationSelectedMutationsDate === undefined) {
        return [];
      }

      locationData = toJS(dataStore.aggLocationSelectedMutationsDate);

      // Filter out 'All Other Sequences' group
      locationData = locationData.filter((row) => {
        return row.group !== GROUPS.ALL_OTHER_GROUP;
      });
    } else {
      if (dataStore.aggLocationGroupDate === undefined) {
        return [];
      }

      locationData = toJS(dataStore.aggLocationGroupDate).map((record) => {
        record.color = groupDataStore.getGroupColor(
          configStore.groupKey,
          record.group_id
        );
        record.group = record.group_id;
        record.group_name = record.group_id;
        return record;
      });
    }

    // TODO: add coverage-adjusted percentages
    // Because in this plot we allow selecting multiple mutations
    // the coverage-adjusted percentage should have, as the denominator,
    // the total number of mutations with *both* mutations covered
    //
    // This isn't really possible with the current range-based setup because
    // the coverage data only tracks counts on a rolling basis
    // for example, let's say we selected mutations at positions 100 and 200,
    // and we had 10 sequences covering [90, 110] and 10 sequences covering [190, 210]
    //
    // From the coverage data we would get 10 as the denominator for both of the
    // sequences separately, but jointly the real denominator would be 0.
    //
    // It's not that big of a deal anyways since this plot is less about comparing mutations
    // to each other and more about comparing frequencies over locations/time

    locationData = aggregate({
      data: locationData,
      groupby: ['location', 'collection_date', 'group'],
      fields: ['counts', 'group_name'],
      ops: ['sum', 'first'],
      as: ['counts', 'group_name'],
    }).map((record) => {
      // Add location counts
      record.location_counts = dataStore.countsPerLocationMap[record.location];
      record.location_date_count = dataStore.countsPerLocationDateMap
        .get(record.location)
        .get(record.collection_date);
      record.cumulative_location_date_count =
        dataStore.cumulativeCountsPerLocationDateMap
          .get(record.location)
          .get(record.collection_date);
      return record;
    });

    // console.log(locationData);
    // console.log(JSON.stringify(locationData));

    return locationData;
  };

  const processSelectedGroups = () => {
    return JSON.parse(JSON.stringify(configStore.selectedGroups));
  };

  const processFocusedLocations = () => {
    return JSON.parse(JSON.stringify(configStore.focusedLocations));
  };

  // There's some weird data persistence things going on with the dateBin
  // in this plot... so instead of passing it as a signal, just modify the
  // spec and force a hard re-render
  const injectDateBinIntoSpec = () => {
    // setState({ ...state, dateBin: event.target.value })
    const spec = JSON.parse(JSON.stringify(initialSpec));
    // Set the date bin
    let dateBin;
    if (plotSettingsStore.locationDateDateBin === DATE_BINS.DATE_BIN_DAY) {
      dateBin = 1000 * 60 * 60 * 24;
    } else if (
      plotSettingsStore.locationDateDateBin === DATE_BINS.DATE_BIN_WEEK
    ) {
      dateBin = 1000 * 60 * 60 * 24 * 7;
    } else if (
      plotSettingsStore.locationDateDateBin === DATE_BINS.DATE_BIN_MONTH
    ) {
      dateBin = 1000 * 60 * 60 * 24 * 30;
    } else if (
      plotSettingsStore.locationDateDateBin === DATE_BINS.DATE_BIN_YEAR
    ) {
      dateBin = 1000 * 60 * 60 * 24 * 365;
    }

    const dateBinSignal = spec.signals.find(
      (signal) => signal.name === 'dateBin'
    );
    dateBinSignal['value'] = dateBin;

    return spec;
  };

  const [state, setState] = useState({
    showWarning: true,
    data: {
      location_data: [],
      selectedGroups: [],
      selected: [],
    },
    hoverLocation: null,
    spec: injectDateBinIntoSpec(),
    signalListeners: {
      hoverLocation: throttle(handleHoverLocation, 100),
    },
    dataListeners: {
      selected: handleSelected,
    },
  });

  const onDismissWarning = () => {
    setState({
      ...state,
      showWarning: false,
    });
  };

  const onChangeNormMode = (event) =>
    plotSettingsStore.setLocationDateNormMode(event.target.value);
  const onChangeCountMode = (event) =>
    plotSettingsStore.setLocationDateCountMode(event.target.value);
  const onChangeDateBin = (event) => {
    plotSettingsStore.setLocationDateDateBin(event.target.value);
  };

  const handleDownloadSelect = (option) => {
    // console.log(option);
    // TODO: use the plot options and configStore options to build a more descriptive filename
    //       something like new_lineages_by_day_S_2020-05-03-2020-05-15_NYC.png...
    if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_DATA) {
      dataStore.downloadAggLocationGroupDate();
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 1);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_2X) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 2);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_4X) {
      vegaRef.current.downloadImage('png', 'vega-export.png', 4);
    } else if (option === PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_SVG) {
      vegaRef.current.downloadImage('svg', 'vega-export.svg');
    }
  };

  // Trigger date bin spec injection when the date bin in the store changes
  useEffect(() => {
    const spec = injectDateBinIntoSpec();
    setState({
      ...state,
      spec,
    });
  }, [plotSettingsStore.locationDateDateBin]);

  useEffect(() => {
    setState({
      ...state,
      hoverLocation: { location: configStore.hoverLocation },
    });
  }, [configStore.hoverLocation]);

  useEffect(() => {
    setState({
      ...state,
      data: {
        ...state.data,
        selected: processFocusedLocations(),
      },
    });
  }, [configStore.focusedLocations]);

  const refreshData = () => {
    if (
      configStore.groupKey === GROUP_MUTATION &&
      UIStore.mutationDataState !== ASYNC_STATES.SUCCEEDED
    ) {
      return;
    }

    setState({
      ...state,
      data: {
        ...state.data,
        location_data: processLocationData(),
        selectedGroups: processSelectedGroups(),
      },
    });
  };

  // Refresh data on mount (i.e., tab change) or when data state changes
  useEffect(refreshData, [
    UIStore.caseDataState,
    UIStore.mutationDataState,
    configStore.selectedGroups,
  ]);
  useEffect(refreshData, []);

  if (UIStore.caseDataState === ASYNC_STATES.STARTED) {
    return (
      <div
        style={{
          paddingTop: '12px',
          paddingRight: '24px',
          paddingLeft: '12px',
          paddingBottom: '24px',
        }}
      >
        <SkeletonElement delay={2} height={400}>
          <LoadingSpinner />
        </SkeletonElement>
      </div>
    );
  }

  if (configStore.selectedLocationNodes.length == 0) {
    return (
      <EmptyPlot height={200}>
        <p>
          No locations selected. Please select one or more locations from the
          sidebar, under &quot;Selected Locations&quot;, to compare counts of{' '}
          <b>{configStore.getGroupLabel()}</b> between them.
        </p>
      </EmptyPlot>
    );
  }

  if (configStore.selectedGroups.length == 0) {
    return (
      <EmptyPlot height={200}>
        <p>
          Please select a <b>{configStore.getGroupLabel()}</b> from the legend
          (above) or the group plot (below), to compare its counts between the
          selected locations.
        </p>
      </EmptyPlot>
    );
  }

  if (
    configStore.groupKey === GROUP_MUTATION &&
    state.data.location_data.length === 0
  ) {
    return (
      <EmptyPlot height={200}>
        <p>
          No sequences with {configStore.getGroupLabel()}s:{' '}
          {`${configStore.selectedGroups
            .map((group) => group.group)
            .join(' & ')}`}
        </p>
      </EmptyPlot>
    );
  }

  let yLabel = '';
  if (
    plotSettingsStore.locationDateCountMode === COUNT_MODES.COUNT_CUMULATIVE
  ) {
    yLabel += 'Cumulative ';
  }
  if (plotSettingsStore.locationDateNormMode === NORM_MODES.NORM_PERCENTAGES) {
    yLabel += '% ';
  }
  if (configStore.groupKey === GROUP_MUTATION) {
    yLabel += 'Sequences with this ' + configStore.getGroupLabel();
  } else {
    yLabel += 'Sequences by ' + configStore.getGroupLabel();
  }

  let plotTitle = '';
  if (
    plotSettingsStore.locationDateCountMode === COUNT_MODES.COUNT_CUMULATIVE
  ) {
    plotTitle += 'Cumulative ';
  } else if (
    plotSettingsStore.locationDateCountMode === COUNT_MODES.COUNT_NEW
  ) {
    plotTitle += 'New ';
  }
  plotTitle += configStore.getGroupLabel();
  plotTitle +=
    plotSettingsStore.locationDateNormMode === NORM_MODES.NORM_PERCENTAGES
      ? ' Percentages'
      : ' Counts';

  if (plotSettingsStore.locationDateDateBin === DATE_BINS.DATE_BIN_DAY) {
    plotTitle += ' by Day';
  } else if (
    plotSettingsStore.locationDateDateBin === DATE_BINS.DATE_BIN_WEEK
  ) {
    plotTitle += ' by Week';
  } else if (
    plotSettingsStore.locationDateDateBin === DATE_BINS.DATE_BIN_MONTH
  ) {
    plotTitle += ' by Month';
  } else if (
    plotSettingsStore.locationDateDateBin === DATE_BINS.DATE_BIN_MONTH
  ) {
    plotTitle += ' by Year';
  }

  if (configStore.selectedGroups.length > 0) {
    if (configStore.groupKey === GROUP_MUTATION) {
      plotTitle += ` (${configStore.selectedGroups
        .map((group) => formatMutation(group.group, configStore.dnaOrAa))
        .join(' & ')})`;
    } else {
      plotTitle += ` (${configStore.selectedGroups
        .map((group) => group.group)
        .join(', ')})`;
    }
  }

  return (
    <PlotContainer>
      <WarningBox show={state.showWarning} onDismiss={onDismissWarning}>
        {config.site_title} plots reflect data contributed to GISAID and are
        therefore impacted by the sequence coverage in each country.
      </WarningBox>
      <PlotOptions>
        <span className="plot-title">{plotTitle}</span>
        <OptionSelectContainer>
          <label>
            <select
              value={plotSettingsStore.locationDateCountMode}
              onChange={onChangeCountMode}
            >
              <option value={COUNT_MODES.COUNT_NEW}>New</option>
              <option value={COUNT_MODES.COUNT_CUMULATIVE}>Cumulative</option>
            </select>
          </label>
        </OptionSelectContainer>
        sequences, shown as{' '}
        <OptionSelectContainer>
          <label>
            <select
              value={plotSettingsStore.locationDateNormMode}
              onChange={onChangeNormMode}
            >
              <option value={NORM_MODES.NORM_COUNTS}>Counts</option>
              <option value={NORM_MODES.NORM_PERCENTAGES}>Percentages</option>
            </select>
          </label>
        </OptionSelectContainer>
        grouped by{' '}
        <OptionSelectContainer>
          <label>
            <select
              value={plotSettingsStore.locationDateDateBin}
              onChange={onChangeDateBin}
            >
              <option value={DATE_BINS.DATE_BIN_DAY}>Day</option>
              <option value={DATE_BINS.DATE_BIN_WEEK}>Week</option>
              <option value={DATE_BINS.DATE_BIN_MONTH}>Month</option>
              <option value={DATE_BINS.DATE_BIN_YEAR}>Year</option>
            </select>
          </label>
        </OptionSelectContainer>
        <div className="spacer"></div>
        <DropdownButton
          text={'Download'}
          options={[
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_DATA,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_2X,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_PNG_4X,
            PLOT_DOWNLOAD_OPTIONS.DOWNLOAD_SVG,
          ]}
          onSelect={handleDownloadSelect}
        />
      </PlotOptions>
      <div style={{ width: `${width}` }}>
        <VegaEmbed
          ref={vegaRef}
          data={state.data}
          recreateOnDatasets={['selectedGroups']}
          spec={state.spec}
          signalListeners={state.signalListeners}
          dataListeners={state.dataListeners}
          width={width}
          signals={{
            dateRangeStart: new Date(config.min_date).getTime() / 1000,
            percentages:
              plotSettingsStore.locationDateNormMode ===
              NORM_MODES.NORM_PERCENTAGES,
            cumulative:
              plotSettingsStore.locationDateCountMode ===
              COUNT_MODES.COUNT_CUMULATIVE,
            skipFiltering: configStore.groupKey === GROUP_MUTATION,
            hoverLocation: state.hoverLocation,
            yLabel,
          }}
          actions={false}
        />
      </div>
    </PlotContainer>
  );
})
Example #26
Source File: LocationGroupPlot.js    From covidcg with MIT License 4 votes vote down vote up
LocationGroupPlot = observer(({ width }) => {
  const vegaRef = useRef();
  const {
    dataStore,
    configStore,
    UIStore,
    plotSettingsStore,
    groupDataStore,
    mutationDataStore,
  } = useStores();

  const handleHoverLocation = (...args) => {
    // Don't fire the action if there's no change
    let hoverLocation = args[1] === null ? null : args[1]['location'];
    if (hoverLocation === configStore.hoverLocation) {
      return;
    }
    configStore.updateHoverLocation(hoverLocation);
  };

  const handleHoverGroup = (...args) => {
    configStore.updateHoverGroup(args[1] === null ? null : args[1]['group']);
  };

  const handleSelectedLocations = (...args) => {
    configStore.updateFocusedLocations(args[1]);
  };

  const handleSelectedGroups = (...args) => {
    configStore.updateSelectedGroups(
      args[1] === null
        ? []
        : args[1].map((item) => {
            return { group: item.group };
          })
    );
  };

  const onChangeHideReference = (e) => {
    plotSettingsStore.setLocationGroupHideReference(e.target.checked);
  };

  const processLocationByGroup = () => {
    //console.log('LOCATION GROUP PLOT PROCESS DATA');

    let locationData;
    if (configStore.groupKey === GROUP_MUTATION) {
      locationData = aggregate({
        data: toJS(dataStore.aggLocationSingleMutationDate),
        groupby: ['location', 'group_id'],
        fields: ['counts'],
        ops: ['sum'],
        as: ['counts'],
      });

      locationData.forEach((record) => {
        let mutation = mutationDataStore.intToMutation(
          configStore.dnaOrAa,
          configStore.coordinateMode,
          record.group_id
        );
        record.color = mutation.color;
        record.group = mutation.mutation_str;
        record.group_name = mutation.name;
      });

      if (plotSettingsStore.locationGroupHideReference) {
        // Filter out 'Reference' group, when in mutation mode
        locationData = locationData.filter((row) => {
          return row.group !== GROUPS.REFERENCE_GROUP;
        });
      }
    } else {
      locationData = aggregate({
        data: toJS(dataStore.aggLocationGroupDate),
        groupby: ['location', 'group_id'],
        fields: ['counts'],
        ops: ['sum'],
        as: ['counts'],
      });
      locationData.forEach((record) => {
        record.color = groupDataStore.getGroupColor(
          configStore.groupKey,
          record.group_id
        );
        record.group = record.group_id;
        record.group_name = record.group_id;
      });
    }

    locationData.forEach((record) => {
      record.location_counts = dataStore.countsPerLocationMap[record.location];
    });

    //console.log(JSON.stringify(locationData));

    return locationData;
  };

  const [state, setState] = useState({
    data: {
      location_by_group: [],
      selectedGroups: [],
      selectedLocations: [],
    },
    hoverGroup: null,
    hoverLocation: null,
    spec: JSON.parse(JSON.stringify(initialSpec)),
    signalListeners: {
      hoverLocation: throttle(handleHoverLocation, 100),
      hoverGroup: debounce(handleHoverGroup, 100),
    },
    dataListeners: {
      selectedLocations: handleSelectedLocations,
      selectedGroups: handleSelectedGroups,
    },
  });

  useEffect(() => {
    setState({
      ...state,
      hoverGroup: { group: configStore.hoverGroup },
    });
  }, [configStore.hoverGroup]);

  useEffect(() => {
    setState({
      ...state,
      hoverLocation: { location: configStore.hoverLocation },
    });
  }, [configStore.hoverLocation]);

  useEffect(() => {
    setState({
      ...state,
      data: {
        ...state.data,
        selectedLocations: toJS(configStore.focusedLocations),
      },
    });
  }, [configStore.focusedLocations]);

  useEffect(() => {
    setState({
      ...state,
      data: {
        ...state.data,
        selectedGroups: toJS(configStore.selectedGroups),
      },
    });
  }, [configStore.selectedGroups]);

  const refreshData = () => {
    if (UIStore.caseDataState !== ASYNC_STATES.SUCCEEDED) {
      return;
    }

    setState({
      ...state,
      data: {
        ...state.data,
        location_by_group: processLocationByGroup(),
        selectedGroups: toJS(configStore.selectedGroups),
      },
    });
  };

  // Refresh data on mount (i.e., tab change) or when data state changes
  useEffect(refreshData, [
    UIStore.caseDataState,
    plotSettingsStore.locationGroupHideReference,
  ]);
  useEffect(refreshData, []);

  if (UIStore.caseDataState === ASYNC_STATES.STARTED) {
    return (
      <div
        style={{
          paddingTop: '12px',
          paddingRight: '24px',
          paddingLeft: '12px',
          paddingBottom: '24px',
        }}
      >
        <SkeletonElement delay={2} height={100} />
      </div>
    );
  }

  if (configStore.selectedLocationNodes.length == 0) {
    return (
      <EmptyPlot height={100}>
        <p>
          No locations selected. Please select one or more locations from the
          sidebar, under &quot;Selected Locations&quot;, to compare counts of{' '}
          <b>{configStore.getGroupLabel()}</b> between them.
        </p>
      </EmptyPlot>
    );
  }

  let xLabel, xLabelFormat, stackOffset;
  if (Object.keys(config.group_cols).includes(configStore.groupKey)) {
    xLabel += `${config.group_cols[configStore.groupKey].title} `;
  } else if (configStore.groupKey === GROUP_MUTATION) {
    if (configStore.dnaOrAa === DNA_OR_AA.DNA) {
      xLabel += 'NT';
    } else {
      xLabel += 'AA';
    }
    xLabel += ' mutation ';
  }
  xLabel += ' (Cumulative, All Sequences)';

  if (configStore.groupKey === GROUP_MUTATION) {
    xLabelFormat = 's';
    stackOffset = 'zero';
    xLabel = `# Sequences with ${configStore.getGroupLabel()} (Cumulative, All Sequences)`;
  } else {
    xLabelFormat = '%';
    stackOffset = 'normalize';
    xLabel = `% Sequences by ${configStore.getGroupLabel()} (Cumulative, All Sequences)`;
  }

  return (
    <PlotContainer>
      <PlotOptions>
        {configStore.groupKey === GROUP_MUTATION && (
          <OptionCheckboxContainer>
            <label>
              <input
                type="checkbox"
                checked={plotSettingsStore.locationGroupHideReference}
                onChange={onChangeHideReference}
              />
              Hide Reference Group
            </label>
          </OptionCheckboxContainer>
        )}
        <div className="spacer" />
      </PlotOptions>
      <div style={{ width: `${width}` }}>
        <VegaEmbed
          ref={vegaRef}
          data={state.data}
          spec={state.spec}
          signalListeners={state.signalListeners}
          dataListeners={state.dataListeners}
          signals={{
            hoverLocation: state.hoverLocation,
            hoverGroup: state.hoverGroup,
            xLabel,
            xLabelFormat,
            stackOffset,
          }}
          width={width}
          actions={false}
        />
      </div>
    </PlotContainer>
  );
})
Example #27
Source File: NumSeqPerLocationLine.js    From covidcg with MIT License 4 votes vote down vote up
NumSeqPerLocationLine = observer(({ width }) => {
  const vegaRef = useRef();
  const { dataStore, configStore, UIStore } = useStores();

  const handleHoverLocation = (...args) => {
    // Don't fire the action if there's no change
    let hoverLocation = args[1] === null ? null : args[1]['location'];
    if (hoverLocation === configStore.hoverLocation) {
      return;
    }
    configStore.updateHoverLocation(hoverLocation);
  };

  const processLocation = () => {
    let dat = toJS(dataStore.countsPerLocationDateMap);
    let dateTransformedData = [];
    // Get list of date for each key and iterate through all lists.
    // Potentially optimizable.
    dat.forEach((value, key) => {
      value.forEach((amount, date) => {
        // Convert date from unix time stamp to human readable, vega useable.
        dateTransformedData.push({ c: key, x: date, y: amount });
      });
    });
    dateTransformedData.sort(function (a, b) {
      let textA = a.c.toUpperCase();
      let textB = b.c.toUpperCase();
      return textA > textB ? -1 : textA < textB ? 1 : 0;
    });
    return dateTransformedData;
  };

  const [state, setState] = useState({
    data: {
      line_data: [],
    },
    hoverLocation: null,
    spec: JSON.parse(JSON.stringify(initialSpec)),
    signalListeners: {
      hoverLocation: throttle(handleHoverLocation, 100),
    },
  });

  useEffect(() => {
    setState({
      ...state,
      hoverLocation: { location: configStore.hoverLocation },
    });
  }, [configStore.hoverLocation]);

  useEffect(() => {
    setState({
      ...state,
      data: {
        ...state.data,
      },
    });
  }, [configStore.focusedLocations]);

  const refreshData = () => {
    if (UIStore.caseDataState !== ASYNC_STATES.SUCCEEDED) {
      return;
    }

    setState({
      ...state,
      data: {
        ...state.data,
        line_data: processLocation(),
      },
    });
  };

  // Refresh data on mount (i.e., tab change) or when data state changes
  useEffect(refreshData, [UIStore.caseDataState]);
  useEffect(refreshData, []);

  if (UIStore.caseDataState === ASYNC_STATES.STARTED) {
    return (
      <div
        style={{
          paddingTop: '12px',
          paddingRight: '24px',
          paddingLeft: '12px',
          paddingBottom: '24px',
        }}
      >
        <SkeletonElement delay={2} height={100} />
      </div>
    );
  }

  if (configStore.selectedLocationNodes.length == 0) {
    return (
      <EmptyPlot height={100}>
        <p>
          No locations selected. Please select one or more locations from the
          sidebar, under &quot;Selected Locations&quot;, to compare counts of{' '}
          <b>{configStore.getGroupLabel()}</b> between them.
        </p>
      </EmptyPlot>
    );
  }
  return (
    <PlotContainer>
      <PlotOptions>
        <div className="spacer" />
      </PlotOptions>
      <div style={{ width: `${width}` }}>
        <VegaEmbed
          ref={vegaRef}
          data={state.data}
          spec={state.spec}
          signals={{
            hoverLocation: state.hoverLocation,
          }}
          signalListeners={state.signalListeners}
          dataListeners={state.dataListeners}
          //width={width}
          actions={false}
        />
      </div>
    </PlotContainer>
  );
})
Example #28
Source File: configStore.js    From covidcg with MIT License 4 votes vote down vote up
getCoordinateRanges() {
    // Set the coordinate range based off the coordinate mode
    if (this.coordinateMode === COORDINATE_MODES.COORD_GENE) {
      // Return ranges if All Genes
      if (this.selectedGene.name === 'All Genes') {
        return this.selectedGene.ranges;
      }
      // Disable residue indices for non-protein-coding genes
      if (!this.selectedGene.protein_coding) {
        return this.selectedGene.segments;
      }
      const coordinateRanges = [];
      this.residueCoordinates.forEach((range) => {
        // Make a deep copy of the current range
        const curRange = range.slice();

        if (this.dnaOrAa === DNA_OR_AA.DNA) {
          for (let i = 0; i < this.selectedGene.aa_ranges.length; i++) {
            const curAARange = this.selectedGene.aa_ranges[i];
            const curNTRange = this.selectedGene.segments[i];
            if (
              (curRange[0] >= curAARange[0] && curRange[0] <= curAARange[1]) ||
              (curRange[0] <= curAARange[0] && curRange[1] >= curAARange[0])
            ) {
              coordinateRanges.push([
                curNTRange[0] + (curRange[0] - curAARange[0]) * 3,
                curNTRange[0] -
                  1 +
                  Math.min(curRange[1] - curAARange[0] + 1, curAARange[1]) * 3,
              ]);
              // Push the beginning of the current range to the end of
              // the current AA range of the gene
              if (curAARange[1] < curRange[1]) {
                curRange[0] = curAARange[1] + 1;
              }
            }
          }
        } else {
          coordinateRanges.push([curRange[0], curRange[1]]);
        }
      });
      return coordinateRanges;
    } else if (this.coordinateMode === COORDINATE_MODES.COORD_PROTEIN) {
      const coordinateRanges = [];
      this.residueCoordinates.forEach((range) => {
        // Make a deep copy of the current range
        const curRange = range.slice();

        if (this.dnaOrAa === DNA_OR_AA.DNA) {
          for (let i = 0; i < this.selectedProtein.aa_ranges.length; i++) {
            const curAARange = this.selectedProtein.aa_ranges[i];
            const curNTRange = this.selectedProtein.segments[i];
            if (
              (curRange[0] >= curAARange[0] && curRange[0] <= curAARange[1]) ||
              (curRange[0] <= curAARange[0] && curRange[1] >= curAARange[0])
            ) {
              coordinateRanges.push([
                curNTRange[0] + (curRange[0] - curAARange[0]) * 3,
                curNTRange[0] -
                  1 +
                  Math.min(curRange[1] - curAARange[0] + 1, curAARange[1]) * 3,
              ]);
              // Push the beginning of the current range to the end of
              // the current AA range of the gene
              if (curAARange[1] < curRange[1]) {
                curRange[0] = curAARange[1] + 1;
              }
            }
          }
        } else {
          coordinateRanges.push([curRange[0], curRange[1]]);
        }
      });
      return coordinateRanges;
    } else if (this.coordinateMode === COORDINATE_MODES.COORD_PRIMER) {
      return this.selectedPrimers.map((primer) => {
        return [primer.Start, primer.End];
      });
    } else if (this.coordinateMode === COORDINATE_MODES.COORD_CUSTOM) {
      return toJS(this.customCoordinates);
    } else if (this.coordinateMode === COORDINATE_MODES.COORD_SEQUENCE) {
      return this.customSequences.map((seq) => {
        return queryReferenceSequence(seq, this.selectedReference);
      });
    }
  }
Example #29
Source File: runtime.js    From apps-ng with Apache License 2.0 4 votes vote down vote up
createAppRuntimeStore = (defaultValue = {}) => {
  const AppRuntimeStore = types
    .model('RuntimeStore', {
      ecdhChannel: types.maybeNull(anyType),
      ecdhShouldJoin: types.optional(types.boolean, false),
      latency: types.optional(types.number, 0),
      info: types.maybeNull(anyType),
      error: types.maybeNull(anyType),
      pApi: types.maybeNull(anyType),
    })
    .views(self => ({
      get runtimeEndpointUrl () {
        return self.appSettings.phalaTeeApiUrl
      },
      get appSettings () {
        return defaultValue.appSettings
      },
      get appAccount () {
        return defaultValue.appAccount
      },
      get accountId () {
        return self.appAccount.address
      },
      get keypair () {
        return self.appAccount.keypair
      },
      get accountIdHex () {
        if (!self.accountId) { return null }
        return ss58ToHex(self.accountId)
      },
      get channelReady () {
        if (!self.ecdhChannel || !self.ecdhChannel.core.agreedSecret || !self.ecdhChannel.core.remotePubkey) {
          console.warn('ECDH not ready')
          return false
        }
        if (!self.keypair || self.keypair.isLocked) {
          console.warn('Account not ready')
          return false
        }
        if (!self.pApi) {
          console.warn('pRuntime not ready')
          return false
        }
        return true
      }
    }))
    .actions(self => ({
      checkChannelReady () {
        if (!self.ecdhChannel || !self.ecdhChannel.core.agreedSecret || !self.ecdhChannel.core.remotePubkey) {
          throw new Error('ECDH not ready')
        }
        if (!self.keypair || self.keypair.isLocked) {
          throw new Error('Account not ready')
        }
        if (!self.pApi) {
          throw new Error('pRuntime not ready')
        }
      },
      async query (contractId, name, getPayload) {
        self.checkChannelReady()
        const data = getPayload ? { [name]: getPayload() } : name
        return self.pApi.query(contractId, data)
      },
      initEcdhChannel: flow(function* () {
        const ch = yield Crypto.newChannel()
        self.ecdhChannel = ch
        self.ecdhShouldJoin = true
      }),
      joinEcdhChannel: flow(function* () {
        const ch = yield Crypto.joinChannel(self.ecdhChannel, self.info.ecdhPublicKey)
        self.ecdhChannel = ch
        self.ecdhShouldJoin = false
        console.log('Joined channel:', toJS(ch))
      }),
      initPApi (endpoint) {
        self.pApi = new PRuntime({
          endpoint,
          channel: self.ecdhChannel,
          keypair: self.keypair
        })
      },
      resetNetwork () {
        self.error = null
        self.latency = 0
        self.info = null
      },
      setInfo (i) {
        self.info = i
      },
      setError (e) {
        self.error = e
      },
      setLatency (dt) {
        self.latency = parseInt((l => l ? l * 0.8 + dt * 0.2 : dt)(self.latency))
      }
    }))

  return AppRuntimeStore.create(defaultValue)
}