Commit 651cde06 authored by amarcic's avatar amarcic
Browse files

Merge branch 'globalHighlighting'

parents 3604f927 5f8b2db2
......@@ -39,6 +39,7 @@ const initialInput = {
selectedMarker: undefined,
timelineSort: "period",
highlightedTimelineObject: undefined,
highlightedObjects: [],
areaA: 1,
areaB: 0,
bigTileArea: "",
......@@ -403,6 +404,7 @@ export const AppContent = () => {
maximizeTileButton={maximizeTileButton}
/>
|| input[area]===1 && <Histogram
reducer={[input, dispatch]}
timelineData={dataObjects?.entitiesMultiFilter.map(timelineMapper)}
maximizeTileButton={maximizeTileButton}
/>
......
......@@ -12,11 +12,12 @@ export const Histogram = (props) => {
const classes = useStyles();
//console.log(props.timelineData);
const [input, dispatch] = props.reducer;
console.log(props.timelineData);
const preparedData = prepareHistogramData(props.timelineData)?.filter( e => e&&e );
//console.log(preparedData);
//console.log("data prepared for histogram: ", preparedData);
const binnedData = binTimespanObjects({timespanObjects: preparedData, approxAmountBins: 20});
//console.log(binnedData);
console.log("data binned for histogram: ", binnedData);
const svgRef = useRef();
//const [data, setData] = useState(binnedData);
......@@ -38,8 +39,6 @@ export const Histogram = (props) => {
//remove previously rendered histogram bars in the case there is no current data from the current search
if (!binnedData||preparedData.length===0) {
svg.select(".bars")
//.append("text")
//.text("hier gibt es nichts zu sehen")
.selectAll(".bar").remove()
} else {
//maximum value on y axis
......@@ -57,12 +56,6 @@ export const Histogram = (props) => {
.domain([0,maxYValue])
.range([height, 0]);
/* unused color scale
const colorScale = scaleLinear()
.domain([0,maxYValue])
.range(["#69b3a2","red"]);
*/
//add x axis to svg and rotate labels
//todo: labels should explicitly convey the span of years the bin covers, not just the lower threshold
svg.select(".xAxis")
......@@ -76,6 +69,8 @@ export const Histogram = (props) => {
svg.select(".yAxis")
.call(axisLeft(y));
//color scale;
//todo: remove; makes no sense to visualize the same thing in two ways
const colorScale = scaleQuantize()
.domain([0,maxYValue])
.range(["#5AE6BA","#4BC8A3","#3EAA8C","#318D75","#25725F"]);
......@@ -84,8 +79,12 @@ export const Histogram = (props) => {
svg.select(".bars")
.attr("transform",`translate(${margin.left}, ${margin.top})`)
.selectAll("rect").data(binnedData).join(
enter => enter.append("rect")
).attr("class","bar")
enter =>
enter.append("rect")).attr("class", value =>
value.values.some( id =>
input.highlightedObjects.indexOf(id) > -1 )
? "bar highlighted"
: "bar")
//.attr("y", value => y(value.values.length))
.attr("y", height*-1)
.attr("x", value => x(value.lower))
......@@ -101,14 +100,19 @@ export const Histogram = (props) => {
.data([value])
.join("text")
.attr("class","tooltip")
.text(`${value.lower}-${value.upper}: ${value.values.length}`)
.text(`${value.lower}-${value.upper}: ${value.values.length} ${t("Item", {count: value.values.length})}`)
.attr("text-anchor","middle")
.transition()
.attr("x", x(value.lower)+x.bandwidth())
.attr("y", y(value.values.length)+3)
.attr("y", y(value.values.length)+3);
})
//.on("mouseleave", () => svg.select(".tooltip").remove())
.on("click", (event, value) => {
dispatch({
type: "UPDATE_INPUT",
payload: {field: "highlightedObjects", value: value.values}
});
} )
.transition()
.attr("height", value => height - y(value.values.length))
.attr("fill", value => colorScale(value.values.length));
......
......@@ -12,6 +12,7 @@ export const Timeline = (props) => {
const [dimensions, setDimensions] = useState({width: 0, height: 0, margin: {top: 0, right: 0, left: 0, bottom: 0}});
const { timelineObjectsData, maximizeTileButton } = props;
const [input, dispatch] = props.reducer;
const filteredTimelineData = timelineObjectsData&&timelineObjectsData
.filter( datapoint =>
datapoint.periodSpans?.[0]!==undefined||datapoint.periodSpans?.length>1);
......@@ -36,7 +37,11 @@ export const Timeline = (props) => {
</Grid>
<Grid id="timelineContainer" className={classes.dashboardTileContent} item container direction="column" spacing={2}>
<Grid item>
<TimelineChart filteredTimelineData={filteredTimelineData} dimensions={dimensions/*getDimensions("timelineContainer")*/} />
<TimelineChart
reducer={[input, dispatch]}
filteredTimelineData={filteredTimelineData}
dimensions={dimensions}
/>
</Grid>
</Grid>
</>
......
......@@ -9,15 +9,12 @@ export const TimelineChart = (props) => {
const classes = useStyles();
const [input, dispatch] = props.reducer;
const svgRef = useRef();
console.log("dimensions", props.dimensions)
//todo: make highlighted global state?
let highlighted = {
objects: [],
periods: [],
locations: []
};
const xDomain = getTimeRangeOfTimelineData(props.filteredTimelineData,"period");
const dataUnsorted = newGroupByPeriods(props.filteredTimelineData);
const data = dataUnsorted && new Map([...dataUnsorted.entries()]
......@@ -46,7 +43,7 @@ export const TimelineChart = (props) => {
//draw timeline everytime filteredTimelineData changes
useEffect( () => {
drawTimeline(timelineData, props.dimensions)
}, [props.filteredTimelineData, props.dimensions] );
}, [props.filteredTimelineData, props.dimensions, input] );
//todo: check if still depending on outer scope (like height, width, margin)
const drawTimeline = (timelineConfig, dimensions) => {
......@@ -54,6 +51,7 @@ export const TimelineChart = (props) => {
const { data, svgRef, xDomain } = timelineConfig;
const { width, height, margin } = dimensions;
const svg = select(svgRef.current);
//todo: change name to be more describing: limit of bar height for rendering labels in different ways
const labelRenderLimit = 8;
//empty canvas in case no data is found by query
......@@ -91,8 +89,8 @@ export const TimelineChart = (props) => {
.domain(itemQuantityExtent)
.range(["#5AE6BA","#4BC8A3","#3EAA8C","#318D75","#25725F"]);
//function to add labels to the bars (when bandwith is heigh enough for readable labels)
//todo: remove outer dependency on selectionLabels
//function to add labels to the bars (when bandwidth is high enough for readable labels)
//todo: remove outer dependency on selectionLabels?
const addLabels = (bandwidth, renderLimit) => {
if (bandwidth > renderLimit) {
selectionLabels
......@@ -179,13 +177,27 @@ export const TimelineChart = (props) => {
.attr("x", value => xScale(value.periodSpan?.[0]))
.attr("y", (value, index) => yScale(periodIds[index]))
.attr("width", value => Math.abs(xScale(value.periodSpan?.[0])-xScale(value.periodSpan?.[1]))||0)
.attr("class", value =>
value.items.map( item =>
item.id ).some( id =>
input.highlightedObjects.indexOf(id) >-1 )
? "bar highlighted"
: "bar")
.attr("fill", value => colorScale(value.items.length))
/*.attr("stroke", value => highlighted.objects.some( id =>
value.items.map( item => item.id).indexOf(id) > -1)
? "black"
: "red")*/
//display tooltip when mouse enters bar on chart
selectionEnteringAndUpdating
.on("mouseenter", (event, value) => {
highlighted.objects = value.items.map( item => item.id);
//console.log("high: ", highlighted)
select(event.currentTarget)
.attr("id", "mouse-target")
.on("mouseleave", () => {
select("#mouse-target")
.attr("id", null)
})
svg
.selectAll(".tooltip")
.data([value])
......@@ -207,6 +219,14 @@ export const TimelineChart = (props) => {
.remove()
} )
selectionEnteringAndUpdating
.on("click", (event, value) => {
dispatch({
type: "UPDATE_INPUT",
payload: {field: "highlightedObjects", value: value.items.map( item => item.id)}
});
})
addLabels(yScale.bandwidth(), labelRenderLimit);
//apply zoom
......@@ -217,145 +237,6 @@ export const TimelineChart = (props) => {
}
/*
useEffect(() => {
svg.attr()
//remove previously rendered timeline bars in the case there is no data from the current search
//if (!timelineObjectsData||!byPeriodData||byPeriodData.size===0) {
if (!data||data.size===0) {
svg.select(".bars")
.selectAll(".bar").remove()
svg.select(".bars")
.selectAll(".label").remove()
svg.select(".background").selectAll("rect").remove();
} else {
const periodIds = [...data.keys()];
//scale for the x axis
const xScale = scaleLinear()
.domain(xDomain)
.range([0,width])
//scale for the y axis
const yScale = scaleBand()
.domain(periodIds)
.range([height,0])
.padding(0.2)
//todo: replace this temporary cleanup of the background svg group
svg.select(".background").selectAll("rect").remove();
//select and position svg element for the timeline
const timelineCanvas = svg.select(".bars")
.attr("transform",`translate(${margin.left}, ${margin.top})`)
//bind svg groups for bars and labels to the values of the data (map grouped by periods)
const barGroups = timelineCanvas
.selectAll(".barGroup")
.data([...data.values()], (data,index) => data.periodName+data.periodSpan[0]);
//todo: evaluate if generating the key form period name + period beginning sufficiently unique?
//console.log("bar groups: ", barGroups);
//instructions for joining the synced data and group elements: add svg g, rect and text elements on enter
//returns a selection of new and updating groups
const newAndUpdatedGroups = barGroups
.join(
enter => {
let group = enter
.append("g");
group.attr("class","barGroup");
group
.append("rect")
.attr("class", "bar")
.attr("height", yScale.bandwidth())
.attr("fill", "#69b3a2");
group
.append("text")
.attr("class","label");
return group;
}
)
//position all new and updating groups
newAndUpdatedGroups
.attr("transform", (value, index) => `translate(${xScale(value.periodSpan?.[0])},${yScale(periodIds[index])})`)
//extend the svg rect elements as chart bars according to the data and return as "bar"
const bar = newAndUpdatedGroups.selectAll(".bar")
.transition()
.attr("width", value => Math.abs(xScale(value.periodSpan?.[0])-xScale(value.periodSpan?.[1]))||0);
//add period names as text labels to the groups and return as "labels"
const labels = newAndUpdatedGroups.selectAll(".label")
.text(value => value.periodName);
console.log("new and updated after labels: ", newAndUpdatedGroups);
//attach click event and sort function to sort button;
select(".sortButton").on("click", () =>
svg.selectAll(".barGroup").sort( (a,b) => ascending(parseInt(a.periodSpan?.[0]),parseInt(b.periodSpan?.[0])) )
.attr("transform", (value, index) => `translate(${xScale(value.periodSpan?.[0])},${yScale(periodIds[index])})`)
);
//on click show detailed view of dated objects for a period
newAndUpdatedGroups.on("click", (event, value) => {
//const element = svg.selectAll(".bar").nodes(),
// index = element.indexOf(event.currentTarget);
const itemDatingMin = min(value.items, item => item.spanDated?.[0]),
itemDatingMax = max(value.items, item => item.spanDated?.[1]);
const xScaleDetail = scaleLinear()
.domain([itemDatingMin,itemDatingMax])
.range([0,width])
const yScaleDetail = scaleLinear()
.domain([0, value.items.length])
.range([height,0]);
//remove bars and labels
svg.selectAll(".bar").remove();
svg.selectAll(".label").remove();
svg.select(".background").selectAll("rect").remove();
//attach x axis
svg.select(".xAxis")
.attr("transform", `translate(0,${height})`)
.call(axisBottom(xScaleDetail))
//join svg rect elements with array of item dating
svg.select(".bars")
.selectAll("rect").data(value.items).join(
enter => enter.append("rect")
).attr("class","bar")
.attr("x", value => xScaleDetail(value.spanDated?.[0]))
.attr("y", (value,index) => yScaleDetail(index))
.attr("height", 5)
.transition()
.attr("width", value => Math.abs(xScaleDetail(value.spanDated?.[0])-xScaleDetail(value.spanDated?.[1]))+1||0)
//paint on the background a rect representing the temporal extension of the period
svg.select(".background")
.selectAll("rect").data([value]).join("rect")
.attr("width", Math.abs(xScaleDetail(value.periodSpan?.[0])-xScaleDetail(value.periodSpan?.[1]))||0)
.attr("height", height)
.attr("x", xScaleDetail(value.periodSpan[0]))
.transition()
.attr("opacity", 0.3)
})
}
}, [data])
*/
return (
<div className="timeline">
<svg ref={svgRef}>
......
......@@ -19,4 +19,11 @@
.marker-cluster-large div {
background-color: #1c9489 !important;
color: #fff !important;
}
\ No newline at end of file
}
.highlighted {
stroke: pink;
stroke-width: 2;
}
#mouse-target {
stroke: blue
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment