Skip to content

Commit

Permalink
Merge pull request #81 from williaster/chris--customize-tooltip-behavior
Browse files Browse the repository at this point in the history
snappy, shared, and programmatically triggered tooltips
  • Loading branch information
williaster authored Dec 7, 2017
2 parents 4bb987d + 4e3f04e commit 2a72bc7
Show file tree
Hide file tree
Showing 59 changed files with 1,650 additions and 716 deletions.
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

0 comments on commit 2a72bc7

Please sign in to comment.