BUG: new converters for sub-second plotting #1599 · pandas-dev/pandas@d0d15b0 (original) (raw)

1

``

`-

from datetime import datetime

`

``

1

`+

from datetime import datetime, timedelta

`

2

2

`import datetime as pydt

`

3

3

`import numpy as np

`

4

4

``

``

5

`+

from dateutil.relativedelta import relativedelta

`

``

6

+

``

7

`+

import matplotlib

`

5

8

`import matplotlib.units as units

`

6

9

`import matplotlib.dates as dates

`

``

10

+

7

11

`from matplotlib.ticker import Formatter, AutoLocator, Locator

`

8

12

`from matplotlib.transforms import nonsingular

`

9

13

``

10

14

`import pandas.lib as lib

`

11

15

`import pandas.core.common as com

`

12

16

`from pandas.core.index import Index

`

13

17

``

``

18

`+

from pandas.tseries.index import date_range

`

14

19

`import pandas.tseries.tools as tools

`

15

20

`import pandas.tseries.frequencies as frequencies

`

16

21

`from pandas.tseries.frequencies import FreqGroup

`

`@@ -74,11 +79,14 @@ def init(self, locs):

`

74

79

`def call(self, x, pos=0):

`

75

80

`fmt = '%H:%M:%S'

`

76

81

`s = int(x)

`

77

``

`-

us = int((x - s) * 1e6)

`

``

82

`+

ms = int((x - s) * 1e3)

`

``

83

`+

us = int((x - s) * 1e6 - ms)

`

78

84

`m, s = divmod(s, 60)

`

79

85

`h, m = divmod(m, 60)

`

80

86

`if us != 0:

`

81

``

`-

fmt += '.%f'

`

``

87

`+

fmt += '.%6f'

`

``

88

`+

elif ms != 0:

`

``

89

`+

fmt += '.%3f'

`

82

90

`return pydt.time(h, m, s, us).strftime(fmt)

`

83

91

``

84

92

`### Period Conversion

`

`@@ -122,17 +130,7 @@ def _dt_to_float_ordinal(dt):

`

122

130

` preserving hours, minutes, seconds and microseconds. Return value

`

123

131

`` is a :func:float.

``

124

132

` """

`

125

``

-

126

``

`-

if hasattr(dt, 'tzinfo') and dt.tzinfo is not None:

`

127

``

`-

delta = dt.tzinfo.utcoffset(dt)

`

128

``

`-

if delta is not None:

`

129

``

`-

dt -= delta

`

130

``

-

131

``

`-

base = float(dt.toordinal())

`

132

``

`-

if hasattr(dt, 'hour'):

`

133

``

`-

base += (dt.hour/HOURS_PER_DAY + dt.minute/MINUTES_PER_DAY +

`

134

``

`-

dt.second/SECONDS_PER_DAY + dt.microsecond/MUSECONDS_PER_DAY

`

135

``

`-

)

`

``

133

`+

base = dates.date2num(dt)

`

136

134

`return base

`

137

135

``

138

136

`### Datetime Conversion

`

`@@ -160,6 +158,209 @@ def try_parse(values):

`

160

158

`return [try_parse(x) for x in values]

`

161

159

`return values

`

162

160

``

``

161

`+

@staticmethod

`

``

162

`+

def axisinfo(unit, axis):

`

``

163

`+

"""

`

``

164

`` +

Return the :class:~matplotlib.units.AxisInfo for unit.

``

``

165

+

``

166

`+

unit is a tzinfo instance or None.

`

``

167

`+

The axis argument is required but not used.

`

``

168

`+

"""

`

``

169

`+

tz = unit

`

``

170

+

``

171

`+

majloc = PandasAutoDateLocator(tz=tz)

`

``

172

`+

majfmt = PandasAutoDateFormatter(majloc, tz=tz)

`

``

173

`+

datemin = pydt.date(2000, 1, 1)

`

``

174

`+

datemax = pydt.date(2010, 1, 1)

`

``

175

+

``

176

`+

return units.AxisInfo( majloc=majloc, majfmt=majfmt, label='',

`

``

177

`+

default_limits=(datemin, datemax))

`

``

178

+

``

179

+

``

180

`+

class PandasAutoDateFormatter(dates.AutoDateFormatter):

`

``

181

+

``

182

`+

def init(self, locator, tz=None, defaultfmt='%Y-%m-%d'):

`

``

183

`+

dates.AutoDateFormatter.init(self, locator, tz, defaultfmt)

`

``

184

`+

matplotlib.dates._UTC has no _utcoffset called by pandas

`

``

185

`+

if self._tz is dates.UTC:

`

