Altair: Setting Custom Domain on datetime Axis

Created on 7 Jul 2018  路  11Comments  路  Source: altair-viz/altair

I'm trying to set the domain on my x-axis for a time based line/area chart. I've tried a few different things based on other issues raised here- but this is my closest result:

y = list(range(0,100))
x = [datetime.datetime(2017, 1, 1) + datetime.timedelta(days=i) for i in y]

# What I wanted to do
# domain = [datetime.datetime(2017,1,5), datetime.datetime(2017,1,30)]
# Per https://github.com/altair-viz/altair/issues/558
# domain_seconds = [(x - datetime.datetime(1970,1,1)).total_seconds() for x in domain]
# Per comment: https://github.com/altair-viz/altair/issues/187#issuecomment-244126070
domain_pd = pd.to_datetime(['2016-12-15', '2017-01-15']).astype(int) / 10 ** 6

time_data = pd.DataFrame({
    'x': x,
    'y': y
})

alt.Chart(time_data).mark_line().encode(
    alt.X('x:T', timeUnit='yearmonthdate', scale=alt.Scale(domain=list(domain_pd))),
    alt.Y('y:Q')
).properties(
    title='Example chart'
)

image

As you can see, this didn't exactly work out. What I really want to do is pass in dates in the domain field and have the chart start and end on those dates. This should happen regardless of the presence of data. For my application there may or may not be data in the dates given. Am I passing in these dates in the right place?

FYI I'm running altair 2.1.0 in a jupyter notebook.

I'm tracking the following issues to try and resolve this:

question

Most helpful comment

Just thinking out loud... sorry for the multiple posts.

Maybe the best way to do this would be to make alt.DateTime accept a flexible range of inputs, so that the user could do

alt.Scale(domain=[alt.DateTime('2016-12-15'), alt.DateTime('2017-01-15')])

That seems concise and convenient, and also nice and explicit.

Edit: This would involve adding the following class to altair/vegalite/v2/api/py:

import altair as alt
import pandas as pd

class DateTime(alt.DateTime):
    @staticmethod
    def _shorthand_to_dict(shorthand):
        dt = pd.to_datetime(shorthand)
        return dict(year=dt.year, month=dt.month, date=dt.day,
                    hours=dt.hour, minutes=dt.minute, seconds=dt.second,
                    milliseconds=0.001 * dt.microsecond)

    def __init__(self, shorthand=alt.Undefined, **kwargs):
        if shorthand is not alt.Undefined:
            # TODO: warn if this involves overwriting
            kwargs.update(self._shorthand_to_dict(shorthand))
        super(alt.DateTime, self).__init__(**kwargs)

All 11 comments

It looks like the domain adjustment worked fine; you just need to also specify clip=True. See https://altair-viz.github.io/user_guide/customization.html#adjusting-axis-limits

Thanks! Although this isn't my favorite solution for setting time axis domain's it works. Do you have any plans to support [datetime.datetime(2017,1,5), datetime.datetime(2017,1,30)] as the passed in domain?

For the sake of those who search this issue, here is the completed graph:

y = list(range(0,100))
x = [datetime.datetime(2017, 1, 1) + datetime.timedelta(days=i) for i in y]

domain_pd = pd.to_datetime(['2016-12-15', '2017-01-15']).astype(int) / 10 ** 6

time_data = pd.DataFrame({
    'x': x,
    'y': y
})

alt.Chart(time_data).mark_line(clip=True).encode(
    alt.X('x:T', timeUnit='yearmonthdate', scale=alt.Scale(domain=list(domain_pd))),
    alt.Y('y:Q')
).properties(
    title='Example chart',
    width=800
)

image

Do you have any plans to support [datetime.datetime(2017,1,5), datetime.datetime(2017,1,30)]

No definite plans... but it's probably a good idea. It would involve overwriting the auto-generated alt.Scale class with a derived subclass of the same name that has special handling for the domain argument. We do that already with a couple other schema definitions.

Here's a slightly cleaner way to pass datetime data to Altair domains:

import altair as alt
import pandas as pd

def to_altair_datetime(dt):
    dt = pd.to_datetime(dt)
    return alt.DateTime(year=dt.year, month=dt.month, date=dt.day,
                        hours=dt.hour, minutes=dt.minute, seconds=dt.second,
                        milliseconds=0.001 * dt.microsecond)

data = pd.DataFrame({
    'x': pd.date_range('2017-01-01', freq='D', periods=100),
    'y': range(100)
})

domain = [to_altair_datetime('2016-12-15'),
          to_altair_datetime('2017-01-15')]

alt.Chart(data).mark_line(clip=True).encode(
    alt.X('x:T', timeUnit='yearmonthdate', scale=alt.Scale(domain=domain)),
    alt.Y('y:Q')
).properties(
    title='Example chart',
    width=800
)

The tricky thing about doing this automatically is trying to infer when the scale domain should be converted to dates (when scale is converted to dict, it doesn't have any information about the type of the variable it's referring to). Making it context-aware would require some fundamental changes in how Altair generates plot specifications.

Just thinking out loud... sorry for the multiple posts.

Maybe the best way to do this would be to make alt.DateTime accept a flexible range of inputs, so that the user could do

alt.Scale(domain=[alt.DateTime('2016-12-15'), alt.DateTime('2017-01-15')])

That seems concise and convenient, and also nice and explicit.

Edit: This would involve adding the following class to altair/vegalite/v2/api/py:

import altair as alt
import pandas as pd

class DateTime(alt.DateTime):
    @staticmethod
    def _shorthand_to_dict(shorthand):
        dt = pd.to_datetime(shorthand)
        return dict(year=dt.year, month=dt.month, date=dt.day,
                    hours=dt.hour, minutes=dt.minute, seconds=dt.second,
                    milliseconds=0.001 * dt.microsecond)

    def __init__(self, shorthand=alt.Undefined, **kwargs):
        if shorthand is not alt.Undefined:
            # TODO: warn if this involves overwriting
            kwargs.update(self._shorthand_to_dict(shorthand))
        super(alt.DateTime, self).__init__(**kwargs)

Current master of vega-lite supports text dates in domain definition I think it will solve the question with less effort from Altair :) ER fixed PR

But proposed DateTime is cool - that what I was looking for.

Oh, cool, I didn't realize that (despite having commented on that PR :smile:)

Since that will soon be supported in Altair, I think we should avoid adding this extra DateTime logic on the Python side.

Nice to see vega-lite will be making this a little easier. If I understand right after vega-lite incorporates this into a release (is this in 2.6.0?) altair can handle:

domain=['2016-12-15', '2017-01-15']
# or
domain=[datetime.date(2016, 12, 15).isoformat(), datetime.date(2017, 1, 15).isoformat()]

I'm looking forward to trying this out.

is this in 2.6.0?

Yep, this is already in 2.6.0. :)

Confirmed, this is working in altair mast branch now. I can successfully use str isodates as domain args.

@jakevdp I'm not sure what your process for handling issues is. Would you like to close this now or wait until documentation is made or a release is performed?

Resolved in master with 087e490ba9f17d17d8a801c72c480a8a6b982c63

Was this page helpful?
0 / 5 - 0 ratings

Related issues

floringogianu picture floringogianu  路  3Comments

HalukaMB picture HalukaMB  路  3Comments

dzonimn picture dzonimn  路  3Comments

nielsmde picture nielsmde  路  4Comments

breadbaron picture breadbaron  路  4Comments