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)
+
+
))
.add('Without context chart', () => (
-
+
+
+
))
@@ -616,11 +545,9 @@ storiesOf('LineChart', module)
+
+
;
})
- .add('Dynamic x domain', () => {
+ .add('Dynamic time domain', () => {
class DynamicXDomain extends React.Component {
state = {
- xDomain: staticXDomain,
+ timeDomain: staticXDomain,
};
- toggleXDomain = () => {
- const { xDomain } = this.state;
- const newDomain = isEqual(xDomain, staticXDomain)
+ toggleTimeDomain = () => {
+ const { timeDomain } = this.state;
+ const newDomain = isEqual(timeDomain, staticXDomain)
? [
staticXDomain[0] - 100000000 * 50,
staticXDomain[1] + 100000000 * 50,
]
: staticXDomain;
- this.setState({ xDomain: newDomain });
+ this.setState({ timeDomain: newDomain });
};
render() {
- const { xDomain } = this.state;
+ const { timeDomain } = this.state;
return (
-
- {isEqual(xDomain, staticXDomain)
- ? 'Shrink xDomain'
+
+ {isEqual(timeDomain, staticXDomain)
+ ? 'Shrink timeDomain'
: 'Reset base domain'}
-
+
+
+
@@ -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 => (
+
+ {String(value)}
+
+ ))}
+
+ reset to default
+
+
+ );
+ });
+ };
+
+ 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 },
- ]}
>
+
+
+
,
+
+
+
+
+
,
]);