panelui-native 0.38.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/lib/module/components/card/index.js +172 -3
- package/lib/module/components/card/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +13 -4
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +113 -6
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +72 -12
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/pagination/index.js +529 -0
- package/lib/module/components/pagination/index.js.map +1 -0
- package/lib/module/components/panelside/index.js +68 -18
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/ring-chart/index.js +183 -45
- package/lib/module/components/ring-chart/index.js.map +1 -1
- package/lib/module/components/steps/index.js +8 -6
- package/lib/module/components/steps/index.js.map +1 -1
- package/lib/module/components/table/index.js +100 -26
- package/lib/module/components/table/index.js.map +1 -1
- package/lib/module/components/text-animation/index.js +857 -0
- package/lib/module/components/text-animation/index.js.map +1 -0
- package/lib/module/components/timeline/index.js +8 -7
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/icons/index.js +25 -7
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/card/index.d.ts +20 -0
- package/lib/typescript/src/components/card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts +13 -1
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +51 -3
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +37 -2
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pagination/index.d.ts +285 -0
- package/lib/typescript/src/components/pagination/index.d.ts.map +1 -0
- package/lib/typescript/src/components/panelside/index.d.ts +50 -4
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ring-chart/index.d.ts +75 -8
- package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/steps/index.d.ts +8 -6
- package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
- package/lib/typescript/src/components/table/index.d.ts +59 -17
- package/lib/typescript/src/components/table/index.d.ts.map +1 -1
- package/lib/typescript/src/components/text-animation/index.d.ts +268 -0
- package/lib/typescript/src/components/text-animation/index.d.ts.map +1 -0
- package/lib/typescript/src/components/timeline/index.d.ts +8 -7
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +6 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/card/index.tsx +227 -2
- package/src/components/drawer/index.tsx +31 -9
- package/src/components/heatmap-chart/index.tsx +147 -5
- package/src/components/map/index.tsx +158 -14
- package/src/components/pagination/index.tsx +603 -0
- package/src/components/panelside/index.tsx +134 -19
- package/src/components/ring-chart/index.tsx +269 -47
- package/src/components/steps/index.tsx +8 -6
- package/src/components/table/index.tsx +146 -30
- package/src/components/text-animation/index.tsx +1093 -0
- package/src/components/timeline/index.tsx +8 -7
- package/src/icons/index.tsx +23 -4
- package/src/index.ts +28 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* ```tsx
|
|
13
13
|
* <HeatmapChart data={weeks}>
|
|
14
|
+
* <HeatmapChart.Header title="Contributions" value="1,204" legend />
|
|
14
15
|
* <HeatmapChart.YAxis />
|
|
15
16
|
* <HeatmapChart.XAxis />
|
|
16
17
|
* <HeatmapChart.Cells />
|
|
@@ -132,7 +133,7 @@ const MONTHS = [
|
|
|
132
133
|
* composition stays a flat list of children instead of four nested slots the
|
|
133
134
|
* caller has to remember the order of.
|
|
134
135
|
*/
|
|
135
|
-
type Slot = 'cells' | 'x-axis' | 'y-axis' | 'rules' | 'tooltip' | 'legend';
|
|
136
|
+
type Slot = 'cells' | 'x-axis' | 'y-axis' | 'rules' | 'tooltip' | 'legend' | 'header';
|
|
136
137
|
|
|
137
138
|
export type HeatmapLayout = 'fluid' | 'fill';
|
|
138
139
|
|
|
@@ -566,6 +567,10 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
566
567
|
onLayout={onLayout}
|
|
567
568
|
className={cn('w-full', className)}
|
|
568
569
|
>
|
|
570
|
+
{/* Above the axis gutter as well as the grid: the header is about the
|
|
571
|
+
whole chart, so it starts at the chart's edge, not the grid's. */}
|
|
572
|
+
{parts.header}
|
|
573
|
+
|
|
569
574
|
{parts.x ? (
|
|
570
575
|
<View style={{ paddingLeft: axisWidth }}>{parts.x}</View>
|
|
571
576
|
) : null}
|
|
@@ -605,6 +610,7 @@ interface Parts {
|
|
|
605
610
|
y: React.ReactElement<{ width?: number }> | null;
|
|
606
611
|
tooltip: ReactNode[];
|
|
607
612
|
legend: ReactNode[];
|
|
613
|
+
header: ReactNode[];
|
|
608
614
|
columns: number;
|
|
609
615
|
}
|
|
610
616
|
|
|
@@ -617,6 +623,7 @@ function splitParts(children: ReactNode): Parts {
|
|
|
617
623
|
y: null,
|
|
618
624
|
tooltip: [],
|
|
619
625
|
legend: [],
|
|
626
|
+
header: [],
|
|
620
627
|
columns: 0,
|
|
621
628
|
};
|
|
622
629
|
|
|
@@ -630,6 +637,7 @@ function splitParts(children: ReactNode): Parts {
|
|
|
630
637
|
else if (slot === 'rules') parts.rules.push(keyed);
|
|
631
638
|
else if (slot === 'tooltip') parts.tooltip.push(keyed);
|
|
632
639
|
else if (slot === 'legend') parts.legend.push(keyed);
|
|
640
|
+
else if (slot === 'header') parts.header.push(keyed);
|
|
633
641
|
else parts.cells.push(keyed);
|
|
634
642
|
});
|
|
635
643
|
|
|
@@ -753,6 +761,12 @@ export interface HeatmapXAxisProps {
|
|
|
753
761
|
* derived. Return an empty string to leave the column unlabelled.
|
|
754
762
|
*/
|
|
755
763
|
formatLabel?: (date: Date, column: number) => string;
|
|
764
|
+
/**
|
|
765
|
+
* Column labels, left to right. Overrides the month names — for a grid whose
|
|
766
|
+
* columns are not weeks, where there is no month to change and so nothing to
|
|
767
|
+
* emit a label on.
|
|
768
|
+
*/
|
|
769
|
+
labels?: string[];
|
|
756
770
|
}
|
|
757
771
|
|
|
758
772
|
/**
|
|
@@ -760,13 +774,21 @@ export interface HeatmapXAxisProps {
|
|
|
760
774
|
*
|
|
761
775
|
* A label is emitted where the month changes rather than at a fixed interval,
|
|
762
776
|
* because months are not the same length — spacing them evenly puts "Mar" over
|
|
763
|
-
* a week in February.
|
|
777
|
+
* a week in February. A grid whose columns are not weeks has no such signal, so
|
|
778
|
+
* it passes `labels` and gets one over every column.
|
|
764
779
|
*/
|
|
765
|
-
function HeatmapXAxis({ className, formatLabel }: HeatmapXAxisProps) {
|
|
780
|
+
function HeatmapXAxis({ className, formatLabel, labels: given }: HeatmapXAxisProps) {
|
|
766
781
|
const { data, grid } = useHeatmap('HeatmapChart.XAxis');
|
|
767
782
|
const step = grid.size + grid.gap;
|
|
768
783
|
|
|
769
784
|
const labels = useMemo(() => {
|
|
785
|
+
if (given) {
|
|
786
|
+
return given
|
|
787
|
+
.slice(0, data.length)
|
|
788
|
+
.map((label, column) => ({ column, label }))
|
|
789
|
+
.filter((entry) => entry.label);
|
|
790
|
+
}
|
|
791
|
+
|
|
770
792
|
const out: { column: number; label: string }[] = [];
|
|
771
793
|
let lastMonth = -1;
|
|
772
794
|
|
|
@@ -784,7 +806,7 @@ function HeatmapXAxis({ className, formatLabel }: HeatmapXAxisProps) {
|
|
|
784
806
|
// collides with the next one. Drop it when it has no room.
|
|
785
807
|
if (out.length > 1 && out[1]!.column - out[0]!.column < 3) out.shift();
|
|
786
808
|
return out;
|
|
787
|
-
}, [data, formatLabel]);
|
|
809
|
+
}, [data, formatLabel, given]);
|
|
788
810
|
|
|
789
811
|
return (
|
|
790
812
|
<View className={cn('h-4', className)} style={{ width: grid.width }}>
|
|
@@ -1002,9 +1024,16 @@ function HeatmapTooltip({
|
|
|
1002
1024
|
}
|
|
1003
1025
|
HeatmapTooltip.slot = 'tooltip' as Slot;
|
|
1004
1026
|
|
|
1027
|
+
/*
|
|
1028
|
+
* A cell with no date is not a cell with no data — it is a cell in a grid that
|
|
1029
|
+
* is not a calendar, and it has a count like any other. Saying "No data" over
|
|
1030
|
+
* it reports a hole in the data that is not there. Without a date there is
|
|
1031
|
+
* simply nothing to say after the number, so the number is the whole label,
|
|
1032
|
+
* and a grid that wants a noun for it passes `formatLabel`.
|
|
1033
|
+
*/
|
|
1005
1034
|
function defaultTooltipLabel(cell: HeatmapCell) {
|
|
1035
|
+
if (!cell.date) return `${cell.count}`;
|
|
1006
1036
|
const count = `${cell.count} ${cell.count === 1 ? 'contribution' : 'contributions'}`;
|
|
1007
|
-
if (!cell.date) return 'No data';
|
|
1008
1037
|
const date = `${MONTHS[cell.date.getMonth()]} ${cell.date.getDate()}`;
|
|
1009
1038
|
return `${count} on ${date}`;
|
|
1010
1039
|
}
|
|
@@ -1061,7 +1090,120 @@ function HeatmapLegend({
|
|
|
1061
1090
|
}
|
|
1062
1091
|
HeatmapLegend.slot = 'legend' as Slot;
|
|
1063
1092
|
|
|
1093
|
+
/* -------------------------------------------------------------------------- */
|
|
1094
|
+
/* Header */
|
|
1095
|
+
/* -------------------------------------------------------------------------- */
|
|
1096
|
+
|
|
1097
|
+
export interface HeatmapHeaderProps extends ViewProps {
|
|
1098
|
+
className?: string;
|
|
1099
|
+
/** Small line above the value — what the grid is of. */
|
|
1100
|
+
title?: string;
|
|
1101
|
+
/** The readout. The largest thing on the card, and the first thing read. */
|
|
1102
|
+
value?: string;
|
|
1103
|
+
/** One muted line under the value — a period, a total, the held cell. */
|
|
1104
|
+
caption?: string;
|
|
1105
|
+
/**
|
|
1106
|
+
* Draw the ramp along the trailing edge, `Less ▢▢▢▢▢ More`. The key for a
|
|
1107
|
+
* grid that scrolls sideways, where `HeatmapChart.Legend` under the cells
|
|
1108
|
+
* would scroll away with them.
|
|
1109
|
+
*/
|
|
1110
|
+
legend?: boolean;
|
|
1111
|
+
/** Text at the low end of the ramp, when `legend` is set. */
|
|
1112
|
+
lessLabel?: string;
|
|
1113
|
+
/** Text at the high end. */
|
|
1114
|
+
moreLabel?: string;
|
|
1115
|
+
/** Trailing slot — a control, a badge, a range picker. Wins over `legend`. */
|
|
1116
|
+
children?: ReactNode;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* The strip above the grid: what the chart is of, what it currently reads, and
|
|
1121
|
+
* what the shading means.
|
|
1122
|
+
*
|
|
1123
|
+
* It belongs to the chart rather than to the card around it because it is about
|
|
1124
|
+
* the *grid* — the number changes as a finger moves across the cells, and the
|
|
1125
|
+
* ramp is the scale the chart itself derived. The card's header is a caption on
|
|
1126
|
+
* the tray the chart sits in; this is the chart introducing itself.
|
|
1127
|
+
*
|
|
1128
|
+
* The value is not derived here. Take it from `onActiveCellChange` and pass the
|
|
1129
|
+
* formatted string down, so one header can show a total when nothing is held
|
|
1130
|
+
* and a day's own count when something is.
|
|
1131
|
+
*/
|
|
1132
|
+
function HeatmapHeader({
|
|
1133
|
+
className,
|
|
1134
|
+
title,
|
|
1135
|
+
value,
|
|
1136
|
+
caption,
|
|
1137
|
+
legend = false,
|
|
1138
|
+
lessLabel = 'Less',
|
|
1139
|
+
moreLabel = 'More',
|
|
1140
|
+
children,
|
|
1141
|
+
...props
|
|
1142
|
+
}: HeatmapHeaderProps) {
|
|
1143
|
+
const { ramp, opacities, grid, cornerRadius } = useHeatmap('HeatmapChart.Header');
|
|
1144
|
+
// Small enough to sit on one line beside the text, whatever the cells are.
|
|
1145
|
+
const size = Math.max(Math.min(grid.size, 12), 8);
|
|
1146
|
+
|
|
1147
|
+
const trailing =
|
|
1148
|
+
children ??
|
|
1149
|
+
(legend ? (
|
|
1150
|
+
<View className="flex-row items-center gap-1.5">
|
|
1151
|
+
<Text size="xs" muted>
|
|
1152
|
+
{lessLabel}
|
|
1153
|
+
</Text>
|
|
1154
|
+
{ramp.map((fill, level) => (
|
|
1155
|
+
<Svg key={level} width={size} height={size}>
|
|
1156
|
+
<Rect
|
|
1157
|
+
x={0}
|
|
1158
|
+
y={0}
|
|
1159
|
+
width={size}
|
|
1160
|
+
height={size}
|
|
1161
|
+
rx={cornerRadius}
|
|
1162
|
+
ry={cornerRadius}
|
|
1163
|
+
fill={fill}
|
|
1164
|
+
fillOpacity={opacities[level]}
|
|
1165
|
+
/>
|
|
1166
|
+
</Svg>
|
|
1167
|
+
))}
|
|
1168
|
+
<Text size="xs" muted>
|
|
1169
|
+
{moreLabel}
|
|
1170
|
+
</Text>
|
|
1171
|
+
</View>
|
|
1172
|
+
) : null);
|
|
1173
|
+
|
|
1174
|
+
return (
|
|
1175
|
+
<View
|
|
1176
|
+
{...props}
|
|
1177
|
+
className={cn('flex-row items-start justify-between gap-3 pb-3', className)}
|
|
1178
|
+
>
|
|
1179
|
+
<View className="flex-1 gap-0.5">
|
|
1180
|
+
{title ? (
|
|
1181
|
+
<Text size="xs" muted>
|
|
1182
|
+
{title}
|
|
1183
|
+
</Text>
|
|
1184
|
+
) : null}
|
|
1185
|
+
{value ? (
|
|
1186
|
+
<Text size="xl" weight="bold">
|
|
1187
|
+
{value}
|
|
1188
|
+
</Text>
|
|
1189
|
+
) : null}
|
|
1190
|
+
{caption ? (
|
|
1191
|
+
<Text size="xs" muted>
|
|
1192
|
+
{caption}
|
|
1193
|
+
</Text>
|
|
1194
|
+
) : null}
|
|
1195
|
+
</View>
|
|
1196
|
+
{/* Shrinkable, unlike a view's default in React Native — held rigid, the
|
|
1197
|
+
ramp takes the width it wants and the caption wraps around it. */}
|
|
1198
|
+
{trailing ? <View className="shrink pt-1">{trailing}</View> : null}
|
|
1199
|
+
</View>
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
HeatmapHeader.displayName = 'HeatmapChart.Header';
|
|
1203
|
+
HeatmapHeader.slot = 'header' as Slot;
|
|
1204
|
+
|
|
1064
1205
|
export const HeatmapChart = Object.assign(HeatmapChartRoot, {
|
|
1206
|
+
Header: HeatmapHeader,
|
|
1065
1207
|
Cells: HeatmapCells,
|
|
1066
1208
|
Separator: HeatmapSeparator,
|
|
1067
1209
|
XAxis: HeatmapXAxis,
|
|
@@ -128,6 +128,43 @@ function useToken(name: string, fallback: string): string {
|
|
|
128
128
|
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** True for a theme token name, as opposed to a literal colour. */
|
|
132
|
+
function isToken(value: string | undefined): value is string {
|
|
133
|
+
return typeof value === 'string' && value.startsWith('--');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The same colour at a given alpha, for a ramp built from one hue.
|
|
138
|
+
*
|
|
139
|
+
* Style expressions are handed to the renderer as strings, so this produces
|
|
140
|
+
* `rgba()` rather than reaching for a colour type the layer would not accept.
|
|
141
|
+
*/
|
|
142
|
+
function withAlpha(color: string, alpha: number): string {
|
|
143
|
+
if (color.startsWith('#')) {
|
|
144
|
+
const hex = color.slice(1);
|
|
145
|
+
const full =
|
|
146
|
+
hex.length === 3
|
|
147
|
+
? hex
|
|
148
|
+
.split('')
|
|
149
|
+
.map((c) => c + c)
|
|
150
|
+
.join('')
|
|
151
|
+
: hex.slice(0, 6);
|
|
152
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
153
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
154
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
155
|
+
if (Number.isNaN(r + g + b)) return color;
|
|
156
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const channels = color.match(/rgba?\(([^)]+)\)/)?.[1];
|
|
160
|
+
if (channels) {
|
|
161
|
+
const [r, g, b] = channels.split(',').map((part) => part.trim());
|
|
162
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return color;
|
|
166
|
+
}
|
|
167
|
+
|
|
131
168
|
interface MapContextValue {
|
|
132
169
|
mapRef: React.RefObject<MapRef | null>;
|
|
133
170
|
cameraRef: React.RefObject<CameraRef | null>;
|
|
@@ -1025,6 +1062,28 @@ export interface MapHeatmapProps {
|
|
|
1025
1062
|
data: unknown;
|
|
1026
1063
|
/** Feature property to weight each point by. Unweighted when omitted. */
|
|
1027
1064
|
weight?: string;
|
|
1065
|
+
/**
|
|
1066
|
+
* Base colour of the field — a theme token by name, or a literal. The ramp
|
|
1067
|
+
* is this colour at rising opacity, so density reads as *more of the same
|
|
1068
|
+
* thing* rather than as a change of subject.
|
|
1069
|
+
*
|
|
1070
|
+
* Defaults to `--color-chart-2`, which is a saturated accent in every theme.
|
|
1071
|
+
* It is deliberately not `--color-chart-1`: that is the series colour a chart
|
|
1072
|
+
* is about, and every theme starts it on something close to the foreground —
|
|
1073
|
+
* near-black in a light theme, near-white in a dark one — which over a
|
|
1074
|
+
* basemap is a smudge rather than a measurement.
|
|
1075
|
+
*/
|
|
1076
|
+
color?: string;
|
|
1077
|
+
/**
|
|
1078
|
+
* Replace the derived ramp outright, coolest first. For the conventional
|
|
1079
|
+
* heat ramp, where the hue carries the reading as well as the opacity —
|
|
1080
|
+
* worth it when the field sits over varied terrain and one hue at five
|
|
1081
|
+
* opacities stops being separable from what is underneath it.
|
|
1082
|
+
*
|
|
1083
|
+
* The first stop is drawn at the lowest density, the last at the highest.
|
|
1084
|
+
* Density zero stays fully transparent either way.
|
|
1085
|
+
*/
|
|
1086
|
+
colors?: string[];
|
|
1028
1087
|
/**
|
|
1029
1088
|
* Spread of a single point, in points, at street zoom. Larger blurs more.
|
|
1030
1089
|
* The drawn radius shrinks as the map zooms out, so a point keeps covering
|
|
@@ -1037,6 +1096,12 @@ export interface MapHeatmapProps {
|
|
|
1037
1096
|
opacity?: number;
|
|
1038
1097
|
/** Above this zoom the layer fades out — see the note on the component. */
|
|
1039
1098
|
maxZoom?: number;
|
|
1099
|
+
/**
|
|
1100
|
+
* Draw the points themselves as the field fades out, coloured from the same
|
|
1101
|
+
* ramp by weight. Without them, zooming past `maxZoom` leaves an empty map:
|
|
1102
|
+
* the layer gets out of the way, and nothing takes its place.
|
|
1103
|
+
*/
|
|
1104
|
+
points?: boolean;
|
|
1040
1105
|
id?: string;
|
|
1041
1106
|
}
|
|
1042
1107
|
|
|
@@ -1050,30 +1115,63 @@ export interface MapHeatmapProps {
|
|
|
1050
1115
|
*
|
|
1051
1116
|
* It fades out past `maxZoom` on purpose. Zoomed far enough in, every point is
|
|
1052
1117
|
* its own island and the blur says less than the points would — so the layer
|
|
1053
|
-
* gets out of the way rather than smearing five records across a street.
|
|
1118
|
+
* gets out of the way rather than smearing five records across a street. Set
|
|
1119
|
+
* `points` and the records themselves fade in as it goes, which is the whole
|
|
1120
|
+
* handover: a field while the question is where, marks once it is which.
|
|
1054
1121
|
*
|
|
1055
1122
|
* The radius is tied to zoom for the mirror-image reason. Left as a fixed
|
|
1056
1123
|
* number of screen points it would mean a different distance at every zoom:
|
|
1057
1124
|
* a blur that reads as a city at street level covers half a continent once
|
|
1058
1125
|
* the map is pulled out, and a field measured over land ends up sitting in
|
|
1059
1126
|
* the sea. Scaling it with the projection keeps the claim the same one.
|
|
1127
|
+
*
|
|
1128
|
+
* Intensity is tied to zoom too, and in the other direction. The same points
|
|
1129
|
+
* are packed into fewer pixels the further out the map goes, so a constant
|
|
1130
|
+
* intensity saturates the whole field at world zoom and shows nothing but its
|
|
1131
|
+
* own ceiling.
|
|
1060
1132
|
*/
|
|
1061
1133
|
function MapHeatmap({
|
|
1062
1134
|
data,
|
|
1063
1135
|
weight,
|
|
1136
|
+
color,
|
|
1137
|
+
colors,
|
|
1064
1138
|
radius = 24,
|
|
1065
1139
|
intensity = 1,
|
|
1066
1140
|
opacity = 0.85,
|
|
1067
1141
|
maxZoom = 15,
|
|
1142
|
+
points = false,
|
|
1068
1143
|
id = 'heatmap',
|
|
1069
1144
|
}: MapHeatmapProps) {
|
|
1070
|
-
const
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1145
|
+
const base = useToken(isToken(color) ? color : '--color-chart-2', '#3b82f6');
|
|
1146
|
+
const resolved = (isToken(color) ? base : color) ?? base;
|
|
1147
|
+
|
|
1148
|
+
/*
|
|
1149
|
+
* One colour at rising opacity rather than five colours, which is how every
|
|
1150
|
+
* other ramp in the library is built: a sequential scale is one quantity
|
|
1151
|
+
* getting larger, and five hues say five categories. `colors` is there for
|
|
1152
|
+
* the case where that is not enough.
|
|
1153
|
+
*/
|
|
1154
|
+
const ramp = colors?.length
|
|
1155
|
+
? colors
|
|
1156
|
+
: [
|
|
1157
|
+
withAlpha(resolved, 0.25),
|
|
1158
|
+
withAlpha(resolved, 0.45),
|
|
1159
|
+
withAlpha(resolved, 0.7),
|
|
1160
|
+
withAlpha(resolved, 0.9),
|
|
1161
|
+
resolved,
|
|
1162
|
+
];
|
|
1073
1163
|
|
|
1074
1164
|
if (!MapLibre) return null;
|
|
1075
1165
|
const { GeoJSONSource, Layer } = MapLibre;
|
|
1076
1166
|
|
|
1167
|
+
// Density zero is transparent rather than the coolest colour, so the map
|
|
1168
|
+
// shows through everywhere nothing was measured instead of the whole
|
|
1169
|
+
// viewport being tinted by the absence of data.
|
|
1170
|
+
const stops = ramp.flatMap((entry, index) => [
|
|
1171
|
+
0.15 + (0.85 * index) / Math.max(1, ramp.length - 1),
|
|
1172
|
+
entry,
|
|
1173
|
+
]);
|
|
1174
|
+
|
|
1077
1175
|
return (
|
|
1078
1176
|
<GeoJSONSource id={`${id}-source`} data={data}>
|
|
1079
1177
|
<Layer
|
|
@@ -1084,7 +1182,15 @@ function MapHeatmap({
|
|
|
1084
1182
|
heatmapWeight: weight
|
|
1085
1183
|
? ['interpolate', ['linear'], ['get', weight], 0, 0, 1, 1]
|
|
1086
1184
|
: 1,
|
|
1087
|
-
heatmapIntensity:
|
|
1185
|
+
heatmapIntensity: [
|
|
1186
|
+
'interpolate',
|
|
1187
|
+
['linear'],
|
|
1188
|
+
['zoom'],
|
|
1189
|
+
0,
|
|
1190
|
+
intensity * 0.6,
|
|
1191
|
+
maxZoom,
|
|
1192
|
+
intensity * 1.2,
|
|
1193
|
+
],
|
|
1088
1194
|
// `radius` is the spread at street zoom; below that it is scaled
|
|
1089
1195
|
// down so it keeps covering the same ground rather than the same
|
|
1090
1196
|
// number of pixels. The stops are the projection: each zoom level
|
|
@@ -1101,21 +1207,13 @@ function MapHeatmap({
|
|
|
1101
1207
|
14,
|
|
1102
1208
|
radius,
|
|
1103
1209
|
],
|
|
1104
|
-
// Transparent at zero rather than the coolest colour, so the map
|
|
1105
|
-
// shows through everywhere nothing was measured instead of the whole
|
|
1106
|
-
// viewport being tinted by the absence of data.
|
|
1107
1210
|
heatmapColor: [
|
|
1108
1211
|
'interpolate',
|
|
1109
1212
|
['linear'],
|
|
1110
1213
|
['heatmap-density'],
|
|
1111
1214
|
0,
|
|
1112
1215
|
'rgba(0,0,0,0)',
|
|
1113
|
-
|
|
1114
|
-
cool,
|
|
1115
|
-
0.6,
|
|
1116
|
-
warm,
|
|
1117
|
-
1,
|
|
1118
|
-
hot,
|
|
1216
|
+
...stops,
|
|
1119
1217
|
],
|
|
1120
1218
|
heatmapOpacity: [
|
|
1121
1219
|
'interpolate',
|
|
@@ -1128,6 +1226,52 @@ function MapHeatmap({
|
|
|
1128
1226
|
],
|
|
1129
1227
|
}}
|
|
1130
1228
|
/>
|
|
1229
|
+
|
|
1230
|
+
{/* The handover, in the two zoom levels the field spends fading: the
|
|
1231
|
+
marks arrive as it leaves, so there is never a zoom with neither. */}
|
|
1232
|
+
{points ? (
|
|
1233
|
+
<Layer
|
|
1234
|
+
id={`${id}-points`}
|
|
1235
|
+
type="circle"
|
|
1236
|
+
minzoom={maxZoom - 2}
|
|
1237
|
+
style={{
|
|
1238
|
+
circleRadius: weight
|
|
1239
|
+
? ['interpolate', ['linear'], ['get', weight], 0, 3, 1, 9]
|
|
1240
|
+
: 5,
|
|
1241
|
+
circleColor: weight
|
|
1242
|
+
? [
|
|
1243
|
+
'interpolate',
|
|
1244
|
+
['linear'],
|
|
1245
|
+
['get', weight],
|
|
1246
|
+
0,
|
|
1247
|
+
ramp[0] as string,
|
|
1248
|
+
1,
|
|
1249
|
+
ramp[ramp.length - 1] as string,
|
|
1250
|
+
]
|
|
1251
|
+
: (ramp[ramp.length - 1] as string),
|
|
1252
|
+
circleStrokeWidth: 1,
|
|
1253
|
+
circleStrokeColor: 'rgba(255,255,255,0.75)',
|
|
1254
|
+
circleOpacity: [
|
|
1255
|
+
'interpolate',
|
|
1256
|
+
['linear'],
|
|
1257
|
+
['zoom'],
|
|
1258
|
+
maxZoom - 2,
|
|
1259
|
+
0,
|
|
1260
|
+
maxZoom,
|
|
1261
|
+
0.85,
|
|
1262
|
+
],
|
|
1263
|
+
circleStrokeOpacity: [
|
|
1264
|
+
'interpolate',
|
|
1265
|
+
['linear'],
|
|
1266
|
+
['zoom'],
|
|
1267
|
+
maxZoom - 2,
|
|
1268
|
+
0,
|
|
1269
|
+
maxZoom,
|
|
1270
|
+
0.6,
|
|
1271
|
+
],
|
|
1272
|
+
}}
|
|
1273
|
+
/>
|
|
1274
|
+
) : null}
|
|
1131
1275
|
</GeoJSONSource>
|
|
1132
1276
|
);
|
|
1133
1277
|
}
|