Skip to content

Commit

Permalink
Merge pull request #1519
Browse files Browse the repository at this point in the history
Date Attribute Legends
  • Loading branch information
bfinzer authored Sep 26, 2024
2 parents 43cd5b9 + 4832b9a commit 13e23c4
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {axisBottom, scaleLinear, format, select, range, min, max, ScaleQuantile, NumberValue} from "d3"
import {kChoroplethHeight} from "../../../data-display-types"
import {neededSigDigitsArrayForQuantiles} from "../../../../../utilities/math-utils"
import { axisBottom, format, max, min, NumberValue, range, scaleLinear, ScaleQuantile, select } from "d3"
import { kChoroplethHeight } from "../../../data-display-types"
import { neededSigDigitsArrayForQuantiles } from "../../../../../utilities/math-utils"
import { DatePrecision, determineLevels, formatDate, mapLevelToPrecision } from "../../../../../utilities/date-utils"
import { getStringBounds } from "../../../../axis/axis-utils"

export type ChoroplethLegendProps = {
isDate?: boolean,
tickSize?: number,
width?: number,
rectHeight?: number,
Expand All @@ -16,14 +19,16 @@ export type ChoroplethLegendProps = {
}

type ChoroplethScale = ScaleQuantile<string>

export function choroplethLegend(scale: ChoroplethScale, choroplethElt: SVGGElement, props: ChoroplethLegendProps) {
if (scale.domain().length === 0) {
select(choroplethElt).selectAll("*").remove()
return
}

const {
tickSize = 6, transform = '', width = 320, marginTop = 0, marginRight = 0, marginLeft = 0,
isDate, tickSize = 6, transform = '', width = 320,
marginTop = 0, marginRight = 0, marginLeft = 0,
ticks = 5, clickHandler, casesInQuantileSelectedHandler
} = props,
minValue = min(scale.domain()) ?? 0,
Expand All @@ -41,43 +46,58 @@ export function choroplethLegend(scale: ChoroplethScale, choroplethElt: SVGGElem
const thresholds = scale.quantiles(),
fullBoundaries = [minValue, ...thresholds, maxValue],
domainValues = scale.domain(),
significantDigits = neededSigDigitsArrayForQuantiles(fullBoundaries, domainValues)
significantDigits = neededSigDigitsArrayForQuantiles(fullBoundaries, domainValues),
dateLevels = isDate ? determineLevels(minValue, maxValue) : {increment: 1, outerLevel: 0, innerLevel: 0},
datePrecision = isDate ? mapLevelToPrecision(dateLevels.innerLevel + 1) : DatePrecision.None

const thresholdFormat = format(tickFormatSpec)
const thresholdFormat = isDate ? (date: number) => formatDate(date * 1000, datePrecision) ?? ''
: format(tickFormatSpec)

const x = scaleLinear()
.domain([-1, scale.range().length - 1])
.rangeRound([marginLeft, width - marginRight])
const legendScale = scaleLinear()
.domain([-1, scale.range().length - 1])
.rangeRound([marginLeft, width - marginRight]),
tickValues = range(thresholds.length),
tickFormat = (i: NumberValue) => thresholdFormat(thresholds[Number(i)]),
minMaxFormat = isDate ? thresholdFormat
: (d: number, i: number) => format(`.${significantDigits[i === 0 ? 0 : 5]}r`)(d),
minStringWidth = getStringBounds(minMaxFormat(minValue, 0)).width,
onlyShowMinMax = minStringWidth > 3 * width / 20 - 10

svg.append("g")
.selectAll("rect")
.data(scale.range())
.join("rect")
.attr('class', 'choropleth-rect')
.classed('legend-rect-selected',
(color) => {
return casesInQuantileSelectedHandler(scale.range().indexOf(color))
})
.attr('transform', transform)
.attr("x", (d, i) => x(i - 1))
.attr("x", (d, i) => legendScale(i - 1))
.attr("y", marginTop)
.attr("width", (d, i) => x(i) - x(i - 1))
.attr("width", (d, i) => legendScale(i) - legendScale(i - 1))
.attr("height", kChoroplethHeight /*height - marginTop - marginBottom*/)
.attr("fill", (d: string) => d)
.on('click', (event, color) => {
clickHandler(scale.range().indexOf(color), event.shiftKey)
})
.append('title')
.text((color) => {
const quantile = scale.range().indexOf(color)
return `${thresholdFormat(fullBoundaries[quantile])} - ${thresholdFormat(fullBoundaries[quantile + 1])}`
})

const tickValues = range(thresholds.length)
const tickFormat = (i: NumberValue) => thresholdFormat(thresholds[Number(i)])

svg.append("g")
const legendAxis = svg.append("g")
.attr('class', 'legend-axis')
.attr("transform", `${transform} translate(0,${kChoroplethHeight + marginTop})`)
.call(axisBottom(x)
if (!onlyShowMinMax) {
legendAxis.call(axisBottom(legendScale)
.ticks(ticks)
.tickFormat(tickFormat)
.tickSize(tickSize)
.tickValues(tickValues))
}

svg.select('.legend-axis')
.append('g')
Expand All @@ -90,7 +110,7 @@ export function choroplethLegend(scale: ChoroplethScale, choroplethElt: SVGGElem
.attr('y', kChoroplethHeight)
.style('text-anchor', (d, i) => i ? 'end' : 'start')
.attr('x', (d, i) => i * width)
.text((d, i) => format(`.${significantDigits[i === 0 ? 0 : 5]}r`)(d))
.text(minMaxFormat)
)

return svg.node()
Expand Down
4 changes: 4 additions & 0 deletions v3/src/components/data-display/components/legend/legend.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
stroke-width: 2px !important;
}

.choropleth-rect {
cursor: pointer;
}

.legend-categories {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const Legend = function Legend({
? <CategoricalLegend
layerIndex={layerIndex}
setDesiredExtent={setDesiredExtent}/>
: attrType === 'numeric'
: attrType === 'numeric' || attrType === 'date'
? <NumericLegend
layerIndex={layerIndex}
setDesiredExtent={setDesiredExtent}/> : null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const NumericLegend =
quantileScale.current.domain(valuesRef.current).range(schemeBlues[5])
choroplethLegend(quantileScale.current, choroplethElt,
{
isDate: dataConfiguration?.attributeType('legend') === 'date',
width: tileWidth,
marginLeft: 6, marginTop: labelHeight, marginRight: 6, ticks: 5,
clickHandler: (quantile: number, extend: boolean) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import {applyModelChange} from "../../../models/history/apply-model-change"
import {cachedFnWithArgsFactory, onAnyAction} from "../../../utilities/mst-utils"
import { isFiniteNumber } from "../../../utilities/math-utils"
import { stringValuesToDateSeconds } from "../../../utilities/date-utils"
import {AttributeType, attributeTypes} from "../../../models/data/attribute"
import {DataSet, IDataSet} from "../../../models/data/data-set"
import {ICase} from "../../../models/data/data-set-types"
Expand Down Expand Up @@ -418,6 +419,11 @@ export const DataConfigurationModel = types
return self.legendQuantileScale(value)
},

getLegendColorForDateValue(value: string): string {
const dateValueArray = stringValuesToDateSeconds([value])
return self.legendQuantileScale(dateValueArray[0])
},

getCasesForCategoryValues(
primaryAttrRole: AttrRole, primaryValue: string, secondaryValue?: string, primarySplitValue?: string,
secondarySplitValue?: string, legendCat?: string, extend = false
Expand Down Expand Up @@ -487,7 +493,7 @@ export const DataConfigurationModel = types
max = quantile === thresholds.length ? Infinity : thresholds[quantile]
return legendID
? self.caseDataArray.filter((aCaseData: CaseData) => {
const value = dataset?.getNumeric(aCaseData.caseID, legendID)
const value = dataDisplayGetNumericValue(dataset, aCaseData.caseID, legendID)
return value !== undefined && value >= min && value < max
}).map((aCaseData: CaseData) => aCaseData.caseID)
: []
Expand Down Expand Up @@ -526,6 +532,8 @@ export const DataConfigurationModel = types
return self.getLegendColorForCategory(legendValue)
case 'numeric':
return self.getLegendColorForNumericValue(Number(legendValue))
case 'date':
return self.getLegendColorForDateValue(legendValue)
default:
return ''
}
Expand Down

0 comments on commit 13e23c4

Please sign in to comment.