Skip to content

Commit

Permalink
[Enhancement] Create factory for histogram and line chart, add brush …
Browse files Browse the repository at this point in the history
…handle to range brush (#1274)

- create factory for hsitogram and linechart
- improve brush interaction
- improve line chart
- improve histogram
- add initial range-plot test

Signed-off-by: Shan He <heshan0131@gmail.com>
  • Loading branch information
heshan0131 committed Sep 18, 2020
1 parent 6681d2e commit ad65170
Show file tree
Hide file tree
Showing 20 changed files with 549 additions and 292 deletions.
18 changes: 15 additions & 3 deletions src/components/common/animation-control/playback-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,38 +44,50 @@ const IconButton = styled(Button)`

function nop() {}
const DEFAULT_BUTTON_HEIGHT = '18px';
const DEFAULT_ICONS = {
reset: Reset,
play: Play,
pause: Pause
};

function AnimationPlaybacksFactory() {
const AnimationPlaybacks = ({
isAnimatable,
isAnimating,
buttonStyle,
width,
pauseAnimation = nop,
updateAnimationTime = nop,
startAnimation = nop,
buttonHeight = DEFAULT_BUTTON_HEIGHT
buttonHeight = DEFAULT_BUTTON_HEIGHT,
playbackIcons = DEFAULT_ICONS
}) => {
const btnStyle = buttonStyle ? {[buttonStyle]: true} : {};
return (
<StyledAnimationControls
className={classnames('time-range-slider__control', {
disabled: !isAnimatable
})}
style={{width: `${width}px`}}
>
<ButtonGroup>
<IconButton
className="playback-control-button"
{...btnStyle}
onClick={updateAnimationTime}
>
<Reset height={buttonHeight} />
<playbackIcons.reset height={buttonHeight} />
</IconButton>
<IconButton
{...btnStyle}
className={classnames('playback-control-button', {active: isAnimating})}
onClick={isAnimating ? pauseAnimation : startAnimation}
>
{isAnimating ? <Pause height={buttonHeight} /> : <Play height={buttonHeight} />}
{isAnimating ? (
<playbackIcons.pause height={buttonHeight} />
) : (
<playbackIcons.play height={buttonHeight} />
)}
</IconButton>
</ButtonGroup>
</StyledAnimationControls>
Expand Down
1 change: 1 addition & 0 deletions src/components/common/field-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ const FieldSelectorFactory = FieldListItemFactory => {
filterOption="name"
fixedOptions={this.props.suggested}
inputTheme={this.props.inputTheme}
size={this.props.size}
isError={this.props.error}
selectedItems={this.selectedItemsSelector(this.props)}
erasable={this.props.erasable}
Expand Down
82 changes: 82 additions & 0 deletions src/components/common/histogram-plot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, {useMemo} from 'react';
import {scaleLinear} from 'd3-scale';
import {max} from 'd3-array';
import styled from 'styled-components';
import classnames from 'classnames';

const histogramStyle = {
highlightW: 0.7,
unHighlightedW: 0.4
};

const HistogramWrapper = styled.svg`
overflow: visible;
.histogram-bars {
rect {
fill: ${props => props.theme.histogramFillOutRange};
}
rect.in-range {
fill: ${props => props.theme.histogramFillInRange};
}
}
`;

function HistogramPlotFactory() {
const HistogramPlot = ({width, height, margin, isRanged, histogram, value, brushComponent}) => {
const domain = useMemo(() => [histogram[0].x0, histogram[histogram.length - 1].x1], [
histogram
]);
const dataId = Object.keys(histogram[0]).filter(k => k !== 'x0' && k !== 'x1')[0];

// use 1st for now
const getValue = useMemo(() => d => d[dataId], [dataId]);

const x = useMemo(
() =>
scaleLinear()
.domain(domain)
.range([0, width]),
[domain, width]
);

const y = useMemo(
() =>
scaleLinear()
.domain([0, max(histogram, getValue)])
.range([0, height]),
[histogram, height, getValue]
);

const barWidth = width / histogram.length;

return (
<HistogramWrapper width={width} height={height} style={{marginTop: `${margin.top}px`}}>
<g className="histogram-bars">
{histogram.map(bar => {
const inRange = bar.x1 <= value[1] + 1 && bar.x0 >= value[0];
const wRatio = inRange ? histogramStyle.highlightW : histogramStyle.unHighlightedW;
return (
<rect
className={classnames({'in-range': inRange})}
key={bar.x0}
height={y(getValue(bar))}
width={barWidth * wRatio}
x={x(bar.x0) + (barWidth * (1 - wRatio)) / 2}
rx={1}
ry={1}
y={height - y(getValue(bar))}
/>
);
})}
</g>
<g transform={`translate(${isRanged ? 0 : barWidth / 2}, 0)`}>{brushComponent}</g>
</HistogramWrapper>
);
};

const EmpptyOrPlot = props =>
!props.histogram || !props.histogram.length ? null : <HistogramPlot {...props} />;

return EmpptyOrPlot;
}
export default HistogramPlotFactory;
4 changes: 2 additions & 2 deletions src/components/common/item-selector/chickleted-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ const ChickletedInputContainer = styled.div`
props.inputTheme === 'secondary'
? props.theme.secondaryChickletedInput
: props.inputTheme === 'light'
? props.theme.chickletedInputLT
: props.theme.chickletedInput}
? props.theme.chickletedInputLT
: props.theme.chickletedInput}
color: ${props =>
props.hasPlaceholder ? props.theme.selectColorPlaceHolder : props.theme.selectColor};
Expand Down
6 changes: 4 additions & 2 deletions src/components/common/item-selector/item-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const StyledDropdownSelect = styled.div.attrs({
? props.theme.inputLT
: props.theme.input};
height: ${props => props.theme.dropdownSelectHeight}px;
height: ${props =>
props.size === 'small' ? props.theme.inputBoxHeightSmall : props.theme.inputBoxHeight};
.list__item__anchor {
${props => props.theme.dropdownListAnchor};
Expand Down Expand Up @@ -269,7 +270,8 @@ class ItemSelector extends Component {
onClick: this._showTypeahead,
onFocus: this._showPopover,
error: this.props.isError,
inputTheme: this.props.inputTheme
inputTheme: this.props.inputTheme,
size: this.props.size
};
const intl = this.props.intl;

Expand Down
132 changes: 132 additions & 0 deletions src/components/common/line-chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React, {useMemo} from 'react';
import moment from 'moment';
import {
HorizontalGridLines,
LineSeries,
XYPlot,
CustomSVGSeries,
Hint,
YAxis,
MarkSeries
} from 'react-vis';
import styled from 'styled-components';
import {getTimeWidgetHintFormatter} from 'utils/filter-utils';

const LineChartWrapper = styled.div`
.rv-xy-plot {
/* important for rendering hint */
position: relative;
}
.rv-xy-plot__inner {
/* important to show axis */
overflow: visible;
}
.rv-xy-plot__grid-lines__line {
stroke: ${props => props.theme.histogramFillOutRange};
stroke-dasharray: 1px 4px;
}
.rv-xy-plot__axis__tick__text {
font-size: 9px;
fill: ${props => props.theme.textColor};
}
`;

const StyledHint = styled.div`
background-color: #d3d8e0;
border-radius: 2px;
color: ${props => props.theme.textColorLT};
font-size: 9px;
margin: 4px;
padding: 3px 6px;
pointer-events: none;
user-select: none;
`;

const HintContent = ({x, y, format}) => (
<StyledHint>
<div className="hint--x">{format(x)}</div>
<div className="row">{y}</div>
</StyledHint>
);

const MARGIN = {top: 0, bottom: 0, left: 0, right: 0};
function LineChartFactory() {
const LineChart = ({
brushComponent,
brushing,
color,
enableChartHover,
height,
hoveredDP,
isEnlarged,
lineChart,
margin,
onMouseMove,
width
}) => {
const {xDomain, series, yDomain} = lineChart;
const hintFormatter = useMemo(() => {
return getTimeWidgetHintFormatter(xDomain);
}, [xDomain]);

const brushData = useMemo(() => {
return [{x: series[0].x, y: yDomain[1], customComponent: () => brushComponent}];
}, [series, yDomain, brushComponent]);

return (
<LineChartWrapper style={{marginTop: `${margin.top}px`}}>
<XYPlot
xType="time"
width={width}
height={height}
margin={MARGIN}
onMouseLeave={() => {
onMouseMove(null);
}}
>
<HorizontalGridLines tickTotal={3} />
<LineSeries
style={{fill: 'none'}}
strokeWidth={2}
color={color}
data={series}
onNearestX={enableChartHover ? onMouseMove : null}
/>
<MarkSeries data={hoveredDP ? [hoveredDP] : []} color={color} size={3} />
<CustomSVGSeries data={brushData} />
{isEnlarged && <YAxis tickTotal={3} />}
{hoveredDP && enableChartHover && !brushing ? (
<Hint value={hoveredDP}>
<HintContent {...hoveredDP} format={val => moment.utc(val).format(hintFormatter)} />
</Hint>
) : null}
</XYPlot>
</LineChartWrapper>
);
};
return LineChart;
}

export default LineChartFactory;

0 comments on commit ad65170

Please sign in to comment.