diff --git a/src/components/Collection/index.tsx b/src/components/Collection/index.tsx new file mode 100644 index 00000000..15f0bd7b --- /dev/null +++ b/src/components/Collection/index.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import Data from '../../context/Data'; +import { ItemProps, WATCHED_PROP_NAMES, Props as SeriesProps } from '../Series'; +import { ItemId } from '../../external'; +import { withDisplayName } from '../../utils/displayName'; + +export interface Props extends ItemProps { + id: ItemId; +} + +type UnregisterCollectionFunction = () => void; + +type RegisterCollectionFunction = ( + collectionProps: Props +) => UnregisterCollectionFunction; + +type UpdateCollectionFunction = (collectionProps: Props) => void; + +interface InternalProps { + registerCollection: RegisterCollectionFunction; + updateCollection: UpdateCollectionFunction; + children?: React.ReactNode[]; +} + +// @ts-ignore - I don't know how to make TypeScript happy about ...props +const Collection: React.FunctionComponent = ({ + id, + + registerCollection, + updateCollection, + children, + + ...props +}) => { + React.useEffect(() => { + return registerCollection({ + id, + ...props, + }); + }, []); + + React.useEffect(() => { + return updateCollection({ + id, + ...props, + }); + // @ts-ignore - It's okay for props[name] to be implicit any. + }, WATCHED_PROP_NAMES.map(name => props[name])); + + if (React.Children.count(children) === 0) { + return null; + } + + return React.Children.map(children, child => { + if (!child || !React.isValidElement(child)) { + return null; + } + return React.cloneElement(child as React.ReactElement, { + ...child.props, + collectionId: id, + }); + }); +}; + +export default withDisplayName( + 'Collection', + (props: Props & { children: React.ReactNode[] }) => ( + + {({ registerCollection, updateCollection }: InternalProps) => ( + + {props.children} + + )} + + ) +); diff --git a/src/components/ContextChart/index.js b/src/components/ContextChart/index.js index 336e27d8..48635063 100644 --- a/src/components/ContextChart/index.js +++ b/src/components/ContextChart/index.js @@ -11,7 +11,7 @@ import AxisPlacement from '../AxisPlacement'; import { multiFormat } from '../../utils/multiFormat'; import Axes from '../../utils/Axes'; import { createYScale, createXScale } from '../../utils/scale-helpers'; -import { stripPlaceholderDomain } from '../Scaler'; +import { firstResolvedDomain } from '../Scaler'; import { calculateDomainFromData } from '../DataProvider'; import { withDisplayName } from '../../utils/displayName'; @@ -71,7 +71,7 @@ const renderXAxis = (position, xAxis, { xAxisPlacement }) => { return xAxis; } if (xAxisPlacement === AxisPlacement.BOTH) { - return React.cloneElement(xAxis, { xAxisPlacement: position }); + return React.cloneElement(xAxis, { placement: position }); } return null; }; @@ -91,8 +91,10 @@ const ContextChart = ({ }) => { const getYScale = (s, height) => { const domain = - stripPlaceholderDomain(s.yDomain) || - stripPlaceholderDomain(Axes.y(domainsByItemId[s.collectionId || s.id])) || + firstResolvedDomain( + s.yDomain, + Axes.y(domainsByItemId[s.collectionId || s.id]) + ) || calculateDomainFromData(s.data, s.yAccessor, s.y0Accessor, s.y1Accessor); return createYScale(domain, height); }; diff --git a/src/components/DataProvider/index.js b/src/components/DataProvider/index.js index c25d0649..84170c15 100644 --- a/src/components/DataProvider/index.js +++ b/src/components/DataProvider/index.js @@ -4,14 +4,15 @@ import Promise from 'bluebird'; import * as d3 from 'd3'; import isEqual from 'lodash.isequal'; import DataContext from '../../context/Data'; -import GriffPropTypes, { seriesPropType } from '../../utils/proptypes'; -import Scaler, { PLACEHOLDER_DOMAIN } from '../Scaler'; +import Scaler from '../Scaler'; +import Series from '../Series'; +import Collection from '../Collection'; export const calculateDomainFromData = ( data, accessor, - minAccessor = null, - maxAccessor = null + minAccessor = undefined, + maxAccessor = undefined ) => { // if there is no data, hard code the domain if (!data || !data.length) { @@ -43,13 +44,12 @@ const deleteUndefinedFromObject = obj => { if (!obj) { return {}; } - const newObject = {}; - Object.keys(obj).forEach(k => { + return Object.keys(obj).reduce((acc, k) => { if (obj[k] !== undefined) { - newObject[k] = obj[k]; + return { ...acc, [k]: obj[k] }; } - }); - return newObject; + return acc; + }, {}); }; /** @@ -64,63 +64,94 @@ const firstDefined = (first, ...others) => { return firstDefined(others[0], ...others.splice(1)); }; +const getTimeSubDomain = ( + timeDomain, + timeSubDomain, + // eslint-disable-next-line no-shadow + limitTimeSubDomain = timeSubDomain => timeSubDomain +) => { + if (!timeSubDomain) { + return timeDomain; + } + const newTimeSubDomain = limitTimeSubDomain(timeSubDomain); + const timeDomainLength = timeDomain[1] - timeDomain[0]; + const timeSubDomainLength = newTimeSubDomain[1] - newTimeSubDomain[0]; + if (timeDomainLength < timeSubDomainLength) { + return timeDomain; + } + if (newTimeSubDomain[0] < timeDomain[0]) { + return [timeDomain[0], timeDomain[0] + timeSubDomainLength]; + } + if (newTimeSubDomain[1] > timeDomain[1]) { + return [timeDomain[1] - timeSubDomainLength, timeDomain[1]]; + } + return newTimeSubDomain; +}; + +const smallerDomain = (domain, subDomain) => { + if (!domain && !subDomain) { + return undefined; + } + + if (!domain || !subDomain) { + return domain || subDomain; + } + + return [Math.max(domain[0], subDomain[0]), Math.min(domain[1], subDomain[1])]; +}; + +const boundedDomain = (a, b) => + a && b ? [Math.min(a[0], b[0]), Math.max(a[1], b[1])] : a || b; + +const DEFAULT_ACCESSORS = { + time: d => d.timestamp, + x: d => d.x, + y: d => d.value, +}; + +const DEFAULT_SERIES_CONFIG = { + color: 'black', + data: [], + hidden: false, + drawPoints: false, + timeAccessor: DEFAULT_ACCESSORS.time, + xAccessor: DEFAULT_ACCESSORS.x, + yAccessor: DEFAULT_ACCESSORS.y, + timeDomain: undefined, + timeSubDomain: undefined, + xDomain: undefined, + xSubDomain: undefined, + yDomain: undefined, + ySubDomain: undefined, + pointWidth: 6, + strokeWidth: 1, +}; + export default class DataProvider extends Component { constructor(props) { super(props); const { limitTimeSubDomain, timeDomain, timeSubDomain } = props; this.state = { - timeSubDomain: DataProvider.getTimeSubDomain( + timeSubDomain: getTimeSubDomain( timeDomain, timeSubDomain, limitTimeSubDomain ), timeDomain, - loaderConfig: {}, - timeDomains: {}, timeSubDomains: {}, - xDomains: {}, xSubDomains: {}, - yDomains: {}, ySubDomains: {}, + collectionsById: {}, + seriesById: {}, }; } - static getDerivedStateFromProps(nextProps, prevState) { - // Check if one of the series got removed from props - // If so, delete the respective keys in loaderconfig - // This is important so we don't cache the values if it gets readded later - const { loaderConfig, yDomains, ySubDomains } = prevState; - const { series } = nextProps; - const seriesKeys = {}; - series.forEach(s => { - seriesKeys[s.id] = true; - }); - const newLoaderConfig = { ...loaderConfig }; - const newYDomains = { ...yDomains }; - const newYSubDomains = { ...ySubDomains }; - let shouldUpdate = false; - Object.keys(loaderConfig).forEach(key => { - if (!seriesKeys[key]) { - // Clean up - delete newLoaderConfig[key]; - delete newYDomains[key]; - shouldUpdate = true; - } - }); - if (shouldUpdate) { - return { - loaderConfig: newLoaderConfig, - yDomains: newYDomains, - ySubDomains: newYSubDomains, - }; - } - return null; - } + componentDidMount() { + const { updateInterval } = this.props; - async componentDidMount() { - const { series } = this.props; - this.startUpdateInterval(); - await Promise.map(series, s => this.fetchData(s.id, 'MOUNTED')); + if (updateInterval) { + this.startUpdateInterval(); + } } async componentDidUpdate(prevProps) { @@ -137,12 +168,7 @@ export default class DataProvider extends Component { } = this.props; const { updateInterval: prevUpdateInterval } = prevProps; if (updateInterval !== prevUpdateInterval) { - if (prevUpdateInterval) { - clearInterval(this.fetchInterval); - } - if (updateInterval) { - this.startUpdateInterval(); - } + this.startUpdateInterval(); } // check if pointsPerSeries changed in props -- if so fetch new data @@ -152,37 +178,15 @@ export default class DataProvider extends Component { ); } - const { series: prevSeries } = prevProps; - if (!prevSeries) { - return; - } - const { timeSubDomain, timeDomain } = this.state; - if (!isEqual(propsTimeSubDomain, prevProps.timeSubDomain)) { this.timeSubDomainChanged(propsTimeSubDomain); } - const currentSeriesKeys = {}; - series.forEach(s => { - currentSeriesKeys[s.id] = true; - }); - const prevSeriesKeys = {}; - prevSeries.forEach(p => { - prevSeriesKeys[p.id] = true; - }); - const newSeries = series.filter(s => prevSeriesKeys[s.id] !== true); - await Promise.map(newSeries, async ({ id }) => { - await this.fetchData(id, 'MOUNTED'); - if (!isEqual(timeSubDomain, timeDomain)) { - // The series got added when zoomed in, - // Need to also fetch a higher-granularity version on mount - await this.fetchData(id, 'UPDATE_SUBDOMAIN'); - } - }); - // Check if timeDomain changed in props -- if so reset state. if (!isEqual(propsTimeDomain, prevProps.timeDomain)) { - const newTimeSubDomain = DataProvider.getTimeSubDomain( + const { seriesById } = this.state; + + const newTimeSubDomain = getTimeSubDomain( propsTimeDomain, propsTimeSubDomain, limitTimeSubDomain @@ -192,12 +196,10 @@ export default class DataProvider extends Component { { timeDomain: propsTimeDomain, timeSubDomain: newTimeSubDomain, - loaderConfig: {}, - yDomains: {}, ySubDomains: {}, }, () => { - series.map(s => this.fetchData(s.id, 'MOUNTED')); + Object.keys(seriesById).map(id => this.fetchData(id, 'MOUNTED')); if (onTimeSubDomainChanged) { onTimeSubDomainChanged(newTimeSubDomain); } @@ -208,241 +210,126 @@ export default class DataProvider extends Component { } componentWillUnmount() { - clearInterval(this.fetchInterval); - } - - static getTimeSubDomain = ( - timeDomain, - timeSubDomain, - // eslint-disable-next-line no-shadow - limitTimeSubDomain = timeSubDomain => timeSubDomain - ) => { - if (!timeSubDomain) { - return timeDomain; - } - const newTimeSubDomain = limitTimeSubDomain(timeSubDomain); - const timeDomainLength = timeDomain[1] - timeDomain[0]; - const timeSubDomainLength = newTimeSubDomain[1] - newTimeSubDomain[0]; - if (timeDomainLength < timeSubDomainLength) { - return timeDomain; - } - if (newTimeSubDomain[0] < timeDomain[0]) { - return [timeDomain[0], timeDomain[0] + timeSubDomainLength]; - } - if (newTimeSubDomain[1] > timeDomain[1]) { - return [timeDomain[1] - timeSubDomainLength, timeDomain[1]]; - } - return newTimeSubDomain; - }; - - getSeriesObjects = () => { - const { collections, series } = this.props; - const collectionsById = {}; - (collections || []).forEach(c => { - collectionsById[c.id] = c; - }); - return series.map(s => - this.enrichSeries(s, collectionsById[s.collectionId || ''] || {}) - ); - }; - - getSingleSeriesObject = id => { - const { collections, series: propsSeries } = this.props; - const series = propsSeries.find(s => id === s.id); - if (!series) { - throw new Error( - `Trying to get single series object for id ${id} which is not defined in props.` - ); - } - return this.enrichSeries( - series, - series.collectionId - ? (collections || []).find(c => series.collectionId === c.id) - : {} - ); - }; - - startUpdateInterval = () => { - const { - isTimeSubDomainSticky, - limitTimeSubDomain, - series, - updateInterval, - } = this.props; - if (updateInterval) { + if (this.fetchInterval) { clearInterval(this.fetchInterval); - this.fetchInterval = setInterval(() => { - const { timeDomain, timeSubDomain } = this.state; - const newTimeDomain = timeDomain.map(d => d + updateInterval); - const newTimeSubDomain = isTimeSubDomainSticky - ? DataProvider.getTimeSubDomain( - newTimeDomain, - timeSubDomain.map(d => d + updateInterval), - limitTimeSubDomain - ) - : timeSubDomain; - this.setState( - { - timeDomain: newTimeDomain, - timeSubDomain: newTimeSubDomain, - }, - () => { - series.map(s => this.fetchData(s.id, 'INTERVAL')); - } - ); - }, updateInterval); } - }; + } - enrichSeries = (series, collection = {}) => { + getSeriesObjects = () => { const { - drawPoints, drawLines, - opacity, - opacityAccessor, - pointWidth, - pointWidthAccessor, - strokeWidth, + drawPoints, timeAccessor, - timeDomain: propTimeDomain, - timeSubDomain, xAccessor, x0Accessor, x1Accessor, - xDomain: propXDomain, - xSubDomain: propXSubDomain, + yAccessor, y0Accessor, y1Accessor, - yAccessor, - yDomain: propYDomain, - ySubDomain: propYSubDomain, + timeDomain, + timeSubDomain, + xDomain, + xSubDomain, + yDomain, + ySubDomain, + pointWidth, + strokeWidth, + opacity, + opacityAccessor, + pointWidthAccessor, } = this.props; const { - loaderConfig, - timeDomains, + collectionsById, + seriesById, timeSubDomains, - xDomains, xSubDomains, - yDomains, ySubDomains, } = this.state; - const yDomain = - collection.yDomain || - series.yDomain || - propYDomain || - yDomains[series.id] || - PLACEHOLDER_DOMAIN; - const xDomain = - collection.xDomain || - series.xDomain || - propXDomain || - xDomains[series.id] || - PLACEHOLDER_DOMAIN; - const timeDomain = - collection.timeDomain || - series.timeDomain || - timeDomains[series.id] || - propTimeDomain || - PLACEHOLDER_DOMAIN; - return { - hidden: collection.hidden, - data: [], - ...deleteUndefinedFromObject(loaderConfig[series.id]), - ...deleteUndefinedFromObject(series), - drawPoints: firstDefined( - (loaderConfig[series.id] || {}).drawPoints, - series.drawPoints, - collection.drawPoints, - drawPoints - ), - drawLines: firstDefined( - (loaderConfig[series.id] || {}).drawLines, - series.drawLines, - collection.drawLines, - drawLines - ), - timeAccessor: firstDefined( - series.timeAccessor, - collection.timeAccessor, - timeAccessor - ), - xAccessor: firstDefined( - series.xAccessor, - collection.xAccessor, - xAccessor - ), - x0Accessor: firstDefined( - series.x0Accessor, - collection.x0Accessor, - x0Accessor - ), - x1Accessor: firstDefined( - series.x1Accessor, - collection.x1Accessor, - x1Accessor - ), - yAccessor: firstDefined( - series.yAccessor, - collection.yAccessor, - yAccessor - ), - y0Accessor: firstDefined( - series.y0Accessor, - collection.y0Accessor, - y0Accessor - ), - y1Accessor: firstDefined( - series.y1Accessor, - collection.y1Accessor, - y1Accessor - ), - strokeWidth: firstDefined( - series.strokeWidth, - collection.strokeWidth, - strokeWidth - ), - pointWidth: firstDefined( - series.pointWidth, - collection.pointWidth, - pointWidth - ), - pointWidthAccessor: firstDefined( - series.pointWidthAccessor, - collection.pointWidthAccessor, - pointWidthAccessor - ), - opacity: firstDefined(series.opacity, collection.opacity, opacity), - opacityAccessor: firstDefined( - series.opacityAccessor, - collection.opacityAccessor, - opacityAccessor - ), - yAxisDisplayMode: - (series.collectionId - ? collection.yAxisDisplayMode - : series.yAxisDisplayMode) || collection.yAxisDisplayMode, - timeDomain, - timeSubDomain: - collection.timeSubDomain || - series.timeSubDomain || - timeSubDomains[series.id] || - timeSubDomain || + return Object.keys(seriesById).reduce((acc, id) => { + const series = seriesById[id]; + const dataProvider = { + drawLines, + drawPoints, + pointWidth, + strokeWidth, + opacity, + opacityAccessor, + pointWidthAccessor, + timeAccessor, + xAccessor, + x0Accessor, + x1Accessor, + yAccessor, + y0Accessor, + y1Accessor, + }; + const collection = + series.collectionId !== undefined + ? collectionsById[series.collectionId] || {} + : {}; + const completedSeries = { + // First copy in the base-level configuration. + ...DEFAULT_SERIES_CONFIG, + + // Then the global props from DataProvider, if any are set. + ...dataProvider, + + // Then the domains because these are in the DataProvider state, which + // supercedes the props. + timeSubDomain: smallerDomain( + timeDomain, + timeSubDomain || timeSubDomains[id] + ), + xSubDomain: smallerDomain(xDomain, xSubDomain || xSubDomains[id]), + ySubDomain: smallerDomain(yDomain, ySubDomain || ySubDomains[id]), timeDomain, - xDomain, - xSubDomain: - collection.xSubDomain || - series.xSubDomain || - propXSubDomain || - xSubDomains[series.id] || xDomain, - yDomain, - ySubDomain: - collection.ySubDomain || - series.ySubDomain || - propYSubDomain || - ySubDomains[series.id] || yDomain, - }; + + // Next, copy over defaults from the parent collection, if there is one. + ...collection, + + // Finally, the series configuration itself. + ...series, + }; + return [...acc, completedSeries]; + }, []); + }; + + onUpdateInterval = () => { + const { + isTimeSubDomainSticky, + limitTimeSubDomain, + updateInterval, + } = this.props; + const { seriesById, timeDomain, timeSubDomain } = this.state; + const newTimeDomain = timeDomain.map(d => d + updateInterval); + const newTimeSubDomain = isTimeSubDomainSticky + ? getTimeSubDomain( + newTimeDomain, + timeSubDomain.map(d => d + updateInterval), + limitTimeSubDomain + ) + : timeSubDomain; + this.setState( + { + timeDomain: newTimeDomain, + timeSubDomain: newTimeSubDomain, + }, + () => { + Object.keys(seriesById).map(id => this.fetchData(id, 'INTERVAL')); + } + ); + }; + + startUpdateInterval = () => { + const { updateInterval } = this.props; + if (this.fetchInterval) { + clearInterval(this.fetchInterval); + } + if (updateInterval) { + this.fetchInterval = setInterval(this.onUpdateInterval, updateInterval); + } }; fetchData = async (id, reason) => { @@ -459,8 +346,11 @@ export default class DataProvider extends Component { yAccessor, onFetchDataError, } = this.props; - const { timeDomain, timeSubDomain } = this.state; - const seriesObject = this.getSingleSeriesObject(id); + const { timeDomain, timeSubDomain, seriesById } = this.state; + const seriesObject = seriesById[id]; + if (!seriesObject) { + return; + } const loader = seriesObject.loader || defaultLoader; if (!loader) { throw new Error(`Series ${id} does not have a loader.`); @@ -471,7 +361,7 @@ export default class DataProvider extends Component { timeDomain, timeSubDomain, pointsPerSeries, - oldSeries: seriesObject, + oldSeries: { data: [], ...seriesObject }, reason, }; try { @@ -479,80 +369,88 @@ export default class DataProvider extends Component { } catch (e) { onFetchDataError(e, params); } - // This needs to happen after the loader comes back because the state can - // change while the load function is operating. If we make a copy of the - // state before the loader executes, then we'll trample any updates which - // may have happened while the loader was loading. - const { loaderConfig: originalLoaderConfig } = this.state; - const loaderConfig = { - data: [], - id, - ...loaderResult, - reason, - yAccessor: seriesObject.yAccessor, - y0Accessor: seriesObject.y0Accessor, - y1Accessor: seriesObject.y1Accessor, - }; - const stateUpdates = {}; - if ( - reason === 'MOUNTED' || - (seriesObject.data.length === 0 && loaderConfig.data.length > 0) - ) { - const { - timeDomains, - timeSubDomains, - xSubDomains, - ySubDomains, - } = this.state; - const calculatedTimeDomain = calculateDomainFromData( - loaderConfig.data, - loaderConfig.timeAccessor || timeAccessor - ); - const calculatedTimeSubDomain = calculatedTimeDomain; - stateUpdates.timeDomains = { - ...timeDomains, - [id]: calculatedTimeDomain, - }; - stateUpdates.timeSubDomains = { - ...timeSubDomains, - [id]: calculatedTimeSubDomain, - }; - const xSubDomain = calculateDomainFromData( - loaderConfig.data, - loaderConfig.xAccessor || xAccessor, - loaderConfig.x0Accessor || x0Accessor, - loaderConfig.x1Accessor || x1Accessor - ); - stateUpdates.xSubDomains = { - ...xSubDomains, - [id]: xSubDomain, - }; + this.setState( + ({ + collectionsById, + seriesById: { [id]: freshSeries }, + seriesById: freshSeriesById, + timeSubDomains: freshTimeSubDomains, + xSubDomains: freshXSubDomains, + ySubDomains: freshYSubDomains, + }) => { + const stateUpdates = {}; + + const series = { + ...freshSeries, + ...loaderResult, + }; - const ySubDomain = calculateDomainFromData( - loaderConfig.data, - loaderConfig.yAccessor || yAccessor, - loaderConfig.y0Accessor || y0Accessor, - loaderConfig.y1Accessor || y1Accessor - ); - stateUpdates.ySubDomains = { - ...ySubDomains, - [id]: ySubDomain, - }; - } - stateUpdates.loaderConfig = { - ...originalLoaderConfig, - [id]: { ...loaderConfig }, - }; - this.setState(stateUpdates, () => { - onFetchData({ ...loaderConfig }); - }); + if ( + // We either couldn't have any data before ... + reason === 'MOUNTED' || + // ... or we didn't have data before, but do now! + (freshSeries.data.length === 0 && loaderResult.data.length > 0) + ) { + const collection = series.collectionId + ? collectionsById[series.collectionId] || {} + : {}; + + stateUpdates.timeSubDomains = { + ...freshTimeSubDomains, + [id]: calculateDomainFromData( + series.data, + series.timeAccessor || timeAccessor || DEFAULT_ACCESSORS.time + ), + }; + stateUpdates.xSubDomains = { + ...freshXSubDomains, + [id]: calculateDomainFromData( + series.data, + series.xAccessor || + collection.xAccessor || + xAccessor || + DEFAULT_ACCESSORS.x, + series.x0Accessor || collection.x0Accessor || x0Accessor, + series.x1Accessor || collection.x1Accessor || x1Accessor + ), + }; + stateUpdates.ySubDomains = { + ...freshYSubDomains, + [id]: calculateDomainFromData( + series.data, + series.yAccessor || + collection.yAccessor || + yAccessor || + DEFAULT_ACCESSORS.y, + series.y0Accessor || collection.y0Accessor || y0Accessor, + series.y1Accessor || collection.y1Accessor || y1Accessor + ), + }; + + series.timeSubDomain = series.timeSubDomain || series.timeDomain; + } + + stateUpdates.seriesById = { + ...freshSeriesById, + [id]: series, + }; + + return stateUpdates; + }, + () => { + const { + seriesById: { [id]: series }, + } = this.state; + onFetchData({ ...series }); + } + ); }; timeSubDomainChanged = timeSubDomain => { - const { limitTimeSubDomain, onTimeSubDomainChanged, series } = this.props; - const { timeDomain, timeSubDomain: current } = this.state; - const newTimeSubDomain = DataProvider.getTimeSubDomain( + const { limitTimeSubDomain, onTimeSubDomainChanged } = this.props; + const { timeDomain, timeSubDomain: current, seriesById } = this.state; + const newTimeSubDomain = getTimeSubDomain( timeDomain, timeSubDomain, limitTimeSubDomain @@ -563,7 +461,10 @@ export default class DataProvider extends Component { clearTimeout(this.timeSubDomainChangedTimeout); this.timeSubDomainChangedTimeout = setTimeout( - () => series.map(s => this.fetchData(s.id, 'UPDATE_SUBDOMAIN')), + () => + Object.keys(seriesById).map(id => + this.fetchData(id, 'UPDATE_SUBDOMAIN') + ), 250 ); this.setState({ timeSubDomain: newTimeSubDomain }, () => { @@ -573,11 +474,107 @@ export default class DataProvider extends Component { }); }; + registerCollection = ({ id, ...collection }) => { + this.setState(({ collectionsById }) => ({ + collectionsById: { + ...collectionsById, + [id]: deleteUndefinedFromObject({ + ...collection, + id, + }), + }, + })); + + // Return an unregistration so that we can do some cleanup. + return () => { + this.setState(({ collectionsById }) => { + const copy = { ...collectionsById }; + delete copy[id]; + return { + collectionsById: copy, + }; + }); + }; + }; + + updateCollection = ({ id, ...collection }) => { + this.setState(({ collectionsById }) => ({ + collectionsById: { + ...collectionsById, + [id]: deleteUndefinedFromObject({ + ...collectionsById[id], + ...collection, + id, + }), + }, + })); + }; + + registerSeries = ({ id, ...series }) => { + this.setState( + ({ seriesById }) => ({ + seriesById: { + ...seriesById, + [id]: deleteUndefinedFromObject({ + ...series, + id, + }), + }, + }), + () => { + this.fetchData(id, 'MOUNTED'); + } + ); + + // Return an unregistration so that we can do some cleanup. + return () => { + this.setState(({ seriesById }) => { + const copy = { ...seriesById }; + delete copy[id]; + return { + seriesById: copy, + }; + }); + }; + }; + + updateSeries = ({ id, ...series }) => { + this.setState(({ seriesById }) => ({ + seriesById: { + ...seriesById, + [id]: deleteUndefinedFromObject({ + ...seriesById[id], + ...series, + id, + }), + }, + })); + }; + + // Add a helper method to render the legacy props using the new tree structure + // format. This is only intended to ease the transition pain and is not + // intended to be an ongoing solution. + renderLegacyItems = () => { + const { series, collections } = this.props; + if (series || collections) { + return ( + + {(series || []).map(s => ( + + ))} + {(collections || []).map(c => ( + + ))} + + ); + } + return null; + }; + render() { - const { loaderConfig, timeDomain, timeSubDomain } = this.state; + const { collectionsById, timeDomain, timeSubDomain } = this.state; const { children, - collections, limitTimeSubDomain, timeDomain: externalTimeDomain, timeSubDomain: externalTimeSubDomain, @@ -585,78 +582,134 @@ export default class DataProvider extends Component { onUpdateDomains, } = this.props; - if (Object.keys(loaderConfig).length === 0) { - // Do not bother, loader hasn't given any data yet. - return null; - } const seriesObjects = this.getSeriesObjects(); - // Compute the domains for all of the collections with one pass over all of - // the series objects. - const collectionDomains = seriesObjects.reduce( - ( - acc, - { collectionId, yDomain: seriesDomain, ySubDomain: seriesXSubDomain } - ) => { - if (!collectionId) { + // // Compute the domains for all of the collections with one pass over all of + // // the series objects. + const domainsByCollectionId = seriesObjects.reduce((acc, series) => { + const { collectionId } = series; + if (!collectionId) { + return acc; + } + + const { + timeDomain: seriesTimeDomain, + timeSubDomain: seriesTimeSubDomain, + xDomain: seriesXDomain, + xSubDomain: seriesXSubDomain, + yDomain: seriesYDomain, + ySubDomain: seriesYSubDomain, + } = series; + + const { + timeDomain: collectionTimeDomain = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + ], + timeSubDomain: collectionTimeSubDomain = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + ], + xDomain: collectionXDomain = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + ], + xSubDomain: collectionXSubDomain = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + ], + yDomain: collectionYDomain = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + ], + ySubDomain: collectionYSubDomain = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + ], + } = acc[collectionId] || {}; + + return { + ...acc, + [collectionId]: { + timeDomain: seriesTimeDomain + ? boundedDomain(collectionTimeDomain, seriesTimeDomain) + : undefined, + timeSubDomain: boundedDomain( + collectionTimeSubDomain, + seriesTimeSubDomain + ), + xDomain: seriesXDomain + ? boundedDomain(collectionXDomain, seriesXDomain) + : undefined, + xSubDomain: boundedDomain(collectionXSubDomain, seriesXSubDomain), + yDomain: seriesYDomain + ? boundedDomain(collectionYDomain, seriesYDomain) + : undefined, + ySubDomain: boundedDomain(collectionYSubDomain, seriesYSubDomain), + }, + }; + }, {}); + + // Then we want to enrich the collection objects with their above-computed + // domains. + const collectionsWithDomains = Object.keys(collectionsById).reduce( + (acc, id) => { + if (!domainsByCollectionId[id]) { return acc; } - const { yDomain: existingDomain, ySubDomain: existingYSubDomain } = acc[ - collectionId - ] || { - yDomain: [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], - ySubDomain: [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], - }; - return { + return [ ...acc, - [collectionId]: { - yDomain: [ - Math.min(existingDomain[0], seriesDomain[0]), - Math.max(existingDomain[1], seriesDomain[1]), - ], - ySubDomain: [ - Math.min(existingYSubDomain[0], seriesXSubDomain[0]), - Math.max(existingYSubDomain[1], seriesXSubDomain[1]), - ], + { + ...collectionsById[id], + ...domainsByCollectionId[id], }, - }; + ]; }, - {} + [] ); - // Then we want to enrich the collection objects with their above-computed - // domains. - const collectionsById = {}; - const collectionsWithDomains = []; - collections.forEach(c => { - if (collectionDomains[c.id]) { - const withDomain = { - ...c, - ...collectionDomains[c.id], - }; - collectionsWithDomains.push(withDomain); - collectionsById[c.id] = withDomain; - } - }); - // Then take a final pass over all of the series and replace their // yDomain and ySubDomain arrays with the one from their collections (if // they're a member of a collection). const collectedSeries = seriesObjects.map(s => { - if (s.collectionId !== undefined) { - const copy = { - ...s, - }; - if (!collectionsById[copy.collectionId]) { - // It's pointing to a collection that doesn't exist. - copy.collectionId = undefined; - return copy; + const { collectionId } = s; + if (collectionId === undefined) { + return s; + } + const copy = { ...s }; + if (!collectionsById[collectionId]) { + // It's pointing to a collection that doesn't exist. + delete copy.collectionId; + } else { + const { + timeDomain: collectionTimeDomain, + timeSubDomain: collectionTimeSubDomain, + xDomain: collectionXDomain, + xSubDomain: collectionXSubDomain, + yDomain: collectionYDomain, + ySubDomain: collectionYSubDomain, + } = domainsByCollectionId[collectionId] || {}; + + if (collectionTimeDomain) { + copy.timeDomain = collectionTimeDomain; + } + if (collectionTimeSubDomain) { + copy.timeSubDomain = collectionTimeSubDomain; + } + if (collectionXDomain) { + copy.xDomain = collectionXDomain; + } + if (collectionXSubDomain) { + copy.xSubDomain = collectionXSubDomain; + } + if (collectionYDomain) { + copy.yDomain = collectionYDomain; + } + if (collectionYSubDomain) { + copy.ySubDomain = collectionYSubDomain; } - copy.yDomain = [...collectionsById[copy.collectionId].yDomain]; - copy.ySubDomain = [...collectionsById[copy.collectionId].ySubDomain]; - return copy; } - return s; + return copy; }); const context = { @@ -672,9 +725,14 @@ export default class DataProvider extends Component { timeSubDomainChanged: this.timeSubDomainChanged, limitTimeSubDomain, onUpdateDomains, + registerCollection: this.registerCollection, + updateCollection: this.updateCollection, + registerSeries: this.registerSeries, + updateSeries: this.updateSeries, }; return ( + {this.renderLegacyItems()} {children} ); @@ -727,8 +785,6 @@ DataProvider.propTypes = { pointsPerSeries: PropTypes.number, children: PropTypes.node.isRequired, defaultLoader: PropTypes.func, - series: seriesPropType.isRequired, - collections: GriffPropTypes.collections, // xSubDomain => void onTimeSubDomainChanged: PropTypes.func, // newSubDomainsPerItem => void @@ -753,36 +809,46 @@ DataProvider.propTypes = { // (error, params) => void // Callback when data loader throws an error onFetchDataError: PropTypes.func, + + series: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + }) + ), + collections: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + }) + ), }; DataProvider.defaultProps = { - collections: [], - defaultLoader: null, - drawPoints: null, + defaultLoader: undefined, + drawPoints: undefined, drawLines: undefined, - onTimeSubDomainChanged: null, - onUpdateDomains: null, + onTimeSubDomainChanged: undefined, + onUpdateDomains: undefined, opacity: 1.0, - opacityAccessor: null, + opacityAccessor: undefined, pointsPerSeries: 250, - pointWidth: null, - pointWidthAccessor: null, - strokeWidth: null, - timeDomain: null, - timeSubDomain: null, - xDomain: null, - xSubDomain: null, + pointWidth: undefined, + pointWidthAccessor: undefined, + strokeWidth: undefined, + timeDomain: undefined, + timeSubDomain: undefined, + xDomain: undefined, + xSubDomain: undefined, updateInterval: 0, timeAccessor: d => d.timestamp, - x0Accessor: null, - x1Accessor: null, + x0Accessor: undefined, + x1Accessor: undefined, xAccessor: d => d.timestamp, - y0Accessor: null, - y1Accessor: null, + y0Accessor: undefined, + y1Accessor: undefined, yAccessor: d => d.value, yAxisWidth: 50, - yDomain: null, - ySubDomain: null, + yDomain: undefined, + ySubDomain: undefined, isTimeSubDomainSticky: false, limitTimeSubDomain: xSubDomain => xSubDomain, onFetchData: () => {}, @@ -790,4 +856,6 @@ DataProvider.defaultProps = { onFetchDataError: e => { throw e; }, + series: [], + collections: [], }; diff --git a/src/components/Line/index.tsx b/src/components/Line/index.tsx index 09bbfa74..f53528db 100644 --- a/src/components/Line/index.tsx +++ b/src/components/Line/index.tsx @@ -19,6 +19,7 @@ export interface Props { drawPoints?: boolean; strokeWidth?: number; opacity?: number; + opacityAccessor?: AccessorFunction; pointWidth?: number; pointWidthAccessor?: AccessorFunction; clipPath: string; @@ -36,8 +37,9 @@ const Line: React.FunctionComponent = ({ step = false, hidden = false, drawPoints = false, - strokeWidth = 6, + strokeWidth = 1, opacity = 1, + opacityAccessor, pointWidth = 6, pointWidthAccessor, clipPath, @@ -111,6 +113,8 @@ const Line: React.FunctionComponent = ({ const x = xAxisAccessor(d); return x >= xSubDomain[0] && x <= xSubDomain[1]; })} + opacity={opacity} + opacityAccessor={opacityAccessor} drawPoints={drawPoints} xAccessor={xAxisAccessor} yAccessor={yAccessor} diff --git a/src/components/LineChart/Layout.js b/src/components/LineChart/Layout.js index d9c1f20a..1309098f 100644 --- a/src/components/LineChart/Layout.js +++ b/src/components/LineChart/Layout.js @@ -29,7 +29,7 @@ const xAxisContainer = area => (axis, placement) => ( width: '100%', }} > - {React.cloneElement(axis, { xAxisPlacement: placement })} + {React.cloneElement(axis, { placement })} ); diff --git a/src/components/Scaler/index.tsx b/src/components/Scaler/index.tsx index b4ca0ac7..c2c580fc 100644 --- a/src/components/Scaler/index.tsx +++ b/src/components/Scaler/index.tsx @@ -3,8 +3,8 @@ import * as PropTypes from 'prop-types'; import DataContext from '../../context/Data'; import ScalerContext from '../../context/Scaler'; import GriffPropTypes, { seriesPropType } from '../../utils/proptypes'; -import Axes, { Domains, Dimension } from '../../utils/Axes'; -import { Domain, Series, Collection, ItemId } from '../../external'; +import Axes, { Domains } from '../../utils/Axes'; +import { Domain, Series, Collection } from '../../external'; import { Item } from '../../internal'; import { withDisplayName } from '../../utils/displayName'; @@ -47,6 +47,11 @@ export interface OnDomainsUpdated extends Function {} type DomainAxis = 'time' | 'x' | 'y'; +interface StateUpdates { + domainsByItemId: DomainsByItemId; + subDomainsByItemId: DomainsByItemId; +} + // If the timeSubDomain is within this margin, consider it to be attached to // the leading edge of the timeDomain. const FRONT_OF_WINDOW_THRESHOLD = 0.05; @@ -55,7 +60,11 @@ const FRONT_OF_WINDOW_THRESHOLD = 0.05; * Provide a placeholder domain so that we can test for validity later, but * it can be safely operated on like a real domain. */ -export const PLACEHOLDER_DOMAIN: Domain = [0, 0]; +export const placeholder = (min: number, max: number): Domain => { + const domain: Domain = [min, max]; + domain.placeholder = true; + return domain; +}; const haveDomainsChanged = (before: Item, after: Item) => before.timeDomain !== after.timeDomain || @@ -87,13 +96,6 @@ const findItemsWithChangedDomains = ( }, []); }; -export const stripPlaceholderDomain = (domain: Domain): Domain | undefined => { - if (isEqual(PLACEHOLDER_DOMAIN, domain)) { - return undefined; - } - return domain; -}; - const isEqual = (a: Domain, b: Domain): boolean => { if (a === b) { return true; @@ -104,6 +106,20 @@ const isEqual = (a: Domain, b: Domain): boolean => { return a[0] === b[0] && a[1] === b[1]; }; +export const firstResolvedDomain = ( + domain: Domain | undefined, + // tslint:disable-next-line + ...domains: (undefined | Domain)[] +): Domain | undefined => { + if (domain && domain.placeholder !== true) { + return [...domain] as Domain; + } + if (domains.length === 0) { + return undefined; + } + return firstResolvedDomain(domains[0], ...(domains.splice(1) as Domain[])); +}; + /** * The scaler is the source of truth for all things related to the domains and * subdomains for all of the items within Griff. Note that an item can be either @@ -136,24 +152,64 @@ class Scaler extends React.Component { static defaultProps = {}; - constructor(props: Props) { - super(props); + static getDerivedStateFromProps( + { dataContext: { timeDomain, timeSubDomain, series, collections } }: Props, + state: State + ) { + // Make sure that all items in the props are present in the domainsByItemId + // and subDomainsByItemId state objects. + const { domainsByItemId, subDomainsByItemId } = state; + let updated = false; + const stateUpdates = series.concat(collections).reduce( + (acc: StateUpdates, item: Item): StateUpdates => { + const updates: StateUpdates = { ...acc }; + if (!domainsByItemId[item.id]) { + updated = true; + updates.domainsByItemId = { + ...updates.domainsByItemId, + [item.id]: { + time: [...timeDomain] as Domain, + x: + firstResolvedDomain(item.xDomain) || + placeholder(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), + y: + firstResolvedDomain(item.yDomain) || + placeholder(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), + }, + }; + } - this.state = { - // Map from item (collection, series) to their respective domains. - domainsByItemId: this.getDomainsByItemId(), + if (!subDomainsByItemId[item.id]) { + updated = true; + updates.subDomainsByItemId = { + ...updates.subDomainsByItemId, + [item.id]: { + time: [...timeSubDomain] as Domain, + x: firstResolvedDomain(item.xSubDomain) || placeholder(0, 1), + y: firstResolvedDomain(item.ySubDomain) || placeholder(0, 1), + }, + }; + } - // Map from item (collection, series) to their respective subdomains. - subDomainsByItemId: this.getSubDomainsByItemId(), - }; + return updates; + }, + { domainsByItemId: {}, subDomainsByItemId: {} } + ); + return updated ? stateUpdates : null; } + state: State = { + domainsByItemId: {}, + subDomainsByItemId: {}, + }; + componentDidUpdate(prevProps: Props) { const { dataContext } = this.props; const { domainsByItemId: oldDomainsByItemId, subDomainsByItemId: oldSubDomainsByItemId, } = this.state; + const changedSeries = findItemsWithChangedDomains( prevProps.dataContext.series, dataContext.series @@ -169,33 +225,48 @@ class Scaler extends React.Component { [...changedSeries, ...changedCollections].forEach(item => { domainsByItemId[item.id] = { time: - dataContext.timeDomain || - (item.timeDomain || - stripPlaceholderDomain(Axes.time(oldDomainsByItemId[item.id]))), + firstResolvedDomain( + dataContext.timeDomain, + item.timeDomain, + Axes.time(oldDomainsByItemId[item.id]) + ) || placeholder(0, Date.now()), x: - item.xDomain || - stripPlaceholderDomain(Axes.x(oldDomainsByItemId[item.id])) || - PLACEHOLDER_DOMAIN, + firstResolvedDomain( + item.xDomain, + Axes.x(oldDomainsByItemId[item.id]) + ) || + // Set a large range because this is a domain. + placeholder(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), y: - item.yDomain || - stripPlaceholderDomain(Axes.y(oldDomainsByItemId[item.id])) || - PLACEHOLDER_DOMAIN, + firstResolvedDomain( + item.yDomain, + Axes.y(oldDomainsByItemId[item.id]) + ) || + // Set a large range because this is a domain. + placeholder(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), }; subDomainsByItemId[item.id] = { time: - dataContext.timeSubDomain || - (item.timeSubDomain || - stripPlaceholderDomain( - Axes.time(oldSubDomainsByItemId[item.id]) - )), + firstResolvedDomain( + dataContext.timeSubDomain || + (item.timeSubDomain || + Axes.time(oldSubDomainsByItemId[item.id])) + ) || + // Set a large range because this is a subdomain. + placeholder(0, Date.now()), x: - item.xSubDomain || - stripPlaceholderDomain(Axes.x(oldSubDomainsByItemId[item.id])) || - PLACEHOLDER_DOMAIN, + firstResolvedDomain( + item.xSubDomain, + Axes.x(oldSubDomainsByItemId[item.id]) + ) || + // Set a small range because this is a subdomain. + placeholder(0, 1), y: - item.ySubDomain || - stripPlaceholderDomain(Axes.y(oldSubDomainsByItemId[item.id])) || - PLACEHOLDER_DOMAIN, + firstResolvedDomain( + item.ySubDomain || Axes.y(oldSubDomainsByItemId[item.id]) + ) || + // Set a small range because this is a subdomain. + placeholder(0, 1), }; }); // eslint-disable-next-line react/no-did-update-set-state @@ -258,36 +329,6 @@ class Scaler extends React.Component { } } - getDomainsByItemId = () => { - const { dataContext } = this.props; - return [...dataContext.series, ...dataContext.collections].reduce( - (acc, item) => ({ - ...acc, - [item.id]: { - time: [...dataContext.timeDomain], - x: [...(item.xDomain || PLACEHOLDER_DOMAIN)], - y: [...(item.yDomain || PLACEHOLDER_DOMAIN)], - }, - }), - {} - ); - }; - - getSubDomainsByItemId = () => { - const { dataContext } = this.props; - return [...dataContext.series, ...dataContext.collections].reduce( - (acc, item) => ({ - ...acc, - [item.id]: { - time: [...dataContext.timeSubDomain], - x: [...(item.xSubDomain || PLACEHOLDER_DOMAIN)], - y: [...(item.ySubDomain || PLACEHOLDER_DOMAIN)], - }, - }), - {} - ); - }; - /** * Update the subdomains for the given items. This is a patch update and will * be merged with the current state of the subdomains. An example payload @@ -333,13 +374,16 @@ class Scaler extends React.Component { subDomainsByItemId[itemId][axis] || newSubDomain; const existingSpan = existingSubDomain[1] - existingSubDomain[0]; - const limits = stripPlaceholderDomain( - ((domainsByItemId || {})[itemId] || {})[axis] || - (axis === String(Axes.time) + const limits = + firstResolvedDomain( + ((domainsByItemId || {})[itemId] || {})[axis], + axis === String(Axes.time) ? // FIXME: Phase out this single timeDomain thing. dataContext.timeDomain - : undefined) - ) || [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; + : undefined + ) || + // Set a large range because this is a limiting range. + placeholder(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); if (newSpan === existingSpan) { // This is a translation; check the bounds. @@ -378,13 +422,16 @@ class Scaler extends React.Component { render() { const { domainsByItemId, subDomainsByItemId } = this.state; - const { children, dataContext } = this.props; + const { + children, + dataContext: { collections, series }, + } = this.props; const finalContext = { // Pick what we need out of the dataContext instead of spreading the // entire object into the context. - collections: dataContext.collections, - series: dataContext.series, + collections, + series, updateDomains: this.updateDomains, domainsByItemId, diff --git a/src/components/Scatterplot/index.js b/src/components/Scatterplot/index.js index da66ecf7..d5c98dca 100644 --- a/src/components/Scatterplot/index.js +++ b/src/components/Scatterplot/index.js @@ -12,6 +12,7 @@ import Axes from '../../utils/Axes'; import AxisCollection from '../AxisCollection'; import LineCollection from '../LineCollection'; import AxisDisplayMode from '../../utils/AxisDisplayMode'; +import { withDisplayName } from '../../utils/displayName'; const propTypes = { size: PropTypes.shape({ @@ -52,8 +53,8 @@ const Y_AXIS_WIDTH = 50; const X_AXIS_HEIGHT = 50; const getYAxisPlacement = ({ collections, series, yAxisPlacement }) => { - const yAxisPlacements = [] - .concat(series.filter(s => s.collectionId === undefined)) + const yAxisPlacements = series + .filter(s => s.collectionId === undefined) .concat(collections) .reduce((acc, item) => { const placement = item.yAxisPlacement || yAxisPlacement; @@ -131,7 +132,7 @@ const ScatterplotComponent = ({ const yAxisPlacement = getYAxisPlacement({ collections, series, - propsYAxisPlacement, + yAxisPlacement: propsYAxisPlacement, }); chartSize.width -= visibleAxes * Y_AXIS_WIDTH; @@ -207,7 +208,7 @@ const SizedScatterplotComponent = sizeMe({ monitorHeight: true, })(ScatterplotComponent); -const Scatterplot = props => ( +export default withDisplayName('Scatterplot', props => ( {({ collections, series }) => ( ( /> )} -); - -export default Scatterplot; +)); diff --git a/src/components/Series/index.tsx b/src/components/Series/index.tsx new file mode 100644 index 00000000..512fc5ed --- /dev/null +++ b/src/components/Series/index.tsx @@ -0,0 +1,123 @@ +import * as React from 'react'; +import { ItemId, AccessorFunction, PointRenderer } from '../../external'; +import Data from '../../context/Data'; +import { Domain } from 'domain'; +import { AxisPlacement } from '../AxisPlacement'; +import { AxisDisplayMode } from '../../utils/AxisDisplayMode'; +import { withDisplayName } from '../../utils/displayName'; + +// TODO: Move this to DataProvider (and define it properly over there) +type LoaderFunction = (params: any) => any; + +export interface ItemProps { + color?: string; + drawLines?: boolean; + drawPoints?: boolean | PointRenderer; + pointWidth?: number; + strokeWidth?: number; + hidden?: boolean; + loader?: LoaderFunction; + step?: boolean; + zoomable?: boolean; + name?: string; + timeAccessor?: AccessorFunction; + xAccessor?: AccessorFunction; + x0Accessor?: AccessorFunction; + x1Accessor?: AccessorFunction; + yAccessor?: AccessorFunction; + y0Accessor?: AccessorFunction; + y1Accessor?: AccessorFunction; + yDomain?: Domain; + ySubDomain?: Domain; + yAxisPlacement?: AxisPlacement; + yAxisDisplayMode?: AxisDisplayMode; + pointWidthAccessor?: AccessorFunction; + opacity?: number; + opacityAccessor?: AccessorFunction; +} + +export const WATCHED_PROP_NAMES = [ + 'color', + 'drawLines', + 'drawPoints', + 'pointWidth', + 'strokeWidth', + 'hidden', + 'loader', + 'step', + 'zoomable', + 'name', + 'timeAccessor', + 'xAccessor', + 'x0Accessor', + 'x1Accessor', + 'yAccessor', + 'y0Accessor', + 'y1Accessor', + 'yDomain', + 'ySubDomain', + 'yAxisPlacement', + 'yAxisDisplayMode', + 'pointWidthAccessor', + 'opacity', + 'opacityAccessor', +]; + +export interface Props extends ItemProps { + id: ItemId; + collectionId?: ItemId; +} + +export type UnregisterSeriesFunction = () => void; + +export type RegisterSeriesFunction = ( + seriesProps: Props +) => UnregisterSeriesFunction; + +export type UpdateSeriesFunction = (seriesProps: Props) => void; + +interface InternalProps { + registerSeries: RegisterSeriesFunction; + updateSeries: UpdateSeriesFunction; +} + +const Series: React.FunctionComponent = ({ + id, + registerSeries, + updateSeries, + children, + + // Below are all of the series props. + ...props +}) => { + // This only happens once, when the component is first mounted. + React.useEffect(() => { + return registerSeries({ + id, + ...props, + }); + }, []); + + // But whenever the component is updated, we want to update the series in the + // DataProvider. + React.useEffect(() => { + return updateSeries({ + id, + ...props, + }); + // @ts-ignore - It's okay for props[name] to be implicit any. + }, WATCHED_PROP_NAMES.map(name => props[name]).concat(props.collectionId)); + return null; +}; + +export default withDisplayName('Series', (props: Props) => ( + + {({ registerSeries, updateSeries }: InternalProps) => ( + + )} + +)); diff --git a/src/components/XAxis/index.tsx b/src/components/XAxis/index.tsx index 2aa427a3..a7137fab 100644 --- a/src/components/XAxis/index.tsx +++ b/src/components/XAxis/index.tsx @@ -162,10 +162,22 @@ const XAxis: React.FunctionComponent = ({ ticks = 0, width = 1, }) => { - const domain = (scaled ? subDomainsByItemId : domainsByItemId)[ - Object.keys(domainsByItemId)[0] - ]; - // @ts-ignore + if (series.length === 0) { + return null; + } + + // TODO: Update this to be multi-series aware. Right now this assumes one + // single x axis, which isn't scalable. + const domain = (scaled ? subDomainsByItemId : domainsByItemId)[series[0].id]; + + // The system hasn't fully booted-up yet (domains / subdomains are still being + // calculated and populated), so we need to wait a heartbeat. + if (!domain) { + return null; + } + + // @ts-ignore - I think that TypeScript is wrong here because nothing here + // will be void .. ? const scale: d3.ScaleLinear = X_SCALER_FACTORY[a]( domain[a], width diff --git a/src/context/Data.js b/src/context/Data.js index 6c7abbbf..ce7eda78 100644 --- a/src/context/Data.js +++ b/src/context/Data.js @@ -4,4 +4,6 @@ export default React.createContext({ series: [], collections: [], xDomain: [Date.now() - 1000 * 60 * 60 * 24 * 365, 0], + // eslint-disable-next-line no-console + registerSeries: (...args) => console.log('Fake-registering series:', ...args), }); diff --git a/src/external.d.ts b/src/external.d.ts index 91d9cce9..6655defb 100644 --- a/src/external.d.ts +++ b/src/external.d.ts @@ -1,7 +1,7 @@ import { Item } from './internal'; export * from './components/AxisPlacement'; -export type Domain = [number, number]; +export type Domain = [number, number] & { placeholder?: boolean }; export type ItemId = string | number; diff --git a/src/index.js b/src/index.js index 53763c62..66091301 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ export { default as AxisDisplayMode } from './utils/AxisDisplayMode'; export { default as AxisPlacement } from './components/AxisPlacement'; export { default as Brush } from './components/Brush'; +export { default as Collection } from './components/Collection'; export { default as ContextChart } from './components/ContextChart'; export { default as DataProvider } from './components/DataProvider'; export { default as GridLines } from './components/GridLines'; @@ -10,3 +11,4 @@ export { default as LineChart } from './components/LineChart'; export { default as ScalerContext } from './context/Scaler'; export { default as Scatterplot } from './components/Scatterplot'; export { default as XAxis } from './components/XAxis'; +export { default as Series } from './components/Series'; diff --git a/src/utils/Axes.ts b/src/utils/Axes.ts index b81e4937..4e4351e8 100644 --- a/src/utils/Axes.ts +++ b/src/utils/Axes.ts @@ -1,4 +1,5 @@ import { Domain } from '../external'; +import { placeholder } from '../components/Scaler'; /** * We only currently recognize three dimensions: time, x, and y. @@ -29,7 +30,7 @@ export interface Dimension extends Function { const dimension = (key: DomainDimension): Dimension => { const functor: Dimension = (input: Domains) => { if (!input) { - return [0, 0]; + return placeholder(0, 0); } return input[key]; }; diff --git a/src/utils/proptypes.js b/src/utils/proptypes.js index dbef625e..d21d8987 100644 --- a/src/utils/proptypes.js +++ b/src/utils/proptypes.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import AxisDisplayModes from '../utils/AxisDisplayMode'; import AxisPlacement from '../components/AxisPlacement'; -import Axes from './Axes'; const idPropType = PropTypes.oneOfType([PropTypes.number, PropTypes.string]); @@ -237,26 +236,19 @@ const updateDomains = PropTypes.func; const domainsByItemId = PropTypes.objectOf( PropTypes.shape({ - [Axes.time]: domainPropType, - [Axes.x]: domainPropType, - [Axes.y]: domainPropType, + time: domainPropType, + x: domainPropType, + y: domainPropType, }) ); const zoomAxes = PropTypes.shape({ - [Axes.time]: PropTypes.bool, - [Axes.x]: PropTypes.bool, - [Axes.y]: PropTypes.bool, + time: PropTypes.bool, + x: PropTypes.bool, + y: PropTypes.bool, }); -const axes = PropTypes.oneOf([ - Axes.time, - String(Axes.time), - Axes.x, - String(Axes.x), - Axes.y, - String(Axes.y), -]); +const axes = PropTypes.oneOf(['time', 'x', 'y']); export default { axisPlacement, diff --git a/stories/AxisCollection.stories.js b/stories/AxisCollection.stories.js index 2b45b92c..3982a94e 100644 --- a/stories/AxisCollection.stories.js +++ b/stories/AxisCollection.stories.js @@ -2,7 +2,12 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import moment from 'moment'; -import { AxisPlacement, DataProvider, AxisDisplayMode } from '../build/src'; +import { + AxisPlacement, + DataProvider, + AxisDisplayMode, + Series, +} from '../build/src'; import { staticLoader } from './loaders'; import AxisCollection from '../build/src/components/AxisCollection'; @@ -16,22 +21,18 @@ storiesOf('components/AxisCollection', module) )) .add('default', () => ( - + + + )) .add('ticks', () => ( - + + + @@ -39,11 +40,9 @@ storiesOf('components/AxisCollection', module) )) .add('zoomable', () => ( - + + + @@ -51,11 +50,9 @@ storiesOf('components/AxisCollection', module) )) .add('axisDisplayMode', () => ( - + + + ( - + + + ( - + + + @@ -100,11 +93,9 @@ storiesOf('components/AxisCollection', module) )) .add('yAxisWidth', () => ( - + + + diff --git a/stories/Chartjs.stories.js b/stories/Chartjs.stories.js index 9f1f78df..6b0d4496 100644 --- a/stories/Chartjs.stories.js +++ b/stories/Chartjs.stories.js @@ -2,7 +2,12 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import moment from 'moment'; import { Bar, Doughnut } from 'react-chartjs-2'; -import { ContextChart, DataProvider, ScalerContext } from '../build/src'; +import { + ContextChart, + DataProvider, + ScalerContext, + Series, +} from '../build/src'; import { staticLoader } from './loaders'; const staticXDomain = [+moment().subtract(1, 'week'), +moment()]; @@ -14,11 +19,9 @@ storiesOf('integrations/ChartJS', module) )) .add('Bar', () => ( - + + + {({ series, subDomainsByItemId }) => ( )) .add('Doughnut', () => ( - + + + {({ series, subDomainsByItemId }) => ( ( +
+ {story()} +
+ )) + .add('Basic LineChart', () => ( + + + + + + + )) + .add('Basic Scatterplot', () => ( + +d.x} + yAccessor={d => +d.y} + > + + + + +
+ +
+
+ )) + .add('Flat structure', () => ( + + + + + + + )) + .add('Change props', () => ( + + )); diff --git a/stories/ContextChart.stories.js b/stories/ContextChart.stories.js index 5f1a2f28..e3785aa8 100644 --- a/stories/ContextChart.stories.js +++ b/stories/ContextChart.stories.js @@ -1,7 +1,12 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import moment from 'moment'; -import { AxisPlacement, ContextChart, DataProvider } from '../build/src'; +import { + AxisPlacement, + ContextChart, + DataProvider, + Series, +} from '../build/src'; import { staticLoader } from './loaders'; const staticXDomain = [+moment().subtract(1, 'week'), +moment()]; @@ -22,33 +27,27 @@ storiesOf('components/ContextChart', module) )) .add('default', () => (
- + + +
)) .add('height', () => (
- + + +
)) .add('annotations', () => (
- + + +
@@ -56,20 +55,14 @@ storiesOf('components/ContextChart', module) .add('zoomable', () => (
- +
- + + +
@@ -77,11 +70,9 @@ storiesOf('components/ContextChart', module) )) .add('xAxisFormatter', () => (
- + + + `${( @@ -96,11 +87,9 @@ storiesOf('components/ContextChart', module) )) .add('xAxisPlacement', () => (
- + + +
diff --git a/stories/GridLines.stories.js b/stories/GridLines.stories.js index 7c206ee5..4dc98abc 100644 --- a/stories/GridLines.stories.js +++ b/stories/GridLines.stories.js @@ -1,7 +1,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { DataProvider, LineChart } from '../build/src'; +import { DataProvider, LineChart, Series } from '../build/src'; import { staticLoader } from './loaders'; import GridLines from '../build/src/components/GridLines'; @@ -10,110 +10,81 @@ const CHART_HEIGHT = 500; storiesOf('Grid Lines', module) .add('Static horizontal lines every 35 pixels', () => ( - + + )) .add('3 static horizontal lines', () => ( - + + )) .add('Static vertical lines every 35 pixels', () => ( - + + )) .add('3 static vertical lines', () => ( - + + )) .add('Static grid lines every 75 pixels', () => ( - + + )) .add('3 grid lines', () => ( - + + )) .add('Dynamic horizontal lines', () => ( - + + )) .add('Dynamic vertical lines', () => ( - + + )) .add('Dynamic grid lines', () => ( - + + )) .add('Dynamic grid lines (multiple series)', () => ( - + + + @@ -124,8 +95,9 @@ storiesOf('Grid Lines', module) key="y-dimension" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + @@ -134,8 +106,9 @@ storiesOf('Grid Lines', module) key="x-dimension" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + @@ -144,8 +117,9 @@ storiesOf('Grid Lines', module) key="grid-object" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + @@ -154,8 +128,9 @@ storiesOf('Grid Lines', module) key="different" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + + + @@ -180,8 +156,9 @@ storiesOf('Grid Lines', module) key="x-dimension" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + @@ -190,8 +167,9 @@ storiesOf('Grid Lines', module) key="grid-object" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + @@ -200,8 +178,9 @@ storiesOf('Grid Lines', module) key="different" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + + + @@ -226,8 +206,9 @@ storiesOf('Grid Lines', module) key="x-dimension" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + @@ -236,8 +217,9 @@ storiesOf('Grid Lines', module) key="grid-object" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + @@ -246,8 +228,9 @@ storiesOf('Grid Lines', module) key="different" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + d.timestamp} yAccessor={d => d.value} - series={[ - { id: 1, color: 'steelblue', name: 'name1' }, - { id: 2, color: 'maroon', name: 'name2' }, - ]} > + + d.timestamp} yAccessor={d => d.value} - series={[ - { id: 1, color: 'steelblue', name: 'name1' }, - { id: 2, color: 'maroon', name: 'name2' }, - ]} > + + )) .add('Area (no zoom)', () => ( - + + + { @@ -78,7 +72,7 @@ storiesOf('components/InteractionLayer', module) /> )) - .add('Area (zoom)', () => { + .add('Area (x-zoom)', () => { class ZoomByArea extends React.Component { state = { xSubDomain: null }; @@ -98,8 +92,9 @@ storiesOf('components/InteractionLayer', module) defaultLoader={staticLoader} timeDomain={staticXDomain} timeSubDomain={xSubDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + + + + + + + + + + + ; }) .add('Double-click events', () => ( - + + + + + onAreaDefined: diff --git a/stories/LegacyAPI.stories.js b/stories/LegacyAPI.stories.js new file mode 100644 index 00000000..32463ede --- /dev/null +++ b/stories/LegacyAPI.stories.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { DataProvider, LineChart } from '../build/src'; + +import { staticLoader } from './loaders'; + +const staticXDomain = [Date.now() - 1000 * 60 * 60 * 24 * 30, Date.now()]; +const CHART_HEIGHT = 500; + +storiesOf('legacy/API', module) + .addDecorator(story => ( +
+ {story()} +
+ )) + .add('Series', () => ( + + + + )) + .add('Collections', () => ( + + + + )); diff --git a/stories/LineChart.stories.js b/stories/LineChart.stories.js index 939c33a6..1c9b68c9 100644 --- a/stories/LineChart.stories.js +++ b/stories/LineChart.stories.js @@ -4,7 +4,13 @@ import Select from 'react-select'; import isEqual from 'lodash.isequal'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { DataProvider, LineChart, Brush } from '../build/src'; +import { + DataProvider, + LineChart, + Brush, + Series, + Collection, +} from '../build/src'; import quandlLoader from './quandlLoader'; import { @@ -26,32 +32,23 @@ storiesOf('LineChart', module)
)) .add('Basic', () => ( - + + + )) .add('Basic with yDomains', () => ( - + + + )) .add('Custom tick formatting', () => ( - + + + n / 1000} @@ -60,57 +57,39 @@ storiesOf('LineChart', module) )) .add('Custom # of y-axis ticks', () => ( - + + + )) .add('Multiple', () => ( - + + + + - + + + + - + + + )) .add('Single-value in y axis', () => ( - + + + + @@ -126,11 +105,9 @@ storiesOf('LineChart', module) margin: '1em', }} > - + + + @@ -142,11 +119,9 @@ storiesOf('LineChart', module) margin: '1em', }} > - + + + @@ -158,11 +133,9 @@ storiesOf('LineChart', module) margin: '1em', }} > - + + + - + + + @@ -191,11 +162,9 @@ storiesOf('LineChart', module) )) .add('Full-size', () => (
- + + +
@@ -247,11 +216,9 @@ storiesOf('LineChart', module) + + @@ -267,61 +234,57 @@ storiesOf('LineChart', module) timeDomain={staticXDomain} timeAccessor={d => d[0]} yAccessor={d => d[1]} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + +
)) .add('min/max', () => { - const y0Accessor = d => d[1] - 0.5; - const y1Accessor = d => d[1] + 0.5; + const y0Accessor = d => d.value - 0.5; + const y1Accessor = d => d.value + 0.5; return ( - d[0]} - yAccessor={d => d[1]} - series={[ - { id: 10, color: 'steelblue', y0Accessor, y1Accessor }, - { id: 2, color: 'maroon' }, - ]} - > + + + ); }) .add('min/max (step series)', () => { - const y0Accessor = d => d[1] - 0.5; - const y1Accessor = d => d[1] + 0.5; + const y0Accessor = d => d.value - 0.5; + const y1Accessor = d => d.value + 0.5; return ( - d[0]} - yAccessor={d => d[1]} - series={[ - { id: 10, color: 'steelblue', y0Accessor, y1Accessor, step: true }, - { id: 2, color: 'maroon', step: true }, - ]} - > + + + ); }) .add('min/max with raw points', () => { - const y0Accessor = d => d[1] - 0.5; - const y1Accessor = d => d[1] + 0.5; + const y0Accessor = d => d.value - 0.5; + const y1Accessor = d => d.value + 0.5; return ( - d[0]} - yAccessor={d => d[1]} - series={[ - { id: 10, color: 'steelblue', y0Accessor, y1Accessor }, - { id: 2, color: 'maroon', drawPoints: true }, - ]} - > + + + ); @@ -330,18 +293,10 @@ storiesOf('LineChart', module) + + )) @@ -366,19 +321,9 @@ storiesOf('LineChart', module) + - + + + @@ -694,8 +616,9 @@ storiesOf('LineChart', module) defaultLoader={staticLoader} timeDomain={staticXDomain} ySubDomain={[0.25, 0.5]} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + +

Set on Series

@@ -703,14 +626,9 @@ storiesOf('LineChart', module) The ySubDomain for the chart should be [0.25, 0.5] for blue{' '} only. Maroon should be [0, 1]

- + + +

Set on Collection

@@ -721,17 +639,12 @@ storiesOf('LineChart', module) + + + +

Set on Series with yDomain

@@ -740,35 +653,30 @@ storiesOf('LineChart', module) chart should be zoomed-out (for the blue line). The blue line should have a maximum zoom-out range of [-1, 2].

- + + +
)) - .add('Dynamic x sub domain', () => { - const xSubDomainFirst = [ + .add('Dynamic time sub domain', () => { + const timeSubDomainFirst = [ Date.now() - 1000 * 60 * 60 * 24 * 20, Date.now() - 1000 * 60 * 60 * 24 * 10, ]; - const xSubDomainSecond = [ + const timeSubDomainSecond = [ Date.now() - 1000 * 60 * 60 * 24 * 10, Date.now(), ]; - class CustomXSubDomain extends React.Component { + class CustomTimeSubDomain extends React.Component { state = { isFirst: true, }; @@ -781,32 +689,31 @@ storiesOf('LineChart', module) type="button" onClick={() => this.setState({ isFirst: !isFirst })} > - {isFirst ? `Switch xSubDomain` : `Switch back xSubDomain`} + {isFirst ? `Switch timeSubDomain` : `Switch back timeSubDomain`} + +
); } } - return ; + return ; }) .add('Live loading', () => ( + + )) @@ -816,11 +723,9 @@ storiesOf('LineChart', module) defaultLoader={liveLoader} timeDomain={liveXDomain} updateInterval={33} - series={[ - { id: 1, color: 'steelblue', name: 'name1' }, - { id: 2, color: 'maroon', name: 'name2' }, - ]} > + + + + + + ({ - id: s.value, - color: colors[s.value], - }))} > + {series.map(s => ( + + ))}
@@ -968,33 +868,29 @@ storiesOf('LineChart', module) } return ; }) - .add('Sticky x subdomain', () => ( + .add('Sticky time subdomain', () => ( + + )) - .add('Sticky x subdomain and ruler', () => ( + .add('Sticky time subdomain and ruler', () => ( + + )) - .add('Limit x subdomain', () => { - class LimitXSubDomain extends React.Component { - limitXSubDomain = subDomain => { + .add('Limit time subdomain', () => { + class LimitTimeSubDomain extends React.Component { + limitTimeSubDomain = subDomain => { const subDomainLength = subDomain[1] - subDomain[0]; const subDomainEnd = Math.min( subDomain[1], @@ -1024,35 +920,34 @@ storiesOf('LineChart', module)
+ +
); } } - return ; + return ; }) .add('onMouseOut', () => ( + + ( + + )); diff --git a/stories/Plotly.stories.js b/stories/Plotly.stories.js index 43de2efb..ef661445 100644 --- a/stories/Plotly.stories.js +++ b/stories/Plotly.stories.js @@ -3,7 +3,12 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import moment from 'moment'; import Plot from 'react-plotly.js'; -import { ContextChart, DataProvider, ScalerContext } from '../build/src'; +import { + ContextChart, + DataProvider, + ScalerContext, + Series, +} from '../build/src'; import { staticLoader } from './loaders'; const staticXDomain = [+moment().subtract(1, 'week'), +moment()]; @@ -33,11 +38,9 @@ storiesOf('integrations/Plotly', module) )) .add('Basic', () => ( - + + + {({ series }) => ( ( - + + + {({ series, subDomainsByItemId }) => ( ( - + + + {({ series, domainsByItemId, subDomainsByItemId, updateDomains }) => ( s.id).join('-')} data={series.map(s => seriesToPlotly(s, subDomainsByItemId))} layout={{ width: '100%', height: 400, - title: 'A Fancy Plot controlled by a ContextChart', + title: 'A Fancy Plot interacting with a ContextChart', }} - onSelected={action('onSelected')} onRelayout={input => { const { 'xaxis.range[0]': lowerTime, diff --git a/stories/Scatterplot.stories.js b/stories/Scatterplot.stories.js index df6da873..7fb8263f 100644 --- a/stories/Scatterplot.stories.js +++ b/stories/Scatterplot.stories.js @@ -8,6 +8,8 @@ import { DataProvider, GridLines, Scatterplot, + Series, + Collection, } from '../build/src'; import { staticLoader, functionLoader } from './loaders'; @@ -31,7 +33,7 @@ const mapping = { const NUM_POINTS = 50; -const scatterplotloader = ({ id, reason, oldSeries, ...params }) => { +export const scatterplotloader = ({ id, reason, oldSeries, ...params }) => { action('scatterplotloader')(reason); if (reason === 'MOUNTED') { const pair = mapping[id] || Math.round(Math.random() * 100); @@ -141,8 +143,8 @@ const scatterplotFunctionLoader = ({ } } - const data = [] - .concat(oldSeries.data.filter(d => d.timestamp <= timeSubDomain[0])) + const data = oldSeries.data + .filter(d => d.timestamp <= timeSubDomain[0]) .concat(newData) .concat(oldSeries.data.filter(d => d.timestamp >= timeSubDomain[1])); @@ -161,7 +163,7 @@ const randomColor = () => { const generateSeries = count => { const series = []; for (let i = 0; i < count; i += 1) { - series.push({ id: `${i} ${i + 1}`, color: randomColor() }); + series.push(); } return series; }; @@ -209,10 +211,10 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} > + @@ -223,14 +225,14 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} > + + + + @@ -241,14 +243,13 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + + + @@ -259,10 +260,11 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + {generateSeries(10)} @@ -277,14 +279,14 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + + + + @@ -301,10 +303,11 @@ storiesOf('Scatterplot', module) timeDomain={[0, 1]} xDomain={[-1, 2]} yDomain={[-1, 2]} - series={[{ id: '1 2', color: 'steelblue' }]} xAccessor={d => +d.x} yAccessor={d => +d.y} + drawPoints > + @@ -316,10 +319,11 @@ storiesOf('Scatterplot', module) defaultLoader={scatterplotloader} timeDomain={[0, 1]} xDomain={[-1, 2]} - series={[{ id: '1 2', color: 'steelblue' }]} xAccessor={d => +d.x} yAccessor={d => +d.y} + drawPoints > + @@ -331,10 +335,11 @@ storiesOf('Scatterplot', module) defaultLoader={scatterplotloader} timeDomain={[0, 1]} yDomain={[-1, 2]} - series={[{ id: '1 2', color: 'steelblue' }]} xAccessor={d => +d.x} yAccessor={d => +d.y} + drawPoints > + @@ -345,10 +350,11 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + @@ -361,10 +367,11 @@ storiesOf('Scatterplot', module) timeDomain={[0, 1]} xDomain={[0.25, 0.75]} yDomain={[0.25, 0.75]} - series={[{ id: '1 2', color: 'steelblue' }]} xAccessor={d => +d.x} yAccessor={d => +d.y} + drawPoints > + @@ -377,10 +384,11 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + n.toFixed(3)} @@ -395,10 +403,11 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + @@ -421,10 +430,11 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + @@ -444,10 +454,11 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + @@ -464,10 +475,11 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + +d.x} yAccessor={d => +d.y} + drawPoints > + + + + + + + + @@ -514,13 +525,12 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + + @@ -528,14 +538,13 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} strokeWidth={10} + drawPoints > + + @@ -547,13 +556,12 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + + @@ -561,14 +569,13 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} pointWidth={10} + drawPoints > + + @@ -576,14 +583,13 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} pointWidthAccessor={d => ((+d.x + +d.y) / 2) * 16 + 1} + drawPoints > + + @@ -595,14 +601,13 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} pointWidth={10} + drawPoints > + + @@ -610,15 +615,14 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} opacityAccessor={(d, i, arr) => (i / arr.length) * 0.9 + 0.1} pointWidth={20} + drawPoints > + + @@ -630,17 +634,16 @@ storiesOf('Scatterplot', module) +d.x} x0Accessor={d => +d.x * 0.9} x1Accessor={d => +d.x * 1.1} yAccessor={d => +d.y} y0Accessor={d => +d.y * 0.9} y1Accessor={d => +d.y * 1.1} + drawPoints > + + @@ -653,11 +656,12 @@ storiesOf('Scatterplot', module) defaultLoader={scatterplotFunctionLoader} timeDomain={[+moment().subtract(1, 'year'), +moment()]} pointsPerSeries={100} - series={[{ id: '1 2', color: 'steelblue' }]} timeAccessor={d => +d.timestamp} xAccessor={d => +d.x} yAccessor={d => +d.y} + drawPoints > +
@@ -675,11 +679,12 @@ storiesOf('Scatterplot', module) defaultLoader={scatterplotFunctionLoader} timeDomain={[+moment().subtract(1, 'year'), +moment()]} pointsPerSeries={100} - series={[{ id: '1 2', color: 'steelblue' }]} timeAccessor={d => +d.timestamp} xAccessor={d => +d.x} yAccessor={d => +d.y} + drawPoints > +
@@ -693,15 +698,15 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + strokeWidth={1} drawLines + drawPoints > + + + @@ -712,21 +717,20 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} + drawPoints > + + + @@ -737,23 +741,20 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} > + + + + + @@ -768,11 +769,11 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} drawPoints={latestPointRenderer} > + @@ -783,21 +784,15 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} > + + @@ -808,20 +803,13 @@ storiesOf('Scatterplot', module) +d.x} yAccessor={d => +d.y} > + + + + diff --git a/stories/Series.stories.js b/stories/Series.stories.js new file mode 100644 index 00000000..412a03e2 --- /dev/null +++ b/stories/Series.stories.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { DataProvider, LineChart, Series, Scatterplot } from '../build/src'; + +import { staticLoader } from './loaders'; +import ToggleRenderer from './ToggleRenderer'; +import { scatterplotloader } from './Scatterplot.stories'; + +const staticXDomain = [Date.now() - 1000 * 60 * 60 * 24 * 30, Date.now()]; +const CHART_HEIGHT = 500; + +export const makePrintable = arr => { + const copy = [...arr]; + copy.toString = () => `[${arr.join(', ')}]`; + return copy; +}; + +/* eslint-disable react/no-multi-comp */ +storiesOf('components/Series', module) + .addDecorator(story => ( +
+ {story()} +
+ )) + .add('Basic LineChart', () => ( + + + + + )) + .add('Basic Scatterplot', () => ( + +d.x} + yAccessor={d => +d.y} + > + +
+ +
+
+ )) + .add('Change props', () => ( + + )); diff --git a/stories/SeriesCollections.stories.js b/stories/SeriesCollections.stories.js index ed8144f8..da36ceda 100644 --- a/stories/SeriesCollections.stories.js +++ b/stories/SeriesCollections.stories.js @@ -1,6 +1,12 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { AxisDisplayMode, DataProvider, LineChart } from '../build/src'; +import { + AxisDisplayMode, + DataProvider, + LineChart, + Collection, + Series, +} from '../build/src'; import { staticLoader } from './loaders'; const staticXDomain = [Date.now() - 1000 * 60 * 60 * 24 * 30, Date.now()]; @@ -12,67 +18,45 @@ storiesOf('Series Collections', module) key="simple" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - ]} - collections={[{ id: '1+2', color: 'red' }]} > + + + +
, d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - yAccessor: d => d.value + 2, - }, - ]} - collections={[{ id: '1+2', color: 'red' }]} > + + + d.value + 2} /> + , ]) .add('Multiple collections', () => ( - d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - { id: 3, collectionId: '3+4', color: 'orange', name: 'name1' }, - { id: 4, collectionId: '3+4', color: 'green', name: 'name2' }, - ]} - collections={[{ id: '1+2', color: 'red' }, { id: '3+4', color: 'gray' }]} - > + + + + + + + + + )) .add('Mixed items', () => ( - d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - { id: 3, color: 'orange', name: 'name3' }, - ]} - collections={[{ id: '1+2', color: 'red' }]} - > + + + + + + )) @@ -81,34 +65,22 @@ storiesOf('Series Collections', module) key="default" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - ]} - collections={[{ id: '1+2', color: 'red', drawPoints: true }]} > + + + + , d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - drawPoints: false, - }, - ]} - collections={[{ id: '1+2', color: 'red', drawPoints: true }]} > + + + + , ]) @@ -117,54 +89,33 @@ storiesOf('Series Collections', module) key="default" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - ]} - collections={[{ id: '1+2', color: 'red', hidden: true }]} > + , d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - hidden: true, - }, - ]} - collections={[{ id: '1+2', color: 'red' }]} > + + + , d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - hidden: false, - }, - ]} - collections={[{ id: '1+2', color: 'red', hidden: true }]} > + , ]) @@ -173,54 +124,33 @@ storiesOf('Series Collections', module) key="default" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - ]} - collections={[{ id: '1+2', color: 'red', strokeWidth: 3 }]} > + + + +
, d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - strokeWidth: 2, - }, - ]} - collections={[{ id: '1+2', color: 'red' }]} > + + + + , d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - strokeWidth: 1, - }, - ]} - collections={[{ id: '1+2', color: 'red', strokeWidth: 3 }]} > + + + + , ]) @@ -232,61 +162,48 @@ storiesOf('Series Collections', module) key="default" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - }, - ]} - collections={[{ id: '1+2', color: 'red', y0Accessor, y1Accessor }]} > + + + +
, d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 3, collectionId: '3+4', color: 'steelblue', name: 'name1' }, - { - id: 4, - collectionId: '3+4', - color: 'maroon', - name: 'name2', - y0Accessor, - y1Accessor, - }, - ]} - collections={[{ id: '3+4', color: 'red' }]} > + + + + , d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 3, collectionId: '3+4', color: 'steelblue', name: 'name1' }, - { - id: 4, - collectionId: '3+4', - color: 'maroon', - name: 'name2', - y0Accessor: null, - y1Accessor: null, - }, - ]} - collections={[{ id: '3+4', color: 'red', y0Accessor, y1Accessor }]} > + + + + , ]; @@ -296,38 +213,22 @@ storiesOf('Series Collections', module) key="default" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - ]} - collections={[ - { id: '1+2', color: 'red', yAxisDisplayMode: AxisDisplayMode.NONE }, - ]} > + + + +
, d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - yAxisDisplayMode: AxisDisplayMode.ALL, - }, - ]} - collections={[ - { id: '1+2', color: 'red', yAxisDisplayMode: AxisDisplayMode.NONE }, - ]} > + + + + , ]) @@ -338,14 +239,11 @@ storiesOf('Series Collections', module) key="default" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - ]} - collections={[{ id: '1+2', color: 'red', yDomain: [-4, 4] }]} > + + + +
, // The yDomain is also provided on one series -- this override should be @@ -354,20 +252,11 @@ storiesOf('Series Collections', module) key="override" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - yDomain: [0.5, 1], - }, - ]} - collections={[{ id: '1+2', color: 'red', yDomain: [-5, 5] }]} > + + + +
, // The two series are offset so that the context chart behavior can be @@ -377,20 +266,11 @@ storiesOf('Series Collections', module) key="scaled" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - yAccessor: d => d.value + 2, - }, - ]} - collections={[{ id: '1+2', color: 'red', yDomain: [-6, 6] }]} > + + + d.value + 2} /> +
, ]) @@ -399,34 +279,35 @@ storiesOf('Series Collections', module) key="default" timeDomain={staticXDomain} defaultLoader={staticLoader} - xAccessor={d => d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { id: 2, collectionId: '1+2', color: 'maroon', name: 'name2' }, - ]} - collections={[{ id: '1+2', color: 'red' }]} > + + + +
, // No color is specified; YAxis should use its default color. + + + + + + + , + // A color is specified; the series' colors should override. d.timestamp} - yAccessor={d => d.value} - series={[ - { id: 1, collectionId: '1+2', color: 'steelblue', name: 'name1' }, - { - id: 2, - collectionId: '1+2', - color: 'maroon', - name: 'name2', - }, - ]} - collections={[{ id: '1+2' }]} > + + + + , ]); diff --git a/stories/ToggleRenderer.js b/stories/ToggleRenderer.js new file mode 100644 index 00000000..2d48dbdb --- /dev/null +++ b/stories/ToggleRenderer.js @@ -0,0 +1,224 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { staticLoader } from './loaders'; +import { makePrintable } from './Series.stories'; +import { + AxisPlacement, + AxisDisplayMode, + Collection, + LineChart, + Series, + DataProvider, +} from '../build/src'; + +const pointRenderer = ( + d, + i, + arr, + { x, y, color, opacity, opacityAccessor } +) => { + const width = Math.floor(((d.value * 100) % 5) + 3); + return ( + + ); +}; +pointRenderer.toString = () => 'custom renderer'; + +const pointWidthAccessor = d => Math.floor(((d.value * 100) % 5) + 3); +pointWidthAccessor.toString = () => 'custom widths'; + +const opacityAccessor = d => ((d.value * 100) % 100) / 100; +opacityAccessor.toString = () => 'custom opacity'; + +const OPTIONS = { + color: ['maroon', 'steelblue', 'darkgreen', 'lightsalmon'], + collectionId: ['missing-collection'], + drawLines: [true, false], + drawPoints: [true, false, pointRenderer], + pointWidth: [4, 6, 8, 10], + pointWidthAccessor: [pointWidthAccessor], + opacity: [0.25, 0.5, 0.75, 1], + opacityAccessor: [opacityAccessor], + strokeWidth: [1, 2, 3, 4, 5, 6], + hidden: [true, false], + step: [true, false], + zoomable: [true, false], + name: ['readable-name'], + yDomain: [[-1, 2], [0, 10], [0.25, 0.75]].map(makePrintable), + ySubDomain: [[-1, 2], [0, 10], [0.25, 0.75]].map(makePrintable), + yAxisPlacement: [ + AxisPlacement.LEFT, + AxisPlacement.RIGHT, + AxisPlacement.BOTH, + AxisPlacement.UNSPECIFIED, + ], + yAxisDisplayMode: [ + AxisDisplayMode.ALL, + AxisDisplayMode.COLLAPSED, + AxisDisplayMode.NONE, + ], +}; + +class ToggleRenderer extends React.Component { + state = {}; + + setProperty = (id, key, value) => () => { + this.setState(state => ({ + [key]: { + ...state[key], + [id]: value, + }, + })); + }; + + getItemOptions = itemId => + Object.keys(OPTIONS).reduce((acc, option) => { + const { [option]: values = {} } = this.state; + return { + ...acc, + [option]: values[itemId], + }; + }, {}); + + renderToggles = key => { + const { collectionIds, seriesIds } = this.props; + const { [key]: currentValues = {} } = this.state; + return [...collectionIds, ...seriesIds].map(id => { + const possibleValues = + key === 'collectionId' + ? [...collectionIds, OPTIONS[key]] + : OPTIONS[key]; + return ( +
+ {possibleValues.map(value => ( + + ))} + +
+ ); + }); + }; + + renderPropertyTable = () => { + const { collectionIds, seriesIds } = this.props; + return ( +
'1fr') + .join(' ')}`, + }} + > +
prop
+ {[...collectionIds, ...seriesIds].map(id => ( +
+ {id} +
+ ))} + {Object.keys(OPTIONS).map(option => ( + +
+ {option} +
+ {this.renderToggles(option)} +
+ ))} +
+ ); + }; + + render() { + const { collectionIds, defaultLoader, seriesIds, timeDomain } = this.props; + return ( +
+ + {collectionIds.map(collectionId => ( + + {seriesIds + .filter(s => s.collectionId === collectionId) + .map(id => ( + + ))} + + ))} + {seriesIds + .filter(s => s.collectionId === undefined) + .map(id => ( + + ))} + + + {this.renderPropertyTable()} +
+ ); + } +} + +ToggleRenderer.propTypes = { + defaultLoader: PropTypes.func, + timeDomain: PropTypes.arrayOf(PropTypes.number), + collectionIds: PropTypes.arrayOf(PropTypes.string), + seriesIds: PropTypes.arrayOf(PropTypes.string), +}; + +ToggleRenderer.defaultProps = { + defaultLoader: staticLoader, + timeDomain: [Date.now() - 1000 * 60 * 60 * 24 * 30, Date.now()], + collectionIds: [], + seriesIds: [], +}; + +export default ToggleRenderer; diff --git a/stories/XAxis.stories.js b/stories/XAxis.stories.js index 0c1fa674..a4fe444f 100644 --- a/stories/XAxis.stories.js +++ b/stories/XAxis.stories.js @@ -1,7 +1,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import moment from 'moment'; -import { DataProvider, XAxis, AxisPlacement } from '../build/src'; +import { DataProvider, XAxis, AxisPlacement, Series } from '../build/src'; import { staticLoader } from './loaders'; storiesOf('components/XAxis', module) @@ -16,10 +16,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + @@ -27,10 +27,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + @@ -42,10 +42,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + @@ -53,10 +53,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + @@ -68,10 +68,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + @@ -79,10 +79,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + @@ -94,10 +94,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + @@ -105,10 +105,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + moment(n).fromNow()} /> @@ -120,10 +120,10 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > + @@ -131,11 +131,11 @@ storiesOf('components/XAxis', module) +d.timestamp} yAccessor={d => +d.y} > - + + ''} />
diff --git a/stories/XAxisPlacements.stories.js b/stories/XAxisPlacements.stories.js index fcc377c0..5df401ca 100644 --- a/stories/XAxisPlacements.stories.js +++ b/stories/XAxisPlacements.stories.js @@ -1,6 +1,12 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { DataProvider, LineChart, AxisPlacement } from '../build/src'; +import { + DataProvider, + LineChart, + AxisPlacement, + Series, + Collection, +} from '../build/src'; import { staticLoader } from './loaders'; const staticXDomain = [Date.now() - 1000 * 60 * 60 * 24 * 30, Date.now()]; @@ -12,20 +18,20 @@ storiesOf('X-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + +
, + + + + , ]) @@ -34,20 +40,20 @@ storiesOf('X-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + +
, + + + + , ]) @@ -56,20 +62,20 @@ storiesOf('X-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + , + + + + , ]) @@ -78,20 +84,20 @@ storiesOf('X-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + , + + + + , ]); diff --git a/stories/YAxisModes.stories.js b/stories/YAxisModes.stories.js index 386444fc..52cd8117 100644 --- a/stories/YAxisModes.stories.js +++ b/stories/YAxisModes.stories.js @@ -1,7 +1,13 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { DataProvider, LineChart, AxisDisplayMode } from '../build/src'; +import { + AxisDisplayMode, + Collection, + DataProvider, + LineChart, + Series, +} from '../build/src'; import { staticLoader } from './loaders'; const staticXDomain = [Date.now() - 1000 * 60 * 60 * 24 * 30, Date.now()]; @@ -14,24 +20,19 @@ storiesOf('Y-Axis Modes', module) }; return ( - + + + + + ( - + + + )) .add('Collapsed y axis', () => ( - + + + )) .add('Collapsed collection', () => ( - + + + + + )) .add('Collapsed collected series', () => ( - + + + + + + + + + ; }) - .add('Some collapsed', () => { - // eslint-disable-next-line - class SomeCollapsed extends React.Component { - state = { - yAxisDisplayMode: AxisDisplayMode.ALL, - }; - - render() { - const { yAxisDisplayMode } = this.state; - return ( - - - - - - ); - } - } - return ; - }) + .add('Some collapsed', () => ( + + + + + + + + + + )) .add('Some collapsed (until hover)', () => { // eslint-disable-next-line class SomeCollapsed extends React.Component { @@ -273,8 +241,10 @@ storiesOf('Y-Axis Modes', module) + {series.map(s => ( + + ))} { - const { collections, series: stateSeries } = this.state; + const { collections, series } = this.state; if (seriesId === 'collapsed') { - const expand = s => ({ ...s, yAxisDisplayMode: AxisDisplayMode.ALL }); + const expand = (acc, id) => ({ ...acc, [id]: AxisDisplayMode.ALL }); this.setState({ - series: stateSeries.map(expand), - collections: collections.map(expand), + series: Object.keys(series).reduce(expand, {}), + collections: Object.keys(collections).reduce(expand, {}), }); } if (this.collapseTimer) { @@ -347,40 +290,60 @@ storiesOf('Y-Axis Modes', module) }; collapseSome = () => { - const { collections, series: stateSeries } = this.state; + const { collections, series } = this.state; this.collapseTimer = setTimeout(() => { this.setState({ - series: stateSeries.map(s => ({ - ...s, - yAxisDisplayMode: - s.id === 1 || s.id === 3 || s.id === 5 - ? AxisDisplayMode.COLLAPSED - : AxisDisplayMode.ALL, - })), - collections: collections.map(s => ({ - ...s, - yAxisDisplayMode: - s.id === 'default-collapsed' - ? AxisDisplayMode.COLLAPSED - : AxisDisplayMode.ALL, - })), + series: Object.keys(series).reduce( + (acc, id) => ({ + ...acc, + [id]: + id === '1' || id === '3' || id === '5' + ? AxisDisplayMode.COLLAPSED + : AxisDisplayMode.ALL, + }), + {} + ), + collections: Object.keys(collections).reduce( + (acc, id) => ({ + ...acc, + [id]: + id === 'default-collapsed' + ? AxisDisplayMode.COLLAPSED + : AxisDisplayMode.ALL, + }), + {} + ), }); }, 50); }; render() { - const { collections, series, yAxisDisplayMode } = this.state; + const { collections, series } = this.state; return ( + + + + + + + + + @@ -405,11 +368,9 @@ storiesOf('Y-Axis Modes', module) + + + + + + , + + + + , ]) @@ -39,20 +41,20 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + , + + + + , ]) @@ -61,20 +63,20 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + , + + + + , ]) @@ -83,20 +85,20 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[{ id: 1, color: 'steelblue' }, { id: 2, color: 'maroon' }]} > + + , + + + + , ]) @@ -105,36 +107,22 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[ - { id: 1, color: 'steelblue', yAxisPlacement: AxisPlacement.LEFT }, - { id: 2, color: 'maroon', yAxisPlacement: AxisPlacement.RIGHT }, - ]} > + + , + + + + + + , ]) @@ -143,44 +131,26 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[ - { id: 1, color: 'steelblue', yAxisPlacement: AxisPlacement.LEFT }, - { id: 2, color: 'maroon', yAxisPlacement: AxisPlacement.RIGHT }, - { id: 3, color: 'orange', yAxisPlacement: AxisPlacement.BOTH }, - ]} > + + + , + + + + + + + + + , ]) @@ -189,44 +159,26 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[ - { id: 1, color: 'steelblue', yAxisPlacement: AxisPlacement.LEFT }, - { id: 2, color: 'maroon', yAxisPlacement: AxisPlacement.RIGHT }, - { id: 3, color: 'orange', yAxisPlacement: AxisPlacement.BOTH }, - ]} > + + + , + + + + + + + + + , ]) @@ -235,33 +187,20 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[ - { id: 1, color: 'steelblue', yAxisPlacement: AxisPlacement.LEFT }, - { id: 2, color: 'maroon', yAxisPlacement: AxisPlacement.LEFT }, - ]} > + + , + + + + , ]) @@ -270,11 +209,9 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[ - { id: 1, color: 'steelblue', yAxisPlacement: AxisPlacement.LEFT }, - { id: 2, color: 'maroon' }, - ]} > + + , ]) @@ -283,41 +220,28 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[ - { - id: 1, - color: 'steelblue', - yAxisDisplayMode: AxisDisplayMode.COLLAPSED, - }, - { id: 2, color: 'maroon', yAxisDisplayMode: AxisDisplayMode.COLLAPSED }, - ]} > - + + + , + + + + , ]) @@ -326,43 +250,38 @@ storiesOf('Y-Axis Placement', module) key="series" defaultLoader={staticLoader} timeDomain={staticXDomain} - series={[ - { - id: 1, - color: 'steelblue', - yAxisDisplayMode: AxisDisplayMode.COLLAPSED, - }, - { id: 2, color: 'maroon', yAxisDisplayMode: AxisDisplayMode.COLLAPSED }, - { id: 2, color: 'orange', yAxisDisplayMode: AxisDisplayMode.ALL }, - ]} > + + + , + + + + + , ]);