``

186

`+

self._tz._utcoffset = self._tz.utcoffset(None)

`

``

187

`+

self.scaled = {

`

``

188

`+

365.0 : '%Y',

`

``

189

`+

  1. : '%b %Y',

`

``

190

`+

1.0 : '%b %d %Y',

`

``

191

`+

  1. / 24. : '%H:%M:%S',

`

``

192

`+

  1. / 24. / 3600. / 1000. : '%H:%M:%S.%f'

`

``

193

`+

}

`

``

194

+

``

195

`+

def _get_fmt(self, x):

`

``

196

+

``

197

`+

scale = float( self._locator._get_unit() )

`

``

198

+

``

199

`+

fmt = self.defaultfmt

`

``

200

+

``

201

`+

for k in sorted(self.scaled):

`

``

202

`+

if k >= scale:

`

``

203

`+

fmt = self.scaled[k]

`

``

204

`+

break

`

``

205

+

``

206

`+

return fmt

`

``

207

+

``

208

`+

def call(self, x, pos=0):

`

``

209

`+

fmt = self._get_fmt(x)

`

``

210

`+

self._formatter = dates.DateFormatter(fmt, self._tz)

`

``

211

`+

return self._formatter(x, pos)

`

``

212

+

``

213

`+

class PandasAutoDateLocator(dates.AutoDateLocator):

`

``

214

+

``

215

`+

def get_locator(self, dmin, dmax):

`

``

216

`+

'Pick the best locator based on a distance.'

`

``

217

`+

delta = relativedelta(dmax, dmin)

`

``

218

+

``

219

`+

num_days = ((delta.years * 12.0) + delta.months * 31.0) + delta.days

`

``

220

`+

num_sec = (delta.hours * 60.0 + delta.minutes) * 60.0 + delta.seconds

`

``

221

`+

tot_sec = num_days * 86400. + num_sec

`

``

222

+

``

223

`+

if tot_sec < self.minticks:

`

``

224

`+

self._freq = -1

`

``

225

`+

locator = MilliSecondLocator(self.tz)

`

``

226

`+

locator.set_axis(self.axis)

`

``

227

+

``

228

`+

locator.set_view_interval(*self.axis.get_view_interval())

`

``

229

`+

locator.set_data_interval(*self.axis.get_data_interval())

`

``

230

`+

return locator

`

``

231

+

``

232

`+

return dates.AutoDateLocator.get_locator(self, dmin, dmax)

`

``

233

+

``

234

`+

def _get_unit(self):

`

``

235

`+

return MilliSecondLocator.get_unit_generic(self._freq)

`

``

236

+

``

237

`+

class MilliSecondLocator(dates.DateLocator):

`

``

238

+

``

239

`+

UNIT = 1. / (24 * 3600 * 1000)

`

``

240

+

``

241

`+

def init(self, tz):

`

``

242

`+

dates.DateLocator.init(self, tz)

`

``

243

`+

self._interval = 1.

`

``

244

+

``

245

`+

def _get_unit(self):

`

``

246

`+

return self.get_unit_generic(-1)

`

``

247

+

``

248

`+

@staticmethod

`

``

249

`+

def get_unit_generic(freq):

`

``

250

`+

unit = dates.RRuleLocator.get_unit_generic(freq)

`

``

251

`+

if unit < 0:

`

``

252

`+

return MilliSecondLocator.UNIT

`

``

253

`+

return unit

`

``

254

+

``

255

`+

def call(self):

`

``

256

`+

if no data have been set, this will tank with a ValueError

`

``

257

`+

try: dmin, dmax = self.viewlim_to_dt()

`

``

258

`+

except ValueError: return []

`

``

259

+

``

260

`+

if dmin>dmax:

`

``

261

`+

dmax, dmin = dmin, dmax

`

``

262

`+

delta = relativedelta(dmax, dmin)

`

``

263

+

``

264

`+

We need to cap at the endpoints of valid datetime

`

``

265

`+

try:

`

``

266

`+

start = dmin - delta

`

``

267

`+

except ValueError:

`

``

268

`+

start = _from_ordinal( 1.0 )

`

``

269

+

``

270

`+

try:

`

``

271

`+

stop = dmax + delta

`

``

272

`+

except ValueError:

`

``

273

`+

The magic number!

`

``

274

`+

stop = _from_ordinal( 3652059.9999999 )

`

``

275

+

``

276

`+

nmax, nmin = dates.date2num((dmax, dmin))

`

``

277

+

``

278

`+

num = (nmax - nmin) * 86400 * 1000

`

``

279

`+

max_millis_ticks = 6

`

``

280

