Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

snappy, shared, and programmatically triggered tooltips #81

Merged
merged 12 commits into from
Dec 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 264 additions & 0 deletions packages/demo/examples/01-xy-chart/LineSeriesExample.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import React from 'react';
import { allColors } from '@data-ui/theme/build/color';
import { Button } from '@data-ui/forms';

import {
CrossHair,
LineSeries,
WithTooltip,
XAxis,
YAxis,
} from '@data-ui/xy-chart';

import ResponsiveXYChart, { formatYear } from './ResponsiveXYChart';
import { timeSeriesData } from './data';
import WithToggle from '../shared/WithToggle';

const seriesProps = [
{
seriesKey: 'Stock 1',
key: 'Stock 1',
data: timeSeriesData,
stroke: allColors.grape[9],
showPoints: true,
dashType: 'solid',
},
{
seriesKey: 'Stock 2',
key: 'Stock 2',
data: timeSeriesData.map(d => ({
...d,
y: Math.random() > 0.5 ? d.y * 2 : d.y / 2,
})),
stroke: allColors.grape[7],
strokeDasharray: '6 4',
dashType: 'dashed',
strokeLinecap: 'butt',
},
{
seriesKey: 'Stock 3',
key: 'Stock 3',
data: timeSeriesData.map(d => ({
...d,
y: Math.random() < 0.3 ? d.y * 3 : d.y / 3,
})),
stroke: allColors.grape[4],
strokeDasharray: '2 2',
dashType: 'dotted',
strokeLinecap: 'butt',
},
];

const MARGIN = { left: 8, top: 16 };
const TOOLTIP_TIMEOUT = 250;
const CONTAINER_TRIGGER = 'CONTAINER_TRIGGER';
const VORONOI_TRIGGER = 'VORONOI_TRIGGER';

class LineSeriesExample extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
index: 0,
programmaticTrigger: false,
trigger: CONTAINER_TRIGGER,
stickyTooltip: false,
};
this.eventTriggerRefs = this.eventTriggerRefs.bind(this);
this.triggerTooltip = this.triggerTooltip.bind(this);
this.renderTooltip = this.renderTooltip.bind(this);
this.restartProgrammaticTooltip = this.restartProgrammaticTooltip.bind(this);
this.setTrigger = this.setTrigger.bind(this);
this.handleClick = this.handleClick.bind(this);
}

componentWillUnmount() {
if (this.timeout) clearTimeout(this.timeout);
}

setTrigger(nextTrigger) {
this.setState(() => ({ trigger: nextTrigger }));
}

handleClick(args) {
if (this.triggers) {
this.setState(({ stickyTooltip }) => ({
stickyTooltip: !stickyTooltip,
}), () => {
this.triggers.mousemove(args);
});
}
}

eventTriggerRefs(triggers) {
this.triggers = triggers;
this.triggerTooltip();
}

triggerTooltip() {
if (this.triggers && this.state.index < seriesProps[0].data.length) {
if (this.timeout) clearTimeout(this.timeout);
this.setState(({ index, trigger }) => {
this.triggers.mousemove({
datum: seriesProps[2].data[index],
series: trigger === VORONOI_TRIGGER ? null : {
[seriesProps[0].seriesKey]: seriesProps[0].data[index],
[seriesProps[1].seriesKey]: seriesProps[1].data[index],
[seriesProps[2].seriesKey]: seriesProps[2].data[index],
},
coords: trigger === VORONOI_TRIGGER ? null : {
y: 50,
},
});

this.timeout = setTimeout(this.triggerTooltip, TOOLTIP_TIMEOUT);

return { index: index + 1, programmaticTrigger: true };
});
} else if (this.triggers) {
this.triggers.mouseleave();
this.timeout = setTimeout(() => {
this.setState(() => ({
index: 0,
programmaticTrigger: false,
}));
}, TOOLTIP_TIMEOUT);
}
}


restartProgrammaticTooltip() {
if (this.timeout) clearTimeout(this.timeout);
if (this.triggers) {
this.setState(() => ({ stickyTooltip: false, index: 0 }), this.triggerTooltip);
}
}

renderControls(disableMouseEvents) {
const { trigger, stickyTooltip } = this.state;
const useVoronoiTrigger = trigger === VORONOI_TRIGGER;
return ([
<div key="buttons" style={{ display: 'flex' }}>
<Button
small
rounded
active={!disableMouseEvents && !useVoronoiTrigger}
disabled={disableMouseEvents}
onClick={() => { this.setTrigger(CONTAINER_TRIGGER); }}
> Shared Tooltip
</Button>
<div style={{ width: 8 }} />
<Button
small
rounded
active={!disableMouseEvents && useVoronoiTrigger}
disabled={disableMouseEvents}
onClick={() => { this.setTrigger(VORONOI_TRIGGER); }}
> Voronoi Tooltip
</Button>
<div style={{ width: 16 }} />
<Button
small
rounded
disabled={disableMouseEvents}
onClick={() => { this.restartProgrammaticTooltip(); }}
> Programatically trigger tooltip
</Button>
</div>,
<div key="sticky" style={{ margin: '8px 0', fontSize: 14 }}>
Click chart for a&nbsp;
<span
style={{
fontWeight: stickyTooltip && 600,
textDecoration: stickyTooltip && `underline ${allColors.grape[4]}`,
}}
>sticky tooltip
</span>
</div>,
]);
}

