Feature/ux fixes (#167)

* fix card toggle

* fix bug and bar marker

* reinstate timeline arrows

* adjust (hard to interpret) category y calculation

* shadows for markers as well

* return markers when there are no categories

* remove year in timeline

* make notifications optional

* WIP: render hovered counts

* show number on hover

* lint

* revert to filteredLocations

* linting

* return mapClustersToLocations

* 💄

* lint

Co-authored-by: efarooqui <efarooqui@pandora.com>
This commit is contained in:
Lachlan Kermode
2020-10-22 20:09:13 +03:00
committed by GitHub
parent f44d3e2481
commit ddbee03f50
9 changed files with 153 additions and 135 deletions

View File

@@ -288,7 +288,7 @@ class Dashboard extends React.Component {
<CardStack
timelineDims={app.timeline.dimensions}
onViewSource={this.handleViewSource}
onSelect={app.associations.narrative ? this.selectNarrativeStep : this.handleSelect}
onSelect={app.associations.narrative ? this.selectNarrativeStep : () => null}
onHighlight={this.handleHighlight}
onToggleCardstack={() => actions.updateSelected([])}
getCategoryColor={this.getCategoryColor}
@@ -316,11 +316,11 @@ class Dashboard extends React.Component {
onClose: actions.toggleInfoPopup
}}
/>
<Notification
{app.debug ? <Notification
isNotification={app.flags.isNotification}
notifications={domain.notifications}
onToggle={actions.markNotificationsRead}
/>
/> : null}
<Search
narrative={app.narrative}
queryString={app.searchQuery}

View File

@@ -89,11 +89,7 @@ class Map extends React.Component {
.setMaxBounds(mapConfig.maxBounds)
// Initialize supercluster index
this.superclusterIndex = new Supercluster({
radius: clusterConfig.radius,
maxZoom: clusterConfig.maxZoom,
minZoom: clusterConfig.minZoom
})
this.superclusterIndex = new Supercluster(clusterConfig)
let firstLayer
@@ -116,6 +112,7 @@ class Map extends React.Component {
map.zoomControl.remove()
map.on('moveend', () => {
// console.log(map.getZoom())
this.updateClusters()
this.alignLayers()
})
@@ -199,11 +196,9 @@ class Map extends React.Component {
}
}
onClusterSelect (e) {
const { id } = e.target
const { longitude, latitude } = e.target.attributes
onClusterSelect ({ id, latitude, longitude }) {
const expansionZoom = Math.max(this.superclusterIndex.getClusterExpansionZoom(parseInt(id)), this.superclusterIndex.options.minZoom)
this.map.flyTo(new L.LatLng(latitude.value, longitude.value), expansionZoom)
this.map.flyTo(new L.LatLng(latitude, longitude), expansionZoom)
}
getClientDims () {

View File

@@ -81,7 +81,7 @@ class Timeline extends React.Component {
categories = categories.filter(cat => !features.GRAPH_NONLOCATED.categories.includes(cat.id))
}
const catHeight = trackHeight / (categories.length)
const shiftUp = trackHeight / (categories.length) / 2
const shiftUp = trackHeight / (categories.length) / 3
const marginShift = marginTop === 0 ? 0 : marginTop
const manualAdjustment = trackHeight <= 60 ? (trackHeight <= 30 ? -8 : -5) : 0
const catsYpos = categories.map((g, i) => {
@@ -138,7 +138,6 @@ class Timeline extends React.Component {
* @param {String} direction: 'forward' / 'backwards'
*/
onMoveTime (direction) {
this.props.methods.onSelect()
const extent = this.getTimeScaleExtent()
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2)
@@ -277,9 +276,10 @@ class Timeline extends React.Component {
return this.state.dims.trackHeight / 2
}
const { category, project } = event
const { category } = event
if (GRAPH_NONLOCATED && GRAPH_NONLOCATED.categories.includes(category)) {
const { project } = event
return this.state.dims.marginTop + domain.projects[project].offset + this.props.ui.eventRadius
}
if (!this.state.scaleY) return 0
@@ -359,6 +359,7 @@ class Timeline extends React.Component {
selected={this.props.app.selected}
getEventX={ev => this.getDatetimeX(ev.datetime)}
getEventY={this.getY}
categories={this.props.domain.categories}
transitionDuration={this.state.transitionDuration}
styles={this.props.ui.styles}
features={this.props.features}

View File

@@ -20,8 +20,8 @@ class TimelineAxis extends React.Component {
sndFmt = ''
// 1yr
} else if (this.props.extent > 43200) {
sndFmt = '%Y'
fstFmt = '%d %b'
sndFmt = '%d %b'
fstFmt = ''
} else {
sndFmt = '%d %b'
fstFmt = '%H:%M'

View File

@@ -1,7 +1,7 @@
import React from 'react'
import React, { useState } from 'react'
import { Portal } from 'react-portal'
import colors from '../../../common/global.js'
import { calcClusterOpacity, calcClusterSize } from '../../../common/utilities'
import { calcClusterOpacity, calcClusterSize, isLatitude, isLongitude } from '../../../common/utilities'
const DefsClusters = () => (
<defs>
@@ -12,97 +12,108 @@ const DefsClusters = () => (
</defs>
)
function Cluster ({ cluster, size, projectPoint, totalPoints, styles, renderHover, onClick }) {
/**
{
geometry: {
coordinates: [longitude, latitude]
},
properties: {
cluster: true|false,
cluster_id: int,
point_count: int,
point_count_abbreviated: int
},
type: "Feature"
}
*/
const { cluster_id: clusterId } = cluster.properties
const { coordinates } = cluster.geometry
const [longitude, latitude] = coordinates
if (!isLatitude(latitude) || !isLongitude(longitude)) return null
const { x, y } = projectPoint([latitude, longitude])
const [hovered, setHovered] = useState(false)
return (
<g
className={'cluster-event'}
transform={`translate(${x}, ${y})`}
onClick={e => onClick({ id: clusterId, latitude, longitude })}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<circle
class='cluster-event-marker'
id={clusterId}
longitude={longitude}
latitude={latitude}
cx='0'
cy='0'
r={size}
style={{
...styles
}}
/>
{hovered ? renderHover(cluster) : null}
</g>
)
}
function ClusterEvents ({
projectPoint,
styleCluster,
onSelect,
isRadial,
svg,
clusters
}) {
function calculateTotalPoints () {
return clusters.reduce((total, cl) => {
if (cl && cl.properties) {
total += cl.properties.point_count
}
return total
}, 0)
const totalPoints = clusters.reduce((total, cl) => {
if (cl && cl.properties) {
total += cl.properties.point_count
}
return total
}, 0)
const styles = {
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
stroke: colors.darkBackground,
strokeWidth: 0
}
function renderClusterBySize (cluster) {
const { point_count: pointCount, cluster_id: clusterId } = cluster.properties
const { coordinates } = cluster.geometry
const [longitude, latitude] = coordinates
const totalPoints = calculateTotalPoints()
const styles = {
fill: isRadial ? "url('#clusterGradient')" : colors.fallbackEventColor,
stroke: colors.darkBackground,
strokeWidth: 0,
fillOpacity: calcClusterOpacity(pointCount, totalPoints)
}
return (
<React.Fragment>
{<circle
class='cluster-event-marker'
id={clusterId}
longitude={longitude}
latitude={latitude}
cx='0'
cy='0'
r={calcClusterSize(pointCount, totalPoints)}
style={styles}
/>}
</React.Fragment>
)
}
function renderCluster (cluster) {
/**
{
geometry: {
coordinates: [longitude, latitude]
},
properties: {
cluster: true|false,
cluster_id: int,
point_count: int,
point_count_abbreviated: int
},
type: "Feature"
}
*/
const { coordinates } = cluster.geometry
const [longitude, latitude] = coordinates
if (!latitude || !longitude) return null
const { x, y } = projectPoint([latitude, longitude])
const customStyles = styleCluster ? styleCluster(cluster) : null
const extraRender = () => (
<React.Fragment>
{customStyles[1]}
</React.Fragment>
)
return (
<g
className={'cluster-event'}
transform={`translate(${x}, ${y})`}
onClick={(e) => onSelect(e)}
>
{renderClusterBySize(cluster)}
{extraRender ? extraRender() : null}
</g>
)
function renderHover (txt) {
return <text text-anchor='middle' y='-3px' style={{ fontWeight: 'bold', fill: 'white' }}>{txt}</text>
}
return (
<Portal node={svg}>
<g className='cluster-locations'>
{isRadial ? <DefsClusters /> : null}
{clusters.map(renderCluster)}
{clusters.map(c => {
const pointCount = c.properties.point_count
const clusterSize = calcClusterSize(pointCount, totalPoints)
return <Cluster
onClick={onSelect}
cluster={c}
size={clusterSize}
projectPoint={projectPoint}
totalPoints={totalPoints}
styles={{
...styles,
fillOpacity: calcClusterOpacity(pointCount, totalPoints)
}}
renderHover={clster => <>
<circle
class='event-hover'
cx='0'
cy='0'
r={clusterSize + 2}
stroke={colors.primaryHighlight}
fill-opacity='0.0'
/>
{renderHover(pointCount)}
</>}
/>
})}
</g>
</Portal>
)

View File

@@ -1,26 +1,24 @@
import React from 'react'
const TimelineHandles = ({ dims, onMoveTime }) => {
return <div />
// temporarilty disabled while we get functionality working again
// return (
// <g className='time-controls-inline'>
// <g
// transform={`translate(${dims.marginLeft - 20}, ${dims.contentHeight - 10})`}
// onClick={() => onMoveTime('backwards')}
// >
// <circle r='15' />
// <path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform='rotate(270)' />
// </g>
// <g
// transform={`translate(${dims.width - dims.width_controls + 20}, ${dims.contentHeight - 10})`}
// onClick={() => onMoveTime('forward')}
// >
// <circle r='15' />
// <path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform='rotate(90)' />
// </g>
// </g>
// )
return (
<g className='time-controls-inline'>
<g
transform={`translate(${dims.marginLeft - 20}, ${dims.contentHeight - 10})`}
onClick={() => onMoveTime('backwards')}
>
<circle r='15' />
<path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform='rotate(270)' />
</g>
<g
transform={`translate(${dims.width - dims.width_controls + 20}, ${dims.contentHeight - 10})`}
onClick={() => onMoveTime('forward')}
>
<circle r='15' />
<path d='M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z' transform='rotate(90)' />
</g>
</g>
)
}
export default TimelineHandles

View File

@@ -1,18 +1,20 @@
import React from 'react'
import colors from '../../../common/global'
import { getEventCategories } from '../../../common/utilities'
const TimelineMarkers = ({
styles,
eventRadius,
getEventX,
getEventY,
categories,
transitionDuration,
selected,
dims,
features
}) => {
function renderMarker (event) {
function renderCircle () {
function renderMarker (acc, event) {
function renderCircle (y) {
return <circle
className='timeline-marker'
cx={0}
@@ -23,10 +25,10 @@ const TimelineMarkers = ({
stroke-linejoin='round'
stroke-dasharray={styles ? styles['stroke-dasharray'] : '2,2'}
style={{
'transform': `translate(${getEventX(event)}px, ${getEventY(event)}px)`,
'transform': `translate(${getEventX(event)}px, ${y}px)`,
'-webkit-transition': `transform ${transitionDuration / 1000}s ease`,
'-moz-transition': 'none',
'opacity': 0.9
'opacity': 1
}}
r={eventRadius * 2}
/>
@@ -35,8 +37,8 @@ const TimelineMarkers = ({
return <rect
className='timeline-marker'
x={0}
y={0}
width={eventRadius / 2}
y={dims.marginTop}
width={eventRadius / 1.5}
height={dims.contentHeight - 55}
stroke={styles ? styles.stroke : colors.primaryHighlight}
stroke-opacity='1'
@@ -48,27 +50,38 @@ const TimelineMarkers = ({
}}
/>
}
const isDot = (!!event.location && !!event.longitude) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
switch (event.shape) {
case 'circle':
return renderCircle()
case 'bar':
return renderBar()
case 'diamond':
return renderCircle()
case 'star':
return renderCircle()
default:
return isDot ? renderCircle() : renderBar()
const isDot = (!!event.location && !!event.longitude) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
const evShadows = getEventCategories(event, categories).map(cat => getEventY({ ...event, category: cat.id }))
function renderMarkerForEvent (y) {
switch (event.shape) {
case 'circle':
case 'diamond':
case 'star':
acc.push(renderCircle(y))
break
case 'bar':
acc.push(renderBar(y))
break
default:
return isDot ? acc.push(renderCircle(y)) : acc.push(renderBar(y))
}
}
if (evShadows.length > 0) {
evShadows.forEach(renderMarkerForEvent)
} else {
renderMarkerForEvent(getEventY(event))
}
return acc
}
return (
<g
clipPath={'url(#clip)'}
>
{selected.map(event => renderMarker(event))}
{selected.reduce(renderMarker, [])}
</g>
)
}

View File

@@ -210,7 +210,6 @@ $timeline-height: 170px;
.timeline-marker {
fill: none;
transition: transform 0.2s ease;
}
.coevent {

View File

@@ -27,6 +27,7 @@ const initial = {
* or by the characteristics of the client, browser, etc.
*/
app: {
debug: true,
errors: {
source: false
},