Offer to come, dig friends take it! I am participating in the 2022 Spring Recruit Punch card activity. Click here for details.


See the Data Visualization column for a series of articles


Custom chronograph

Sometimes you need to do simple customizations based on the various types of intervals provided by D3. You can use the interval.filter(test) method, which returns a (customized) interval

The interval.filter(test) method takes a test function that takes a Date object as an argument, and only returns truthy if the test function returns truthy. This Date object can be used as a “potentially harvestable point in time” for a customized interval, similar to the arr.filter() method for arrays, to filter elements.

For example, use d3.timeday. range(start, end) to collect time points every day in the time range, and finally generate an array containing a series of Date objects. If the interval d3. TimeDay is first customized, it can only collect dates ending in 1

// Only the dates ending in 1, such as 1st, 11th, 21th, and 31th of each month, can be collected
d3.timeDay.filter(d= > (d.getDate() - 1) % 10= = =0)
Copy the code

The customized interval returned by ⚠️ interval.filter() does not have the interval.count() method

💡 For the need to collect time at specific intervals/steps, D3 provides a simpler method interval.every(step), which is equivalent to the syntactic sugar of interval.filter() (but does not need to set up the complex test function, Specify step), and return a (customized) interval

d3.timeDay.every(2).range(new Date(2015.0.1), new Date(2015.0.7));
// [2015-01-01T00:00, 2015-01-03T00:00, 2015-01-05T00:00]
Copy the code

Although interval.every() and interval.range() are similar, their roles and scenarios are different.

  • Interval.every () returns a custom interval that requires further calls to interval’s other methods (which modify the time) to return the Date object; Interval.range () returns an array of Date objects

  • Interval. Every (step) has the step size for the parent of the interval’s time scale, for example, d3.timeminute. Every (15) because the step size is 15, that is, the time point is collected every 15 minutes, Therefore, the “potential collection time points” of the returned interval are :00, :15, :30 and so on. The starting point of the collection is fixed. Since the time scale is minutes, its parent is hours, so it starts from the lower bound of the parent time scale, namely 0 minutes 0 seconds. The step size of interval.range(start, end, step) is specific to the time scale of the interval itself, that is, the start point of collection is controlled by start, and from greater than (or equal to) the start point, Start from the lower bound of the time scale of interval, and collect data every specific step.

    console.log("--- within month ---");
    const startDateWithinMonth = new Date("2022-05-02T10:12:12Z");
    const endDateWithinMonth = new Date("2022-05-10T14:12:12Z");
    
    // Use an unmodified interval
    // The interval time scale is days
    // The collection time step is 2, that is, every other day
    const rangeArrWithinMonth = d3.utcDay.range(
      startDateWithinMonth,
      endDateWithinMonth,
      2
    );
    
    // First customize interval
    // The parent time scale is month
    // Set the step size to 2
    // Set the step size of collection time point to 2, that is, collection every other day
    const filterInterval = d3.utcDay.every(2);
    // Then use the customized interval for time collection
    const filterArrWithinMonth = filterInterval.range(
      startDateWithinMonth,
      endDateWithinMonth
    );
    
    // When the time range starts and ends within a month, the results are sometimes the same (see below for an explanation of how interval.every() works).
    console.log("within month range: ", rangeArrWithinMonth);
    console.log("within month filter range: ", filterArrWithinMonth);
    
    /*
    * --- outside month ---
    */
    
    console.log("--- outside month ---");
    // The situation is different for time ranges that span months
    // Because interval.every(step) sets the step size to the parent's time scale first
    // D3.timeday.every (2) can be read as the first day of every month (parent time scale) and the time is collected every other day, i.e. the 1st, 3rd, 5th of every month... The 27th, 29th, and 31st (if there is a 31st in the month) are all fixed
    D3.timeday. range(start, end, 2) is determined by the start parameter
    const startDateOutsideMonth = new Date("2022-05-28T10:12:12Z");
    const endDateOutsideMonth = new Date("2022-06-06T14:12:12Z");
    
    const rangeArrOutsideMonth = d3.utcDay.range(
      startDateOutsideMonth,
      endDateOutsideMonth,
      2
    );
    
    const filterArrOutsideMonth = filterInterval.range(
      startDateOutsideMonth,
      endDateOutsideMonth
    );
    
    console.log("outside month range: ", rangeArrOutsideMonth);
    console.log("outside month filter range: ", filterArrOutsideMonth);
    Copy the code

    Here is the output from the console, and you can see the Codepen for a code demonstration

    Therefore, interval is set by using interval.every(step), and then a series of Date objects are obtained by using the customized interval.range(). The interval between them may be uneven

    💡 You can use interval.every(step) to set the interval of the collection time from the parent time scale, or set the step of the collection time from the time scale of the interval when calling interval.range(start, end, step). Interval becomes an array of discrete time points. Then, the step size set in interval.range(start, end, step) is set to select elements in this array

    const start = new Date("2022-05-02T10:12:12Z");
    const end = new Date("2022-05-10T14:12:12Z");
    
    const filterInterval = d3.utcDay.every(2);
    
    const rangeArr = filterInterval.range(start, end);
    
    const rangeArrWithStep = filterInterval.range(start, end, 2);
    
    console.log('range array: ', rangeArr);
    console.log('range array with step: ', rangeArrWithStep)
    Copy the code

    The console output is as follows

    ⚠️ as with the interval.filter() method, the custom interval returned by interval.every() does not have the interval.count() method