`+

for interval in [1, 10, 50, 100, 200, 500]:

`

``

281

`+

if num <= interval * (max_millis_ticks - 1):

`

``

282

`+

self._interval = interval

`

``

283

`+

break

`

``

284

`+

else:

`

``

285

`+

We went through the whole loop without breaking, default to 1

`

``

286

`+

self._interval = 1000.

`

``

287

+

``

288

`+

estimate = (nmax - nmin) / (self._get_unit() * self._get_interval())

`

``

289

+

``

290

`+

if estimate > self.MAXTICKS * 2:

`

``

291

`+

raise RuntimeError(('MillisecondLocator estimated to generate %d '

`

``

292

`+

'ticks from %s to %s: exceeds Locator.MAXTICKS'

`

``

293

`+

'* 2 (%d) ') %

`

``

294

`+

(estimate, dmin, dmax, self.MAXTICKS * 2))

`

``

295

+

``

296

`+

freq = '%dL' % self._get_interval()

`

``

297

`+

tz = self.tz.tzname(None)

`

``

298

`+

st = _from_ordinal(dates.date2num(dmin)) # strip tz

`

``

299

`+

ed = _from_ordinal(dates.date2num(dmax))

`

``

300

`+

all_dates = date_range(start=st, end=ed, freq=freq, tz=tz).asobject

`

``

301

+

``

302

`+

try:

`

``

303

`+

if len(all_dates) > 0:

`

``

304

`+

locs = self.raise_if_exceeds(dates.date2num(all_dates))

`

``

305

`+

return locs

`

``

306

`+

except Exception, e:

`

``

307

`+

pass

`

``

308

+

``

309

`+

lims = dates.date2num([dmin, dmax])

`

``

310

`+

return lims

`

``

311

+

``

312

`+

def _get_interval(self):

`

``

313

`+

return self._interval

`

``

314

+

``

315

`+

def autoscale(self):

`

``

316

`+

"""

`

``

317

`+

Set the view limits to include the data range.

`

``

318

`+

"""

`

``

319

`+

dmin, dmax = self.datalim_to_dt()

`

``

320

`+

if dmin>dmax:

`

``

321

`+

dmax, dmin = dmin, dmax

`

``

322

+

``

323

`+

delta = relativedelta(dmax, dmin)

`

``

324

+

``

325

`+

We need to cap at the endpoints of valid datetime

`

``

326

`+

try:

`

``

327

`+

start = dmin - delta

`

``

328

`+

except ValueError:

`

``

329

`+

start = _from_ordinal(1.0)

`

``

330

+

``

331

`+

try:

`

``

332

`+

stop = dmax + delta

`

``

333

`+

except ValueError:

`

``

334

`+

The magic number!

`

``

335

`+

stop = _from_ordinal( 3652059.9999999 )

`

``

336

+

``

337

`+

dmin, dmax = self.datalim_to_dt()

`

``

338

+

``

339

`+

vmin = dates.date2num(dmin)

`

``

340

`+

vmax = dates.date2num(dmax)

`

``

341

+

``

342

`+

return self.nonsingular(vmin, vmax)

`

``

343

+

``

344

+

``

345

`+

def _from_ordinal(x, tz=None):

`

``

346

`+

ix = int(x)

`

``

347

`+

dt = datetime.fromordinal(ix)

`

``

348

`+

remainder = float(x) - ix

`

``

349

`+

hour, remainder = divmod(24*remainder, 1)

`

``

350

`+

minute, remainder = divmod(60*remainder, 1)

`

``

351

`+

second, remainder = divmod(60*remainder, 1)

`

``

352

`+

microsecond = int(1e6*remainder)

`

``

353

`+

if microsecond<10: microsecond=0 # compensate for rounding errors

`

``

354

`+

dt = datetime(dt.year, dt.month, dt.day, int(hour), int(minute),

`

``

355

`+

int(second), microsecond)

`

``

356

`+

if tz is not None:

`

``

357

`+

dt = dt.astimezone(tz)

`

``

358

+

``

359

`+

if microsecond > 999990: # compensate for rounding errors

`

``

360

`+

dt += timedelta(microseconds = 1e6 - microsecond)

`

``

361

+

``

362

`+

return dt

`

``

363

+

163

364

`### Fixed frequency dynamic tick locators and formatters

`

164

365

``

165

366

`##### -------------------------------------------------------------------------

`

`@@ -717,4 +918,3 @@ def call(self, x, pos=0):

`

717

918

`fmt = self.formatdict.pop(x, '')

`

718

919

`return Period(ordinal=int(x), freq=self.freq).strftime(fmt)

`

719

920

``

720

``

-