Skip to content

Commit c28a647

Browse files
Merge pull request #181 from CodeForPhilly/feat/zbl-heightslider
Add dual-handle height range slider with dynamic min/max based on fil…
2 parents 8de6cf8 + 348ff40 commit c28a647

3 files changed

Lines changed: 201 additions & 1 deletion

File tree

lib/server.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,104 @@ module.exports = async function ({ plants, nurseries }) {
879879
}
880880
}
881881
}
882+
// Calculate min/max height from filtered plants (excluding height filter)
883+
let heightRange = { min: 0, max: 0 };
884+
if (!req.query.favorites) {
885+
const heightFilter = filters.find(f => f.name === "Height (feet)");
886+
if (heightFilter && heightFilter.range) {
887+
// Rebuild all filter conditions from req.query, excluding height filter
888+
// This ensures we include ALL user filters (States, Sun Exposure, etc.)
889+
const heightMatchAnd = [];
890+
for (const filter of filters) {
891+
// Skip the height filter itself
892+
if (filter.name === "Height (feet)") {
893+
continue;
894+
}
895+
896+
if (filter.range) {
897+
const min = parseInt(req.query?.[filter.name]?.min);
898+
const max = parseInt(req.query?.[filter.name]?.max);
899+
if (
900+
!isNaN(min) &&
901+
!isNaN(max) &&
902+
min <= max &&
903+
(min !== 0 || max !== filter.choices.length - 1)
904+
) {
905+
const $in = [];
906+
for (let i = min; i <= max; i++) {
907+
$in.push(i);
908+
}
909+
if ($in.length) {
910+
heightMatchAnd.push({
911+
[filter.byNumber]: {
912+
$in,
913+
},
914+
});
915+
}
916+
}
917+
} else if (filter.boolean) {
918+
let input = req.query[filter.name] || [];
919+
if (input && input.length) {
920+
heightMatchAnd.push({
921+
[filter.name]: true,
922+
});
923+
}
924+
} else if (filter.array) {
925+
let input = req.query[filter.name] || [];
926+
if (!Array.isArray(input)) {
927+
input = [input];
928+
}
929+
if (input && input.length) {
930+
heightMatchAnd.push({
931+
[filter.name]: {
932+
$in: input,
933+
},
934+
});
935+
}
936+
}
937+
}
938+
939+
// Build match query with all filters except height
940+
const matchQuery = {};
941+
if (heightMatchAnd.length > 0) {
942+
matchQuery.$and = heightMatchAnd;
943+
}
944+
945+
// For semantic search, use scoredResults if available
946+
if (query._useSemantic && scoredResults) {
947+
const allHeights = [];
948+
for (const plant of scoredResults) {
949+
const heights = plant["Height (feet) By Number"];
950+
if (Array.isArray(heights) && heights.length > 0) {
951+
allHeights.push(...heights);
952+
}
953+
}
954+
if (allHeights.length > 0) {
955+
heightRange.min = Math.min(...allHeights);
956+
heightRange.max = Math.max(...allHeights);
957+
}
958+
} else {
959+
// Use aggregation to find min/max from filtered plants
960+
const heightAggregation = await plants.aggregate([
961+
{ $match: matchQuery },
962+
{ $unwind: { path: `$${heightFilter.byNumber}`, preserveNullAndEmptyArrays: false } },
963+
{
964+
$group: {
965+
_id: null,
966+
minHeight: { $min: `$${heightFilter.byNumber}` },
967+
maxHeight: { $max: `$${heightFilter.byNumber}` }
968+
}
969+
}
970+
]).toArray();
971+
972+
if (heightAggregation.length > 0 && heightAggregation[0].minHeight !== null) {
973+
heightRange.min = heightAggregation[0].minHeight;
974+
heightRange.max = heightAggregation[0].maxHeight;
975+
}
976+
}
977+
}
978+
}
979+
882980
let response = {
883981
results,
884982
total,
@@ -898,6 +996,7 @@ module.exports = async function ({ plants, nurseries }) {
898996
),
899997
])
900998
),
999+
heightRange: heightRange,
9011000
};
9021001
}
9031002
for (const filter of filters) {

src/components/Explorer.vue

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ export default {
645645
{
646646
name: "Height (feet)",
647647
range: true,
648-
double: false,
648+
double: true,
649649
choices: [],
650650
min: 0,
651651
max: 0,
@@ -2399,6 +2399,62 @@ export default {
23992399
for (const filter of this.filters) {
24002400
filter.choices = data.choices[filter.name];
24012401
}
2402+
// Update height filter min/max based on filtered plants
2403+
if (data.heightRange && data.heightRange.min !== undefined && data.heightRange.max !== undefined) {
2404+
const heightFilter = this.filters.find(f => f.name === "Height (feet)");
2405+
if (heightFilter) {
2406+
const oldMin = heightFilter.min;
2407+
const oldMax = heightFilter.max;
2408+
// Only update if we have a valid range (max >= min) and it's not the default 0-0 (no plants)
2409+
// Also update if we're initializing (oldMin === oldMax === 0)
2410+
const isInitializing = oldMin === 0 && oldMax === 0;
2411+
if (data.heightRange.max >= data.heightRange.min && (data.heightRange.max > 0 || isInitializing)) {
2412+
heightFilter.min = data.heightRange.min;
2413+
heightFilter.max = data.heightRange.max;
2414+
// Update choices to match the available range
2415+
if (heightFilter.choices && heightFilter.choices.length > 0) {
2416+
heightFilter.choices = heightFilter.choices.filter(
2417+
choice => choice >= data.heightRange.min && choice <= data.heightRange.max
2418+
);
2419+
} else {
2420+
// Generate choices array if not present
2421+
heightFilter.choices = [];
2422+
for (let i = data.heightRange.min; i <= data.heightRange.max; i++) {
2423+
heightFilter.choices.push(i);
2424+
}
2425+
}
2426+
// Adjust filter value if it's outside the new range
2427+
const currentValue = this.filterValues[heightFilter.name];
2428+
if (currentValue) {
2429+
let needsUpdate = false;
2430+
const newValue = { ...currentValue };
2431+
if (currentValue.min < data.heightRange.min) {
2432+
newValue.min = data.heightRange.min;
2433+
needsUpdate = true;
2434+
}
2435+
if (currentValue.max > data.heightRange.max) {
2436+
newValue.max = data.heightRange.max;
2437+
needsUpdate = true;
2438+
}
2439+
// If range was reset (oldMin === oldMax === 0), set to full range
2440+
if (isInitializing && data.heightRange.max >= data.heightRange.min) {
2441+
newValue.min = data.heightRange.min;
2442+
newValue.max = data.heightRange.max;
2443+
needsUpdate = true;
2444+
}
2445+
if (needsUpdate) {
2446+
this.filterValues[heightFilter.name] = newValue;
2447+
}
2448+
} else {
2449+
// Initialize to full range if no value set
2450+
this.filterValues[heightFilter.name] = {
2451+
min: data.heightRange.min,
2452+
max: data.heightRange.max
2453+
};
2454+
}
2455+
}
2456+
}
2457+
}
24022458
}
24032459
if (!data.results.length || this.favorites) {
24042460
this.loadedAll = true;

src/components/Range.vue

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@
1818
</li>
1919
</ul>
2020
</div>
21+
<button
22+
v-if="isFilterActive"
23+
class="range-clear-button"
24+
@click="clearFilter"
25+
type="button"
26+
title="Clear height filter"
27+
>
28+
<span class="material-icons">close</span>
29+
Clear
30+
</button>
2131
</div>
2232
</template>
2333

@@ -50,6 +60,12 @@ export default {
5060
}
5161
},
5262
emits: [ 'update:modelValue' ],
63+
computed: {
64+
isFilterActive() {
65+
// Filter is active if current values differ from min/max range
66+
return this.modelValue.min !== this.min || this.modelValue.max !== this.max;
67+
}
68+
},
5369
data() {
5470
return {
5571
isDown: false,
@@ -169,6 +185,13 @@ export default {
169185
valueToPixels(value) {
170186
const linearUnit = (value - this.min) / (this.max - this.min);
171187
return Math.pow(linearUnit, 1 / this.exponent) * this.clientWidth;
188+
},
189+
clearFilter() {
190+
// Reset to full range
191+
this.$emit('update:modelValue', {
192+
min: this.min,
193+
max: this.max
194+
});
172195
}
173196
}
174197
};
@@ -221,4 +244,26 @@ export default {
221244
height: 2em;
222245
list-style: none;
223246
}
247+
.range-clear-button {
248+
display: flex;
249+
align-items: center;
250+
gap: 4px;
251+
margin-top: 8px;
252+
padding: 6px 12px;
253+
background-color: #fcf9f4;
254+
color: #b74d15;
255+
border: 1px solid #b74d15;
256+
border-radius: 4px;
257+
font-size: 14px;
258+
font-family: Roboto;
259+
cursor: pointer;
260+
transition: background-color 0.2s, color 0.2s;
261+
}
262+
.range-clear-button:hover {
263+
background-color: #b74d15;
264+
color: #fcf9f4;
265+
}
266+
.range-clear-button .material-icons {
267+
font-size: 16px;
268+
}
224269
</style>

0 commit comments

Comments
 (0)