TimeInterval (floor, offset[, count[, field]]) Finally, the method returns a deeply customized interval:

The first argument, floor, is a function that accepts a Date object that revises the time scale and returns the lower bound (a Date object). The corresponding methods interval([date]), interval.ceil(date), interval.round(date) also behave accordingly.

The second argument, offset, is a function that takes a Date object and the offset step (which should be an integer), which offsets time by step in the current time scale and returns the offset Date object

The third (optional) argument count is a function that takes two arguments (modified to the lower edge of the corresponding time scale) representing the start (not included) and end (included) points of the time range, The function is to calculate how many intervals there are in the (start, end) time range on the corresponding time scale. ⚠️ If this parameter is not set, the returned deeply customized interval does not have the interval.count() and interval.every() methods

The fourth (optional) argument, field, is a function that takes a Date object (modified to the lower boundary of the appropriate time scale) and returns the value of a specific field of the Date object, such as the d3.timeDay timer provided by D3 by default, The field function is date => date.getDate() -1 returns the number of days in the month of the date object. This method defines the behavior of interval.every(), so the constraints of its parent’s time scale should be considered when modifying Date and returning a specific field value of the Date object. ⚠️ If this parameter is not set, the number of intervals since January 1, 1970 in the current time scale (in UTC) is returned by default

// Create a custom timer
// It simply removes the second, setting the second field of the Date object to 0
// The time scale of its offset behavior is minute
const customInterval = d3.timeInterval(
  (date) = > {
    date.setSeconds(0.0);
  },
  (date, step) = >{ date.setMinutes(date.getMinutes() + step); });const date = new Date("The 2022-02-06 T13:12:. 123 z");
console.log("original date: ", date); / / the original date: 2022-02-06 T13: true. The 123 z

const floorTime = customInterval(date);
const offsetTime = customInterval.offset(date, 2);

console.log("floor date: ", floorTime); / / floor date: 2022-02-06 T13:12:00. 000 z
console.log("offset date: ", offsetTime); / / offset date: 2022-02-06 T13: then 123 z
Copy the code

💡 The code can be viewed at Codepen

Time scale

D3 provides a simple method for the generation of time scale

The method d3.timeTicks(start, stop, count) or d3.utcTicks(start, stop, count) can be adjusted within a given time range (both start and end points) based on the number of ticks needed to generate count, Generate a series of readable time objects 💡, similar to the d3.ticks method

Based on the distance between start and end, interval (

  • 1 second
  • 5 seconds
  • 15 seconds
  • 30 seconds
  • 1 minute
  • 5 minutes
  • 15 minutes
  • 30 minutes
  • 1 hour
  • 3 hours
  • 6 hours
  • 12 hours
  • 1 day
  • 2 days
  • 1 week
  • 1 month
  • 3 months
  • 1 year

💡 The above methods internally use the corresponding interval method interval.range to generate a series of Date objects as the timescale scale. If you want to know which interval is used internally, You can obtain the value by using d3.timeTickInterval(start, stop, count) and d3.utcTickInterval(start, stop, count)

💡 is also supported for smaller (milliseconds) and larger (years) intervals between start and end, and the scale value generation follows the rules of the D3.ticks method

start = new Date(Date.UTC(1970.2.1))
stop = new Date(Date.UTC(1996.2.19))
count = 4

d3.utcTicks(start, stop, count) // [1975-01-01, 1980-01-01, 1985-01-01, 1990-01-01, 1995-01-01]
Copy the code

💡 If the argument count is not a number and is directly an interval, the Date object is collected using the interval.range() method of the timer.