Skip to content

Commit

Permalink
Merge pull request #4 from abcnews/cases-graphic-date-windows
Browse files Browse the repository at this point in the history
Cases graphic date windows
  • Loading branch information
colingourlay committed Aug 4, 2020
2 parents 9256abf + c88d1f4 commit c80a0b6
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 160 deletions.
2 changes: 2 additions & 0 deletions public/standalone-cases-graphic/index.html
Expand Up @@ -32,6 +32,8 @@ <h2>Encoded (hint, with longform elsewhere in the document)</h2>
name="q7thzrjkv0fdnhidlngm3ga42ak48hdju5fejoby9ioo1x5ps7ctjob1m9d5vmo2ppbndxm15l62xxyr0c6kd9p7qkafd5l25ek4n70go5euox5zgc468x8wjg128jyauwio7dzg1i27m3t5p23gilocswh4j4ggba68fajd6boi3gz3x0ge5kda7vrjd5bffm4yehhifd6fmpz84qvlt6tlurk6xu0k730fdx2963zlhxb70kpgre0dzzjpg2qw95likee66kde1952ydz68irjhjvloqts3oxhj15dagi5s7i2axzb5vrijferswdo0miw4e0m84tyq49kc52qgfvry3jgssrpp7hescfz4u861wlkkqqykruwxvh"
>
</a>
<h2>Preset (with MAXDATE)</h2>
<a name="casesgraphicPRESETkeyMAXDATE20200331"> </a>
<script src="../standalone-cases-graphic.js"></script>
</body>
</html>
17 changes: 11 additions & 6 deletions src/components/App/index.js
Expand Up @@ -7,10 +7,11 @@ import DoublingGraphic from '../DoublingGraphic';
import Placeholder from '../TestComponent';
import styles from './styles.css';

const ONE_DAY = 864e5;
const DATE_USA_HIT_100_CASES = new Date(2020, 2, 4);

