kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
172 lines (146 loc) • 5.02 kB
JavaScript
// Copyright (c) 2018 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, {Component} from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import {createSelector} from 'reselect';
import {format} from 'd3-format';
import moment from 'moment';
import {
SCALE_TYPES,
SCALE_FUNC,
ALL_FIELD_TYPES
} from 'constants/default-settings';
import {getTimeWidgetHintFormatter} from 'utils/filter-utils';
const ROW_H = 10;
const GAP = 4;
const RECT_W = 20;
const StyledLegend = styled.div`
${props => props.theme.sidePanelScrollBar};
max-height: 150px;
overflow-y: auto;
svg {
text {
font-size: 9px;
fill: ${props => props.theme.textColor};
}
}
`;
const defaultFormat = d => d;
const getTimeLabelFormat = domain => {
const formatter = getTimeWidgetHintFormatter(domain);
return val => moment.utc(val).format(formatter);
};
const getNumericLabelFormat = domain => {
const diff = domain[1] - domain[0];
if (diff < 10) {
return format('.2f');
}
return format('.1f');
};
const getQuantLabelFormat = (domain, fieldType) => {
// quant scale can only be assigned to linear Fields: real, timestamp, integer
return fieldType === ALL_FIELD_TYPES.timestamp
? getTimeLabelFormat(domain)
: !fieldType ? defaultFormat : getNumericLabelFormat(domain);
};
const getOrdinalLegends = scale => {
const domain = scale.domain();
return {
data: domain.map(scale),
labels: domain
};
};
const getQuantLegends = (scale, labelFormat) => {
const labels = scale.range().map(d => {
const invert = scale.invertExtent(d);
return `${labelFormat(invert[0])} to ${labelFormat(invert[1])}`;
});
return {
data: scale.range(),
labels
};
};
export default class ColorLegend extends Component {
static propTypes = {
width: PropTypes.number.isRequired,
scaleType: PropTypes.string,
domain: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
fieldType: PropTypes.string,
range: PropTypes.arrayOf(PropTypes.string),
labelFormat: PropTypes.func
};
domainSelector = props => props.domain;
rangeSelector = props => props.range;
labelFormatSelector = props => props.labelFormat;
scaleTypeSelector = props => props.scaleType;
fieldTypeSelector = props => props.fieldType;
legendsSelector = createSelector(
this.domainSelector,
this.rangeSelector,
this.scaleTypeSelector,
this.labelFormatSelector,
this.fieldTypeSelector,
(domain, range, scaleType, labelFormat, fieldType) => {
const scaleFunction = SCALE_FUNC[scaleType];
// color scale can only be quantize, quantile or ordinal
const scale = scaleFunction()
.domain(domain)
.range(range);
if (scaleType === SCALE_TYPES.ordinal) {
return getOrdinalLegends(scale);
}
const formatLabel =
labelFormat || getQuantLabelFormat(scale.domain(), fieldType);
return getQuantLegends(scale, formatLabel);
}
);
render() {
const {width, scaleType, domain, range, displayLabel = true} = this.props;
if (!domain || !range || !scaleType) {
return null;
}
const legends = this.legendsSelector(this.props);
const height = legends.data.length * (ROW_H + GAP);
return (
<StyledLegend>
<svg width={width - 24} height={height}>
{legends.data.map((color, idx) => (
<LegendRow
key={idx}
label={legends.labels[idx]}
displayLabel={displayLabel}
color={color}
idx={idx}
/>
))}
</svg>
</StyledLegend>
);
}
}
const LegendRow = ({label = '', displayLabel, color, idx}) => (
<g transform={`translate(0, ${idx * (ROW_H + GAP)})`}>
<rect width={RECT_W} height={ROW_H} style={{fill: color}} />
<text x={RECT_W + 8} y={ROW_H - 1}>
{displayLabel ? label.toString() : ''}
</text>
</g>
);