renderTooltip({ datum, series }) {
const { programmaticTrigger, trigger } = this.state;
return (
<div>
<div>
<strong>{formatYear(datum.x)}</strong>
{(!series || Object.keys(series).length === 0) &&
<div>
${datum.y.toFixed(2)}
</div>}
</div>
{trigger === CONTAINER_TRIGGER && <br />}
{seriesProps.map(({ seriesKey, stroke: color, dashType }) => (
series && series[seriesKey] &&
<div key={seriesKey}>
<span
style={{
color,
textDecoration: !programmaticTrigger && series[seriesKey] === datum
? `underline ${dashType} ${color}` : null,
fontWeight: series[seriesKey] === datum ? 600 : 200,
}}
>
{`${seriesKey} `}
</span>
${series[seriesKey].y.toFixed(2)}
</div>
))}
</div>
);
}

render() {
const { trigger, stickyTooltip } = this.state;
const useVoronoiTrigger = trigger === VORONOI_TRIGGER;
return (
<WithToggle id="line_mouse_events_toggle" label="Disable mouse events">
{disableMouseEvents => (
<div>
{this.renderControls(disableMouseEvents)}

{/* Use WithTooltip to intercept mouse events in stickyTooltip state */}
<WithTooltip renderTooltip={this.renderTooltip}>
{({ onMouseLeave, onMouseMove, tooltipData }) => (
<ResponsiveXYChart
ariaLabel="Required label"
eventTrigger={useVoronoiTrigger ? 'voronoi' : 'container'}
eventTriggerRefs={this.eventTriggerRefs}
margin={MARGIN}
onClick={disableMouseEvents ? null : this.handleClick}
onMouseMove={disableMouseEvents || stickyTooltip ? null : onMouseMove}
onMouseLeave={disableMouseEvents || stickyTooltip ? null : onMouseLeave}
renderTooltip={null}
showVoronoi={useVoronoiTrigger}
snapTooltipToDataX
snapTooltipToDataY={useVoronoiTrigger}
tooltipData={tooltipData}
xScale={{ type: 'time' }}
yScale={{ type: 'linear' }}
>
<XAxis label="Time" numTicks={5} />
<YAxis label="Stock price ($)" numTicks={4} />
{seriesProps.map(props => (
<LineSeries {...props} disableMouseEvents={disableMouseEvents} />
))}
<CrossHair
fullHeight
showHorizontalLine={false}
strokeDasharray=""
stroke={allColors.grape[4]}
circleStroke={allColors.grape[4]}
circleFill="#fff"
showCircle={useVoronoiTrigger || !this.state.programmaticTrigger}
/>
</ResponsiveXYChart>
)}
</WithTooltip>
</div>
)}
</WithToggle>
);
}
}

export default LineSeriesExample;
3 changes: 2 additions & 1 deletion packages/demo/examples/01-xy-chart/ResponsiveXYChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {

export const parseDate = timeParse('%Y%m%d');
export const formatDate = timeFormat('%b %d');
export const formatYear = timeFormat('%Y');
export const dateFormatter = date => formatDate(parseDate(date));

// this is a little messy to handle all cases across series types
function renderTooltip({ datum, seriesKey, color }) {
export function renderTooltip({ datum, seriesKey, color }) {
const { x, x0, y, value } = datum;
let xVal = x || x0;
if (typeof xVal === 'string') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class ScatterWithHistogram extends React.PureComponent {
margin={marginScatter}
theme={theme}
renderTooltip={renderTooltip}
useVoronoi
eventTrigger="voronoi"
showVoronoi={this.state.showVoronoi}
>
{datasets.map((dataset, i) => (
Expand Down
39 changes: 19 additions & 20 deletions packages/demo/examples/01-xy-chart/StackedAreaExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,25 @@ export default function StackedAreaExample() {
return (
<WithToggle id="lineseries_toggle" label="As percent" initialChecked>
{asPercent => (
<div
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
>
<LegendOrdinal
key="legend"
direction="row"
scale={legendScale}
shape={({ fill, width, height }) => (
<svg width={width} height={height}>
<rect
width={width}
height={height}
fill={fill}
/>
</svg>
)}
fill={({ datum }) => legendScale(datum)}
labelFormat={label => label}
/>

<div>
<div style={{ marginLeft: 24 }}>
<LegendOrdinal
key="legend"
direction="row"
scale={legendScale}
shape={({ fill, width, height }) => (
<svg width={width} height={height}>
<rect
width={width}
height={height}
fill={fill}
/>
</svg>
)}
fill={({ datum }) => legendScale(datum)}
labelFormat={label => label}
/>
</div>
<ResponsiveXYChart
ariaLabel="Stacked area chart of temperatures"
key="chart"
Expand Down
Loading