const App = ({ scrollyData }) => {
const [preset, setPreset] = useState('initial');
let maxDate = getInclusiveDateFromYYYYMMDD(scrollyData.panels.length ? scrollyData.panels[0].config.maxdate : '');

const onMarker = useCallback(config => {
const { preset } = config;

Expand All @@ -22,10 +23,14 @@ const App = ({ scrollyData }) => {

if (graphic === 'cases') {
Graphic = CasesGraphic;
graphicProps = {
...graphicProps,
maxDate
};

const maxDate = getInclusiveDateFromYYYYMMDD(scrollyData.panels.length ? scrollyData.panels[0].config.maxdate : '');

if (maxDate) {
graphicProps.toDate = maxDate;
graphicProps.xScaleDaysCap = Math.max(30, Math.round((maxDate - DATE_USA_HIT_100_CASES) / ONE_DAY));
console.log(graphicProps);
}
} else if (graphic === 'doubling') {
Graphic = DoublingGraphic;
}
Expand Down
133 changes: 65 additions & 68 deletions src/components/CasesGraphic/index.js
Expand Up @@ -12,6 +12,7 @@ import {
scaleLog,
scaleTime,
select,
timeDay,
timeWeek,
timeFormat
} from 'd3';
Expand Down Expand Up @@ -135,7 +136,7 @@ function createTrendCasesData(increasePerPeriod, daysToSimulate, startingValue)
return data;
}

function generateTrendsData(trends, startDate, numDays, yScaleCap) {
function generateTrendsData(trends, startDate, numDays, yUpperExtent) {
const dates = [];
let currentDate = new Date(startDate);

Expand All @@ -146,7 +147,7 @@ function generateTrendsData(trends, startDate, numDays, yScaleCap) {

return trends.reduce((memo, trend) => {
const increasePerPeriod = calculateIncreasePerPeriod(trend.doublingTimePeriods);
const casesData = createTrendCasesData(increasePerPeriod, numDays, 100).filter(count => count <= yScaleCap);
const casesData = createTrendCasesData(increasePerPeriod, numDays, 100).filter(count => count <= yUpperExtent);
const item = {
key: trend.name,
doublingTimePeriods: trend.doublingTimePeriods,
Expand All @@ -156,8 +157,8 @@ function generateTrendsData(trends, startDate, numDays, yScaleCap) {
}
};

const daysToYScaleCap = calculatePeriodsToIncrease(increasePerPeriod, 100, yScaleCap);
const daysDiff = Math.min(1, daysToYScaleCap - casesData.length + 1);
const daysToYUpperExtent = calculatePeriodsToIncrease(increasePerPeriod, 100, yUpperExtent);
const daysDiff = Math.min(1, daysToYUpperExtent - casesData.length + 1);

if (daysDiff > 0) {
const lastItem = last(item.dataAs.dates);
Expand All @@ -166,8 +167,8 @@ function generateTrendsData(trends, startDate, numDays, yScaleCap) {
// Meet extent of y-scale
let fractionalDate = new Date(lastItem.date.valueOf() + ONE_DAY * daysDiff);

item.dataAs.dates.push({ date: fractionalDate, cases: yScaleCap });
item.dataAs.daysSince100Cases.push({ day: daysToYScaleCap, cases: yScaleCap });
item.dataAs.dates.push({ date: fractionalDate, cases: yUpperExtent });
item.dataAs.daysSince100Cases.push({ day: daysToYUpperExtent, cases: yUpperExtent });
} else {
// Meet extent of x-scale
let fractionalCases = lastItem.cases + lastItem.cases * increasePerPeriod;
Expand Down Expand Up @@ -220,9 +221,7 @@ function setTruncatedLineDashArray(node) {

let transformedPlacesDataCache = {};

function transformPlacesData(placesData, maxDate, placesDataURL) {
const cacheKey = `${placesDataURL}_${maxDate}`;

function transformPlacesData(placesData, cacheKey) {
if (!transformedPlacesDataCache[cacheKey]) {
transformedPlacesDataCache[cacheKey] = Object.keys(placesData)
.map(place => {
Expand Down Expand Up @@ -270,7 +269,7 @@ function transformPlacesData(placesData, maxDate, placesDataURL) {
}
]);
}, [])
.filter(({ cases, date }) => cases >= 1 && (!maxDate || date <= maxDate)); // should this be filtered on maxDate at render time?
.filter(({ cases, date }) => cases >= 1);

const dataAs_daysSince100Cases = dataAs_dates
.filter(({ cases }) => cases >= 100)
Expand Down Expand Up @@ -325,7 +324,7 @@ let nextIDIndex = 0;
const CasesGraphic = props => {
const renderId = generateRenderId();

const { placesDataURL, maxDate, xScaleType, yScaleType, title, hasFootnotes } = {
const { placesDataURL, xScaleType, yScaleType, title, hasFootnotes } = {
...DEFAULT_PROPS,
...props
};
Expand All @@ -351,12 +350,12 @@ const CasesGraphic = props => {
{ isLoading: isPlacesDataLoading, error: placesDataError, data: untransformedPlacesData },
setPlacesDataURL
] = usePlacesData(placesDataURL);
const [placesData, earliestDate, latestDate, numDates] = useMemo(() => {
const [placesData, earliestDate, latestDate] = useMemo(() => {
if (!untransformedPlacesData) {
return [];
}

const placesData = transformPlacesData(untransformedPlacesData, maxDate, placesDataURL);
const placesData = transformPlacesData(untransformedPlacesData, placesDataURL);
const earliestDate = placesData.reduce((memo, d) => {
const candidate = d.dataAs.dates[0].date;

Expand All @@ -375,10 +374,9 @@ const CasesGraphic = props => {

return memo;
}, last(placesData[0].dataAs.dates).date);
const numDates = Math.round((latestDate - earliestDate) / ONE_DAY);

return [placesData, earliestDate, latestDate, numDates];
}, [untransformedPlacesData, maxDate]);
return [placesData, earliestDate, latestDate];
}, [untransformedPlacesData]);
const [state, setState] = useState({
width: null,
height: null,
Expand Down Expand Up @@ -451,7 +449,9 @@ const CasesGraphic = props => {
trends,
xScaleType,
yScaleType,
yScaleProp
yScaleProp,
fromDate,
toDate
} = {
...DEFAULT_PROPS,
...props
Expand Down Expand Up @@ -498,10 +498,11 @@ const CasesGraphic = props => {
places = placesData.filter(places).map(x => x.key);
}

// Filter placesData to just visible places, and create visible/highlighted comparison utils
const isPlaceVisible = inclusionCheckGenerator(places, 'key');
const isPlaceHighlighted = inclusionCheckGenerator(highlightedPlaces, 'key');
const visiblePlacesData = placesData.filter(isPlaceVisible);
const timeLowerExtent = fromDate ? new Date(fromDate) : earliestDate;
const timeUpperExtent = toDate ? new Date(toDate) : latestDate;
const timeRangeDays = Math.round((timeUpperExtent - timeLowerExtent) / ONE_DAY);
const timeRangeFilter = d => d.date >= timeLowerExtent && d.date <= timeUpperExtent;
const daysCapFilter = d => xScaleDaysCap === false || d.day <= xScaleDaysCap;

// Only allow trend lines when we are showing cases since 100th case
if (yScaleProp !== 'cases' || xScaleType !== 'daysSince100Cases') {
Expand All @@ -514,42 +515,42 @@ const CasesGraphic = props => {
const isDailyFigures = yScaleProp.indexOf('new') === 0;
const isPerCapitaFigures = yScaleProp.indexOf('pmp') > -1;

const underlyingProp = yScaleProp.match(UNDERLYING_PROPS_PATTERN)[0];
const logarithmicLowerExtent =
LOWER_LOGARITHMIC_EXTENTS[yScaleProp] || (isDailyFigures && isPerCapitaFigures ? 0.01 : 0.1);

if (isDailyFigures || isPerCapitaFigures) {
yScaleCap = false;
}

const largestVisibleYScaleValue = visiblePlacesData.reduce((memo, d) => {
return Math.max.apply(null, [memo].concat(d.dataAs.dates.map(t => t[yScaleProp])));
}, 0);
const underlyingProp = yScaleProp.match(UNDERLYING_PROPS_PATTERN)[0];
const logarithmicLowerExtent =
LOWER_LOGARITHMIC_EXTENTS[yScaleProp] || (isDailyFigures && isPerCapitaFigures ? 0.01 : 0.1);
const logarithmicLowerExtentFilter = d => d[underlyingProp] >= LOWER_LOGARITHMIC_EXTENTS[underlyingProp];

// Y-scale cap should be the lower of the passed in prop and the largest value of the current Y-scale prop
yScaleCap = yScaleCap === false ? largestVisibleYScaleValue : Math.min(yScaleCap, largestVisibleYScaleValue);
// Filter placesData to just visible places, and create visible/highlighted comparison utils
const isPlaceVisible = inclusionCheckGenerator(places, 'key');
const isPlaceHighlighted = inclusionCheckGenerator(highlightedPlaces, 'key');

const cappedNumDays =
xScaleType.indexOf('dates') === 0
? null
: visiblePlacesData.reduce((memo, d) => {
const itemsWithinCaps = d.dataAs[xScaleType].filter(
item => (xScaleDaysCap === false || item.day <= xScaleDaysCap) && item[yScaleProp] <= yScaleCap
);
let yUpperExtent = yScaleCap || 0;
let xDaysUpperExtent = xScaleDaysCap || 0;

if (!itemsWithinCaps.length) {
return memo;
const visiblePlacesData = placesData.filter(isPlaceVisible).filter(
place =>
place.dataAs[xScaleType]
.filter(d => (xScaleType !== 'dates' ? daysCapFilter(d) : timeRangeFilter(d)))
.filter(d => typeof yScaleCap !== 'number' || d[yScaleProp] <= yScaleCap)
.filter(d => yScaleType !== 'logarithmic' || logarithmicLowerExtentFilter(d))
.map(d => {
// Update dataset-limited extents for use in scales/filters later

if (xScaleType !== 'dates') {
xDaysUpperExtent = Math.max(xDaysUpperExtent, d[xScaleProp]);
}

return Math.max(memo, last(itemsWithinCaps).day);
}, 0);
if (typeof yScaleCap !== 'number') {
yUpperExtent = Math.max(yUpperExtent, d[yScaleProp]);
}

// TODO:
// The yScaleCap may have potentially lowered due to cappedNumDays
// filtering out some of our data. Before scales & axes are generated,
// we could safely adjust yScaleCap now to the smaller of:
// * Itself, and
// * The largest dataAs[xScaleType]#yScaleProp value
return d;
}).length > 0
);

const xAxisLabel =
xScaleProp === 'day' ? `Days since ${UNDERLYING_PROPS_LOWER_LOGARITHMIC_EXTENT_LABELS[underlyingProp]}` : 'Date';
Expand All @@ -563,33 +564,29 @@ const CasesGraphic = props => {
const chartWidth = width - MARGIN.right - MARGIN.left;
const chartHeight = svgHeight - MARGIN.top - MARGIN.bottom;
const xScale = (xScaleType === 'dates'
? scaleTime().domain([new Date(earliestDate), new Date(latestDate)])
: scaleLinear().domain([0, cappedNumDays])
? scaleTime().domain([timeLowerExtent, timeUpperExtent])
: scaleLinear().domain([0, xDaysUpperExtent])
).range([0, chartWidth]);
const yScale = (yScaleType === 'logarithmic'
? scaleLog().domain([logarithmicLowerExtent, yScaleCap], true)
: scaleLinear().domain([0, yScaleCap], true)
? scaleLog().domain([logarithmicLowerExtent, yUpperExtent], true)
: scaleLinear().domain([0, yUpperExtent], true)
).range([chartHeight, 0]);
const safe_yScale = x =>
yScale(yScaleType === 'logarithmic' && x <= logarithmicLowerExtent ? logarithmicLowerExtent : x);
const getUncappedDataCollection = d =>
d.dataAs[xScaleType].filter(item => item[underlyingProp] >= LOWER_LOGARITHMIC_EXTENTS[underlyingProp]);
const getUncappedYDataCollection = d =>
d.dataAs[xScaleType].filter(item =>
xScaleType.indexOf('days') === 0 ? daysCapFilter(item) : timeRangeFilter(item)
);
const getDataCollection = d =>
getUncappedDataCollection(d).reduce(
(memo, item) =>
memo.concat(
item[yScaleProp] <= yScaleCap &&
(xScaleType.indexOf('days') === -1 || xScaleDaysCap === false || item.day <= xScaleDaysCap)
? [item]
: []
),
[]
getUncappedYDataCollection(d).filter(
item => item[yScaleProp] <= yUpperExtent && (yScaleType !== 'logarithmic' || logarithmicLowerExtentFilter(item))
);
const generateLinePath = d =>
line()
.x(d => xScale(d[xScaleProp]))
.y(d => safe_yScale(d[yScaleProp]))(getDataCollection(d));
const isPlaceYCapped = d => last(getUncappedDataCollection(d))[yScaleProp] > yScaleCap;
const isPlaceYCapped = d =>
typeof yScaleCap === 'number' && last(getUncappedYDataCollection(d))[yScaleProp] > yScaleCap;
const generateLinePathLength = d => (isPlaceYCapped(d) ? 100 : 95.5);
const plotPointTransformGenerator = d => `translate(${xScale(d[xScaleProp])}, ${safe_yScale(d[yScaleProp])})`;
const lineEndTransformGenerator = d => plotPointTransformGenerator(last(getDataCollection(d)));
Expand Down Expand Up @@ -617,21 +614,21 @@ const CasesGraphic = props => {
const visibleTrendsData = generateTrendsData(
TRENDS.filter(isTrendVisible),
earliestDate,
xScaleType === 'dates' ? numDates : cappedNumDays,
yScaleCap
xScaleType === 'dates' ? timeRangeDays : xDaysUpperExtent,
yUpperExtent
);
const getAllocatedColor = generateColorAllocator(visiblePlacesData);
const xAxisGenerator =
xScaleType === 'dates'
? axisBottom(xScale)
.ticks(timeWeek.every(2))
.ticks(timeRangeDays < 10 ? timeDay.every(1) : timeRangeDays < 60 ? timeWeek.every(1) : timeWeek.every(2))
.tickFormat(timeFormat('%-d/%-m'))
: axisBottom(xScale).ticks(5);
const yAxisGeneratorBase = () =>
yScaleType === 'linear'
? axisLeft(yScale).ticks(5)
: axisLeft(yScale).tickValues(
TICK_VALUES['logarithmic'].filter(value => value >= logarithmicLowerExtent && value <= yScaleCap)
TICK_VALUES['logarithmic'].filter(value => value >= logarithmicLowerExtent && value <= yUpperExtent)
);
// const yAxisGenerator = yAxisGeneratorBase().tickFormat(format('~s'));
const yAxisGenerator = yAxisGeneratorBase().tickFormat(value => (value >= 1 ? FORMAT_S(value) : value));
Expand Down Expand Up @@ -921,7 +918,7 @@ const CasesGraphic = props => {
fx: 0,
targetY: safe_yScale(last(getDataCollection(d))[yScaleProp])
}));
if (chartWidth < 640 || xScaleType === 'dates' || yScaleType === 'logarithmic') {
if (chartWidth < 640 || xScaleType === 'dates' || xScaleDaysCap || yScaleType === 'logarithmic') {
const plotLabelsForceSimulation = forceSimulation()
.nodes(plotLabelForceNodes)
.force('collide', forceCollide(PLOT_LABEL_HEIGHT / 2))
Expand Down

0 comments on commit c80a0b6

Please sign in to comment.