• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keplergl / kepler.gl / 12509145710

26 Dec 2024 10:43PM UTC coverage: 67.491%. Remained the same
12509145710

push

github

web-flow
[chore] ts refactoring (#2861)

- move several base layer types to layer.d.ts
- other ts changes

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

5841 of 10041 branches covered (58.17%)

Branch coverage included in aggregate %.

8 of 12 new or added lines in 9 files covered. (66.67%)

31 existing lines in 6 files now uncovered.

11978 of 16361 relevant lines covered (73.21%)

87.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.0
/src/components/src/common/histogram-plot.tsx
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import React, {ReactElement, useMemo} from 'react';
5
import {scaleLinear} from 'd3-scale';
6
import {hcl} from 'd3-color';
7
import {min, max} from 'd3-array';
8
import styled from 'styled-components';
9

10
import {Bins} from '@kepler.gl/types';
11

12
// clipping mask constants
13
export const HISTOGRAM_MASK_MODE = {
6✔
14
  NoMask: 0,
15
  Mask: 1,
16
  MaskWithOverlay: 2
17
};
18
const HISTOGRAM_MASK_BGCOLOR = '#FFFFFF';
6✔
19
const HISTOGRAM_MASK_FGCOLOR = '#000000';
6✔
20

21
const histogramStyle = {
6✔
22
  highlightW: 0.7,
23
  unHighlightedW: 0.4
24
};
25

26
const HistogramWrapper = styled.svg`
6✔
27
  overflow: visible;
28
`;
29

30
const HistogramMaskRect = styled.rect`
6✔
31
  fill: ${props => props.theme.panelBackground};
11✔
32
`;
33

34
const HistogramBreakLine = styled.g`
6✔
35
  stroke: ${props => props.theme.histogramBreakLineColor};
11✔
36
  stroke-width: 1px;
37
  transform: translate(0, 0);
38
`;
39

40
type BarType = {
41
  inRange: boolean;
42
  isOverlay: boolean;
43
};
44
const BarUnmemoized = styled.rect<BarType>(
6✔
45
  ({theme, inRange, isOverlay, color}) => `
182✔
46
  ${
47
    isOverlay
182✔
48
      ? `fill: ${color ?? theme.histogramOverlayColor};`
50✔
49
      : inRange
157✔
50
      ? `fill: ${color ?? theme.histogramFillInRange};`
164✔
51
      : `fill: ${color ? hcl(color).darker() : theme.histogramFillOutRange};`
17✔
52
  }
53
`
54
);
55
const Bar = React.memo(BarUnmemoized);
6✔
56
Bar.displayName = 'Bar';
6✔
57

58
const isBarInRange = (
6✔
59
  bar: {x0: number; x1: number},
60
  index: number,
61
  list: any[],
62
  filterDomain: any[],
63
  filterValue: any[]
64
) => {
65
  // first
66
  // if x0 <= domain[0] and current value[0] wasn't changed from the original domain
67
  const x0Condition =
68
    index === 0 ? bar.x0 <= filterDomain[0] && filterDomain[0] === filterValue[0] : false;
375✔
69
  // Last
70
  // if x1 >= domain[1] and current value[1] wasn't changed from the original domain
71
  const x1Condition =
72
    index === list.length - 1
375✔
73
      ? bar.x1 >= filterDomain[1] && filterDomain[1] === filterValue[1]
53✔
74
      : false;
75
  return (x0Condition || bar.x0 >= filterValue[0]) && (x1Condition || bar.x1 <= filterValue[1]);
375✔
76
};
77

78
export type HistogramMaskModeType = {
79
  NoMask: number;
80
  Mask: number;
81
  MaskWithOverlay: number;
82
};
83

84
interface HistogramPlotProps {
85
  width: number;
86
  height: number;
87
  margin: {top: number; bottom: number; left: number; right: number};
88
  isRanged?: boolean;
89
  value: number[];
90
  isMasked?: number;
91
  brushComponent?: ReactElement;
92
  histogramsByGroup: Bins;
93
  colorsByGroup?: null | {[dataId: string]: string};
94
  countProp?: string;
95
  range?: number[];
96
  breakLines?: number[];
97
}
98

99
function HistogramPlotFactory() {
100
  const HistogramPlot = ({
14✔
101
    width,
102
    height,
103
    histogramsByGroup,
104
    colorsByGroup,
105
    isMasked = HISTOGRAM_MASK_MODE.NoMask,
15✔
106
    countProp = 'count',
26✔
107
    margin,
108
    isRanged,
109
    range,
110
    value,
111
    brushComponent,
112
    breakLines
113
  }: HistogramPlotProps) => {
114
    const undefinedToZero = (x: number | undefined) => (x ? x : 0);
75!
115
    const groupKeys = useMemo(
26✔
116
      () =>
117
        Object.keys(histogramsByGroup)
11✔
118
          // only keep non-empty groups
119
          .filter(key => histogramsByGroup[key]?.length > 0),
16✔
120
      [histogramsByGroup]
121
    );
122

123
    const domain = useMemo(
26✔
124
      () =>
125
        range ?? [
11✔
126
          min(groupKeys, key => histogramsByGroup[key][0].x0) ?? 0,
10!
127
          max(groupKeys, key => histogramsByGroup[key][histogramsByGroup[key].length - 1].x1) ?? 0
10!
128
        ],
129
      [range, histogramsByGroup, groupKeys]
130
    );
131

132
    const barWidth = useMemo(() => {
26✔
133
      if (groupKeys.length === 0) return 0;
17!
134
      // find histogramsByGroup with max number of bins
135
      const maxGroup = groupKeys.reduce((accu, key, idx) => {
17✔
136
        if (histogramsByGroup[key].length > accu.length) {
22!
137
          return histogramsByGroup[key];
×
138
        }
139
        return accu;
22✔
140
      }, histogramsByGroup[groupKeys[0]]);
141

142
      // find the bin for measuring step
143
      const stdBinIdx = maxGroup.length > 1 ? 1 : 0;
17!
144
      const xStep = maxGroup[stdBinIdx].x1 - maxGroup[stdBinIdx].x0;
17✔
145
      const maxBins = (domain[1] - domain[0]) / xStep;
17✔
146
      if (!maxBins) return 0;
17!
147
      return width / maxBins / (isMasked ? 1 : groupKeys.length);
17✔
148
    }, [histogramsByGroup, domain, groupKeys, width, isMasked]);
149

150
    const x = useMemo(
26✔
151
      () => scaleLinear().domain(domain).range([barWidth, width]),
17✔
152
      [domain, width, barWidth]
153
    );
154

155
    const y = useMemo(
26✔
156
      () =>
157
        scaleLinear()
11✔
158
          .domain([
159
            0,
160
            Math.max(
161
              Number(max(groupKeys, key => max(histogramsByGroup[key], d => d[countProp]))),
280✔
162
              1
163
            )
164
          ])
165
          .range([0, height]),
166
      [histogramsByGroup, groupKeys, height, countProp]
167
    );
168

169
    if (groupKeys.length === 0) {
26!
UNCOV
170
      return null;
×
171
    }
172

173
    const maskedHistogram = () => {
26✔
174
      return (
11✔
175
        <HistogramWrapper
176
          width={width}
177
          height={height}
178
          style={{margin: `${margin.top}px ${margin.right}px ${margin.bottom}px ${margin.left}px`}}
179
        >
180
          <defs>
181
            <mask id="histogram-mask">
182
              <rect
183
                x="0"
184
                y="0"
185
                width={width}
186
                height={height + margin.bottom}
187
                fill={HISTOGRAM_MASK_BGCOLOR}
188
              />
189
              <g key="filtered-bins" className="histogram-bars">
190
                {histogramsByGroup.filteredBins.map((bar, idx, list) => {
191
                  const inRange = isBarInRange(bar, idx, list, domain, value);
275✔
192
                  const wRatio = inRange
275✔
193
                    ? histogramStyle.highlightW
194
                    : histogramStyle.unHighlightedW;
195
                  return (
275✔
196
                    <Bar
197
                      inRange={inRange}
198
                      color={HISTOGRAM_MASK_FGCOLOR}
199
                      key={`mask-${idx}`}
200
                      height={y(bar[countProp])}
201
                      width={barWidth * wRatio}
202
                      x={x(bar.x0) + (barWidth * (1 - wRatio)) / 2}
203
                      y={height - y(bar[countProp])}
204
                    />
205
                  );
206
                })}
207
              </g>
208
            </mask>
209
          </defs>
210
          <g transform="translate(0,0)">
211
            <HistogramMaskRect
212
              x="0"
213
              y="0"
214
              width="100%"
215
              height={height + margin.bottom}
216
              mask="url(#histogram-mask)"
217
            />
218
          </g>
219
          {isMasked === HISTOGRAM_MASK_MODE.MaskWithOverlay && (
12✔
220
            <g key="bins" transform="translate(0,0)" className="overlay-histogram-bars">
221
              {histogramsByGroup.bins.map((bar, idx, list) => {
222
                const filterBar = histogramsByGroup.filteredBins[idx];
25✔
223
                const maskHeight = filterBar
25!
224
                  ? y(bar[countProp]) - y(filterBar[countProp])
225
                  : y(bar[countProp]);
226
                const inRange = isBarInRange(bar, idx, list, domain, value);
25✔
227
                const wRatio = inRange ? histogramStyle.highlightW : histogramStyle.unHighlightedW;
25✔
228
                return (
25✔
229
                  <Bar
230
                    inRange={inRange}
231
                    isOverlay={true}
232
                    key={`bar-${idx}`}
233
                    height={maskHeight}
234
                    width={barWidth * wRatio}
235
                    x={x(bar.x0) + (barWidth * (1 - wRatio)) / 2}
236
                    y={height - y(bar[countProp])}
237
                  />
238
                );
239
              })}
240
            </g>
241
          )}
242
          <HistogramBreakLine>
243
            {(breakLines ?? []).map((pos, idx) => {
11!
244
              return (
29✔
245
                <path key={`mask-line-${idx}`} strokeDasharray="4,4" d={`M${pos}, 0 l0 100`} />
246
              );
247
            })}
248
          </HistogramBreakLine>
249
          <g transform={`translate(${isRanged ? 0 : barWidth / 2}, 0)`}>{brushComponent}</g>
11!
250
        </HistogramWrapper>
251
      );
252
    };
253

254
    return isMasked ? (
26✔
255
      maskedHistogram()
256
    ) : (
257
      <HistogramWrapper
258
        width={width}
259
        height={height}
260
        style={{margin: `${margin.top}px ${margin.right}px ${margin.bottom}px ${margin.left}px`}}
261
      >
262
        <g>
263
          {groupKeys.map((key, i) => (
264
            <g key={key} className="histogram-bars">
15✔
265
              {histogramsByGroup[key].map((bar, idx, list) => {
266
                const inRange = isBarInRange(bar, idx, list, domain, value);
75✔
267

268
                const wRatio = inRange ? histogramStyle.highlightW : histogramStyle.unHighlightedW;
75✔
269
                const startX =
270
                  x(undefinedToZero(bar.x0)) + barWidth * i + (barWidth * (1 - wRatio)) / 2;
75✔
271
                if (startX > 0 && startX + barWidth * histogramStyle.unHighlightedW <= width) {
75✔
272
                  return (
36✔
273
                    <Bar
274
                      inRange={inRange}
275
                      color={colorsByGroup?.[key]}
276
                      key={`bar-${idx}`}
277
                      height={y(bar[countProp])}
278
                      width={barWidth * wRatio}
279
                      x={startX}
280
                      rx={1}
281
                      ry={1}
282
                      y={height - y(bar[countProp])}
283
                    />
284
                  );
285
                }
286
                return null;
39✔
287
              })}
288
            </g>
289
          ))}
290
        </g>
291
        <g transform={`translate(${isRanged ? 0 : barWidth / 2}, 0)`}>{brushComponent}</g>
15!
292
      </HistogramWrapper>
293
    );
294
  };
295

296
  const HistogramPlotWithGroups = props => {
14✔
297
    const groups = useMemo(
28✔
298
      () => (props.histogramsByGroup ? Object.keys(props.histogramsByGroup) : null),
13✔
299
      [props.histogramsByGroup]
300
    );
301

302
    if (!groups?.length) {
28✔
303
      return null;
2✔
304
    }
305

306
    return <HistogramPlot {...props} />;
26✔
307
  };
308

309
  return HistogramPlotWithGroups;
14✔
310
}
311
export default HistogramPlotFactory;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc