kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
229 lines (201 loc) • 7.69 kB
JavaScript
// Copyright (c) 2021 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 from 'react';
import styled from 'styled-components';
import {rgb} from 'd3-color';
import ColorLegend from 'components/common/color-legend';
import {CHANNEL_SCALES, DIMENSIONS} from 'constants/default-settings';
import {FormattedMessage} from 'localization';
export const StyledMapControlLegend = styled.div`
padding: 10px ${props => props.theme.mapControl.padding}px 10px
${props => props.theme.mapControl.padding}px;
font-size: 11px;
border-bottom-color: ${props => props.theme.panelBorderColor};
border-bottom-style: solid;
border-bottom-width: ${props => (props.last ? 0 : '1px')};
width: ${props => props.width}px;
.legend--layer_name {
font-size: 12px;
padding-right: ${props => props.theme.mapControl.padding}px;
color: ${props => props.theme.textColor};
font-weight: 500;
}
.legend--layer_type {
color: ${props => props.theme.subtextColor};
font-weight: 500;
font-size: 11px;
padding-right: ${props => props.theme.mapControl.padding}px;
}
.legend--layer__title {
padding-right: ${props => props.theme.mapControl.padding}px;
}
.legend--layer_by {
color: ${props => props.theme.subtextColor};
}
.legend--layer_color_field {
color: ${props => props.theme.textColorHl};
font-weight: 500;
}
.legend--layer_color-legend {
margin-top: 6px;
}
`;
export const VisualChannelMetric = ({name}) => {
return (
<div className="legend--layer__title">
<span className="legend--layer_color_field">
<FormattedMessage id={name} />
</span>
</div>
);
};
/** @type {typeof import('./map-legend').LayerSizeLegend} */
export const LayerSizeLegend = ({label, name}) => (
<div className="legend--layer_size-schema">
<p>
<span className="legend--layer_by">
<FormattedMessage id={label} />
</span>
<span className="legend--layer_by"> by </span>
</p>
<VisualChannelMetric name={name} />
</div>
);
const SINGLE_COLOR_DOMAIN = [''];
/** @type {typeof import('./map-legend').SingleColorLegend} */
export const SingleColorLegend = React.memo(({width, color}) => (
<ColorLegend
scaleType="ordinal"
displayLabel={false}
domain={SINGLE_COLOR_DOMAIN}
fieldType={null}
range={{colors: [rgb(...color).toString()]}}
width={width}
/>
));
SingleColorLegend.displayName = 'SingleColorLegend';
/** @type {typeof import('./map-legend').LayerColorLegend} */
export const LayerColorLegend = React.memo(({description, config, width, colorChannel}) => {
const enableColorBy = description.measure;
const {scale, field, domain, range, property} = colorChannel;
const [colorScale, colorField, colorDomain] = [scale, field, domain].map(k => config[k]);
const colorRange = config.visConfig[range];
return (
<div>
<div className="legend--layer_color-schema">
<div>
{enableColorBy ? <VisualChannelMetric name={enableColorBy} /> : null}
<div className="legend--layer_color-legend">
{enableColorBy ? (
<ColorLegend
scaleType={colorScale}
displayLabel
domain={colorDomain}
fieldType={(colorField && colorField.type) || 'real'}
range={colorRange}
width={width}
/>
) : (
<SingleColorLegend
color={config.visConfig[property] || config[property] || config.color}
width={width}
/>
)}
</div>
</div>
</div>
</div>
);
});
LayerColorLegend.displayName = 'LayerColorLegend';
const isColorChannel = visualChannel =>
[CHANNEL_SCALES.color, CHANNEL_SCALES.colorAggr].includes(visualChannel.channelScaleType);
export function LayerLegendHeaderFactory() {
/** @type {typeof import('./map-legend').LayerLegendHeader }> */
const LayerLegendHeader = ({options, layer}) => {
return options?.showLayerName !== false ? (
<div className="legend--layer_name">{layer.config.label}</div>
) : null;
};
return LayerLegendHeader;
}
export function LayerLegendContentFactory() {
/** @type {typeof import('./map-legend').LayerLegendContent }> */
const LayerLegendContent = ({layer, containerW}) => {
const colorChannels = Object.values(layer.visualChannels).filter(isColorChannel);
const nonColorChannels = Object.values(layer.visualChannels).filter(vc => !isColorChannel(vc));
return (
<>
{colorChannels.map(colorChannel =>
!colorChannel.condition || colorChannel.condition(layer.config) ? (
<LayerColorLegend
key={colorChannel.key}
description={layer.getVisualChannelDescription(colorChannel.key)}
config={layer.config}
width={containerW - 2 * DIMENSIONS.mapControl.padding}
colorChannel={colorChannel}
/>
) : null
)}
{nonColorChannels.map(visualChannel => {
const matchCondition = !visualChannel.condition || visualChannel.condition(layer.config);
const enabled = layer.config[visualChannel.field] || visualChannel.defaultMeasure;
const description = layer.getVisualChannelDescription(visualChannel.key);
return matchCondition && enabled ? (
<LayerSizeLegend
key={visualChannel.key}
label={description.label}
name={description.measure}
/>
) : null;
})}
</>
);
};
return LayerLegendContent;
}
MapLegendFactory.deps = [LayerLegendHeaderFactory, LayerLegendContentFactory];
function MapLegendFactory(LayerLegendHeader, LayerLegendContent) {
/** @type {typeof import('./map-legend').MapLegend }> */
const MapLegend = ({layers = [], width, options}) => (
<div className="map-legend">
{layers.map((layer, index) => {
if (!layer.isValidToSave() || layer.config.hidden) {
return null;
}
const containerW = width || DIMENSIONS.mapControl.width;
return (
<StyledMapControlLegend
className="legend--layer"
last={index === layers.length - 1}
key={index}
width={containerW}
>
<LayerLegendHeader options={options} layer={layer} />
<LayerLegendContent containerW={containerW} layer={layer} />
</StyledMapControlLegend>
);
})}
</div>
);
MapLegend.displayName = 'MapLegend';
return MapLegend;
}
export default MapLegendFactory;