Skip to content

Commit

Permalink
Add a configurable legend to timeline tracks
Browse files Browse the repository at this point in the history
Remove sorting from event attributes in tooltip
  • Loading branch information
alisman committed May 2, 2023
1 parent 44be7b9 commit 753645f
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 59 deletions.
23 changes: 20 additions & 3 deletions packages/cbioportal-clinical-timeline/src/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ function handleMouseEvents(
}

const hoverCallback = (
e: React.MouseEvent<any>,
e: React.MouseEvent<Element, MouseEvent>,
styleTag: MutableRefObject<null>,
uniqueId: string
) => {
Expand All @@ -168,12 +168,29 @@ const hoverCallback = (
#${uniqueId} .tl-timeline .tl-track:nth-child(${trackIndex +
1}) .tl-track-highlight {
opacity: 1 !important;
}
}
#${uniqueId} .tl-tracklegend:nth-of-type(${trackIndex +
1}) {
display:block !important;
}
`);
}
break;
default:
jQuery(styleTag.current!).empty();
// when mouse is moving INTO the tooltip, we do not want to abide the
// mouseleave event. we treat it as if it's part of the track element
if (
e.type === 'mouseleave' &&
(e?.relatedTarget as Element).getAttribute('class') ===
'arrow'
) {
break;
} else {
// get rid of the above created inline style
jQuery(styleTag.current!).empty();
}
break;
}
}
Expand Down
7 changes: 6 additions & 1 deletion packages/cbioportal-clinical-timeline/src/TimelineStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,12 @@ export class TimelineStore {
if (content === null) {
// Show default tooltip if there's no custom track tooltip renderer,
// or if the track renderer returns `null`
content = <EventTooltipContent event={activeItem} />;
content = (
<EventTooltipContent
trackConfig={tooltipModel.track.trackConf}
event={activeItem}
/>
);
}

const multipleItems = tooltipModel.events.length > 1;
Expand Down
72 changes: 40 additions & 32 deletions packages/cbioportal-clinical-timeline/src/TimelineTrack.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
EventPosition,
POINT_COLOR,
ITrackEventConfig,
POINT_RADIUS,
TimeLineColorGetter,
TimelineEvent,
TimelineEventAttribute,
TimelineTrackSpecification,
TimelineTrackType,
} from './types';
Expand All @@ -14,6 +15,7 @@ import {
formatDate,
getTrackEventCustomColorGetterFromConfiguration,
REMOVE_FOR_DOWNLOAD_CLASSNAME,
segmentAndSortAttributesForTooltip,
TIMELINE_TRACK_HEIGHT,
} from './lib/helpers';
import { TimelineStore } from './TimelineStore';
Expand Down Expand Up @@ -449,47 +451,53 @@ export const OurPopup: React.FunctionComponent<any> = observer(function(

export const EventTooltipContent: React.FunctionComponent<{
event: TimelineEvent;
}> = function({ event }) {
const attributes = event.event.attributes.filter(attr => {
trackConfig: ITrackEventConfig | undefined;
}> = function({ event, trackConfig }) {
let attributes = event.event.attributes.filter(attr => {
return (
attr.key !== COLOR_ATTRIBUTE_KEY && attr.key !== SHAPE_ATTRIBUTE_KEY
);
});

// if we have an attribute order configuration, we need to
// update attribute list accordingly
if (trackConfig?.attributeOrder) {
attributes = segmentAndSortAttributesForTooltip(
attributes,
trackConfig.attributeOrder
);
}

return (
<div>
<table>
<table className={'table table-condensed'}>
<tbody>
{_.map(
attributes.sort((a: any, b: any) =>
a.key > b.key ? 1 : -1
),
(att: any) => {
return (
<tr>
<th>{att.key.replace(/_/g, ' ')}</th>
<td>
<ReactMarkdown
allowedElements={['p', 'a']}
linkTarget={'_blank'}
components={{
a: ({ node, ...props }) => (
<OurPopup {...props} />
),
}}
>
{att.value}
</ReactMarkdown>
</td>
</tr>
);
}
)}
{_.map(attributes, (att: any) => {
return (
<tr>
<td>{att.key.replace(/_/g, ' ')}</td>
<td>
<ReactMarkdown
allowedElements={['p', 'a']}
linkTarget={'_blank'}
components={{
a: ({ node, ...props }) => (
<OurPopup {...props} />
),
}}
>
{att.value}
</ReactMarkdown>
</td>
</tr>
);
})}
<tr>
<th>{`${
<td>{`${
event.event.endNumberOfDaysSinceDiagnosis
? 'START DATE'
: 'DATE'
}`}</th>
}`}</td>
<td className={'nowrap'}>
{formatDate(
event.event.startNumberOfDaysSinceDiagnosis
Expand All @@ -498,7 +506,7 @@ export const EventTooltipContent: React.FunctionComponent<{
</tr>
{event.event.endNumberOfDaysSinceDiagnosis && (
<tr>
<th>END DATE</th>
<td>END DATE</td>
<td className={'nowrap'}>
{formatDate(
event.event.endNumberOfDaysSinceDiagnosis
Expand Down
84 changes: 74 additions & 10 deletions packages/cbioportal-clinical-timeline/src/TimelineTracks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import CustomTrack, { CustomTrackSpecification } from './CustomTrack';
import { TICK_AXIS_HEIGHT } from './TickAxis';
import { useObserver } from 'mobx-react-lite';
import { getBrowserWindow } from 'cbioportal-frontend-commons';
import ReactDOM from 'react-dom';
import { TimelineTrackSpecification } from './types';

export interface ITimelineTracks {
store: TimelineStore;
Expand Down Expand Up @@ -38,16 +40,22 @@ export const TimelineTracks: React.FunctionComponent<ITimelineTracks> = observer

if (isTrackVisible) {
return (
<TimelineTrack
limit={store.trimmedLimit}
trackData={track.track}
getPosition={store.getPosition}
handleTrackHover={handleTrackHover}
store={store}
y={y}
height={track.height}
width={width}
/>
<>
<TimelineTrack
limit={store.trimmedLimit}
trackData={track.track}
getPosition={store.getPosition}
handleTrackHover={handleTrackHover}
store={store}
y={y}
height={track.height}
width={width}
/>
<TimelineTrackLegend
y={y + 20}
track={track.track}
/>
</>
);
} else {
return null;
Expand Down Expand Up @@ -90,6 +98,7 @@ export const TimelineTracks: React.FunctionComponent<ITimelineTracks> = observer
transform: placementLeft
? 'translate(-100%, 0)'
: '',
minWidth: 400,
}}
className={'tl-timeline-tooltip cbioTooltip'}
positionLeft={
Expand All @@ -109,3 +118,58 @@ export const TimelineTracks: React.FunctionComponent<ITimelineTracks> = observer
);

export default TimelineTracks;

export const TimelineTrackLegend: React.FC<{
y: number;
track: TimelineTrackSpecification;
}> = function({ y, track }) {
let legendEl = <span className={'tl-tracklegend'}></span>;

if (track.trackConf?.legend) {
legendEl = (
<div
className={'positionAbsolute tl-tracklegend tl-displaynone'}
style={{ top: y }}
>
<strong>Track Legend:</strong>
<table>
<tbody>
{track.trackConf.legend.map(item => {
return (
<tr>
<td>
<svg
viewBox="0 0 10 10"
height={8}
width={8}
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="5"
cy="5"
r="4"
fill={item.color}
stroke={'#000000'}
/>
</svg>
</td>
<td>{item.label}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
} else {
// we need to have an element for the css selector stratetgy to work
legendEl = (
<div className={'positionAbsolute tl-tracklegend hidden'}></div>
);
}

return ReactDOM.createPortal(
legendEl,
document.getElementsByClassName('tl-timelineviewport')[0]
);
};
34 changes: 34 additions & 0 deletions packages/cbioportal-clinical-timeline/src/lib/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
formatDate,
getAttributeValue,
getPointInTrimmedSpace,
segmentAndSortAttributesForTooltip,
} from './helpers';
import intersect from './intersect';
import { TimelineEvent } from '../types';
Expand Down Expand Up @@ -256,3 +257,36 @@ describe('color getter helpers', () => {
);
});
});

describe('order attributes according to config', () => {
const atts = [
{ key: 'key1', value: 'someValue' },
{ key: 'key2', value: 'someValue' },
{ key: 'key3', value: 'someValue' },
{ key: 'key4', value: 'someValue' },
];

it('places attributes in order by config', () => {
const processedAtts = segmentAndSortAttributesForTooltip(atts, [
'key4',
'key2',
]);

assert.deepEqual(
processedAtts.map(a => a.key),
['key4', 'key2', 'key1', 'key3']
);
});

it('handles missing attributes in config', () => {
const processedAtts = segmentAndSortAttributesForTooltip(atts, [
'key10',
'key2',
]);

assert.deepEqual(
processedAtts.map(a => a.key),
['key2', 'key1', 'key3', 'key4']
);
});
});
33 changes: 33 additions & 0 deletions packages/cbioportal-clinical-timeline/src/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
POINT_COLOR,
SegmentedAttributes,
TimeLineColorGetter,
TimelineEvent,
TimelineEventAttribute,
TimelineTick,
TimelineTrackSpecification,
TimelineTrackType,
Expand Down Expand Up @@ -304,3 +306,34 @@ export const colorGetterFactory = (eventColorGetter?: TimeLineColorGetter) => {
}
};
};

export function segmentAndSortAttributesForTooltip(
attributes: TimelineEventAttribute[],
attributeOrder: string[]
) {
const segmentedAttributes = attributes.reduce(
(agg: SegmentedAttributes, att) => {
if (attributeOrder?.includes(att.key)) {
agg.first.push(att);
} else {
agg.rest.push(att);
}
return agg;
},
{ first: [], rest: [] }
);

// now we need to sort the first according to the provided attribute configuration
// make a map keyed by attr key for easy lookup
const attrMap = _.keyBy(segmentedAttributes.first, att => att.key);
// iterate through the configuration and get the attr object for
// each type
segmentedAttributes.first = _(attributeOrder)
.map(k => attrMap[k])
// we need to get rid of undefined if corresponding att is missing (sometimes is)
.compact()
.value();

// now overwrite attributes with first followed by rest list
return segmentedAttributes.first.concat(segmentedAttributes.rest);
}
Loading

0 comments on commit 753645f

Please sign in to comment.