web: more register chart improvements

- show a background color for future and less-than-zero regions
- show points for transactions, not all line corners
- hovering over point shows balance, date, posted amount and transaction
- clicking a point scrolls towards that date
This commit is contained in:
Simon Michael 2014-07-18 16:45:46 -07:00
parent 470835adc2
commit f2d9c6e9c1
5 changed files with 144 additions and 60 deletions

View File

@ -75,6 +75,7 @@ module Hledger.Data.Amount (
mixed, mixed,
amounts, amounts,
filterMixedAmount, filterMixedAmount,
filterMixedAmountByCommodity,
normaliseMixedAmountPreservingFirstPrice, normaliseMixedAmountPreservingFirstPrice,
normaliseMixedAmountPreservingPrices, normaliseMixedAmountPreservingPrices,
-- ** arithmetic -- ** arithmetic
@ -429,6 +430,17 @@ amounts (Mixed as) = as
filterMixedAmount :: (Amount -> Bool) -> MixedAmount -> MixedAmount filterMixedAmount :: (Amount -> Bool) -> MixedAmount -> MixedAmount
filterMixedAmount p (Mixed as) = Mixed $ filter p as filterMixedAmount p (Mixed as) = Mixed $ filter p as
-- | Return an unnormalised MixedAmount containing exactly one Amount
-- with the specified commodity and the quantity of that commodity
-- found in the original. NB if Amount's quantity is zero it will be
-- discarded next time the MixedAmount gets normalised.
filterMixedAmountByCommodity :: Commodity -> MixedAmount -> MixedAmount
filterMixedAmountByCommodity c (Mixed as) = Mixed as'
where
as' = case filter ((==c) . acommodity) as of
[] -> [nullamt{acommodity=c}]
as'' -> [sum as'']
-- | Convert a mixed amount's component amounts to the commodity of their -- | Convert a mixed amount's component amounts to the commodity of their
-- assigned price, if any. -- assigned price, if any.
costOfMixedAmount :: MixedAmount -> MixedAmount costOfMixedAmount :: MixedAmount -> MixedAmount

View File

@ -11,9 +11,12 @@ a some base account. They are used by hledger-web.
module Hledger.Reports.TransactionsReports ( module Hledger.Reports.TransactionsReports (
TransactionsReport, TransactionsReport,
TransactionsReportItem, TransactionsReportItem,
triOrigTransaction,
triDate, triDate,
triAmount,
triBalance, triBalance,
triSimpleBalance, triCommodityAmount,
triCommodityBalance,
journalTransactionsReport, journalTransactionsReport,
accountTransactionsReport, accountTransactionsReport,
transactionsReportByCommodity transactionsReportByCommodity
@ -51,11 +54,12 @@ type TransactionsReportItem = (Transaction -- the original journal transaction,
,MixedAmount -- the running balance for the current account(s) after this transaction ,MixedAmount -- the running balance for the current account(s) after this transaction
) )
triDate (t,_,_,_,_,_) = tdate t triOrigTransaction (torig,_,_,_,_,_) = torig
triDate (_,tacct,_,_,_,_) = tdate tacct
triAmount (_,_,_,_,a,_) = a triAmount (_,_,_,_,a,_) = a
triBalance (_,_,_,_,_,a) = a triBalance (_,_,_,_,_,a) = a
triSimpleBalance (_,_,_,_,_,Mixed a) = case a of [] -> "0" triCommodityAmount c = filterMixedAmountByCommodity c . triAmount
(Amount{aquantity=q}):_ -> show q triCommodityBalance c = filterMixedAmountByCommodity c . triBalance
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -237,9 +241,5 @@ filterTransactionsReportByCommodity c (label,items) =
go bal ((t,t2,s,o,amt,_):is) = (t,t2,s,o,amt,bal'):go bal' is go bal ((t,t2,s,o,amt,_):is) = (t,t2,s,o,amt,bal'):go bal' is
where bal' = bal + amt where bal' = bal + amt
-- | Filter out all but the specified commodity from this amount.
filterMixedAmountByCommodity :: Commodity -> MixedAmount -> MixedAmount
filterMixedAmountByCommodity c (Mixed as) = Mixed $ filter ((==c). acommodity) as
------------------------------------------------------------------------------- -------------------------------------------------------------------------------

View File

@ -134,6 +134,7 @@ instance Yesod App where
addScript $ StaticR js_jquery_hotkeys_js addScript $ StaticR js_jquery_hotkeys_js
addScript $ StaticR js_jquery_flot_min_js addScript $ StaticR js_jquery_flot_min_js
addScript $ StaticR js_jquery_flot_time_min_js addScript $ StaticR js_jquery_flot_time_min_js
addScript $ StaticR js_jquery_flot_tooltip_min_js
toWidget [hamlet| \<!--[if lte IE 8]> <script type="text/javascript" src="@{StaticR js_excanvas_min_js}"></script> <![endif]--> |] toWidget [hamlet| \<!--[if lte IE 8]> <script type="text/javascript" src="@{StaticR js_excanvas_min_js}"></script> <![endif]--> |]
addStylesheet $ StaticR hledger_css addStylesheet $ StaticR hledger_css
addScript $ StaticR hledger_js addScript $ StaticR hledger_js

View File

@ -4,6 +4,7 @@ module Handler.RegisterR where
import Import import Import
import Data.List
import Data.Maybe import Data.Maybe
import Safe import Safe
@ -105,31 +106,69 @@ registerChartHtml percommoditytxnreports =
<div#register-chart style="width:85%; height:150px; margin-bottom:1em; display:block;"> <div#register-chart style="width:85%; height:150px; margin-bottom:1em; display:block;">
<script type=text/javascript> <script type=text/javascript>
\$(document).ready(function() { \$(document).ready(function() {
var chartdiv = $('#register-chart'); var $chartdiv = $('#register-chart');
if (chartdiv.is(':visible')) { if ($chartdiv.is(':visible')) {
\$('#register-chart-label').text('#{charttitle}'); \$('#register-chart-label').text('#{charttitle}');
registerChart( var seriesData = [
chartdiv, $forall (c,(_,items)) <- percommoditytxnreports
[ /* we render each commodity using two series:
$forall (comm,(_,items)) <- percommoditytxnreports * one with extra data points added to show a stepped balance line */
{ {
data: [ data: [
$forall i <- reverse items $forall i <- reverse items
[#{dayToJsTimestamp $ triDate i}, #{triSimpleBalance i}], [
/* [] */ #{dayToJsTimestamp $ triDate i},
#{simpleMixedAmountQuantity $ triCommodityBalance c i},
], ],
label: '#{comm}', /* [] */
color: #{colorForCommodity comm}, ],
}, label: '#{c}',
] color: #{colorForCommodity c},
); lines: {
show: true,
steps: true,
},
points: {
show: false,
},
clickable: false,
hoverable: false,
},
/* and one with the original data, showing one clickable, hoverable point per transaction */
{
data: [
$forall i <- reverse items
[
#{dayToJsTimestamp $ triDate i},
#{simpleMixedAmountQuantity $ triCommodityBalance c i},
'#{show $ triCommodityAmount c i}',
'#{show $ triCommodityBalance c i}',
'#{concat $ intersperse "\\n" $ lines $ show $ triOrigTransaction i}',
],
/* [] */
],
label: '',
color: '#{colorForCommodity c}',
lines: {
show: false,
},
points: {
show: true,
color: '#{colorForCommodity c}',
},
},
]
var plot = registerChart($chartdiv, seriesData);
\$chartdiv.bind("plotclick", registerChartClick);
}; };
}); });
|] |]
-- [#{dayToJsTimestamp $ ltrace "\ndate" $ triDate i}, #{ltrace "balancequantity" $ simpleMixedAmountQuantity $ triCommodityBalance c i}, '#{ltrace "balance" $ show $ triCommodityBalance c i}, '#{ltrace "amount" $ show $ triCommodityAmount c i}''],
where where
charttitle = case maybe "" (fst.snd) $ headMay percommoditytxnreports charttitle = case maybe "" (fst.snd) $ headMay percommoditytxnreports
of "" -> "" of "" -> ""
s -> s++":" s -> s++":"
colorForCommodity = fromMaybe 0 . flip lookup commoditiesIndex colorForCommodity = fromMaybe 0 . flip lookup commoditiesIndex
commoditiesIndex = zip (map fst percommoditytxnreports) [0..] :: [(Commodity,Int)] commoditiesIndex = zip (map fst percommoditytxnreports) [0..] :: [(Commodity,Int)]
simpleMixedAmountQuantity = maybe 0 aquantity . headMay . amounts

View File

@ -32,65 +32,97 @@ $(document).ready(function() {
// REGISTER CHART // REGISTER CHART
function registerChart($container, series) { function registerChart($container, series) {
// https://github.com/flot/flot/blob/master/API.md // https://github.com/flot/flot/blob/master/API.md
return $container.plot( return $container.plot(
series, series,
{ /* general chart options */ { /* general chart options */
series: { // series: {
points: { // },
show: true, // yaxis: {
}, // /* ticks: 6, */
lines: { // },
show: true,
steps: true,
},
bars: {
// show: true,
// barWidth: 1000 * 60 * 60, // ms
},
},
yaxis: {
/* mode: "time", */
/* timeformat: "%y/%m/%d", */
/* ticks: 6, */
},
xaxis: { xaxis: {
mode: "time", mode: "time",
timeformat: "%Y/%m/%d" timeformat: "%Y/%m/%d"
/* ticks: 6, */ /* ticks: 6, */
}, },
grid: { grid: {
// clickable: true,
// hoverable: true,
// autoHighlight: true,
markings: markings:
function (axes) { function (axes) {
// console.log(axes); var now = Date.now();
// var markings = [];
// for (var x = Math.floor(axes.xaxis.min); x < axes.xaxis.max; x += 2)
// markings.push({ xaxis: { from: x, to: x + 1 } });
// midx = Math.floor(axes.xaxis.min + (axes.xaxis.max - axes.xaxis.min) / 2);
var now = Date.now();
var markings = [ var markings = [
// {
// xaxis: { to: now }, // past
// yaxis: { from: 0, to: 0 }, // =0
// color: '#d88',
// lineWidth:1
// },
{ {
xaxis: { from: now, to: now }, xaxis: { to: now }, // past
color: '#888', yaxis: { to: 0 }, // <0
lineWidth:1 color: '#ffdddd',
},
// {
// xaxis: { from: now, to: now }, // now
// color: '#bbb',
// },
{
xaxis: { from: now }, // future
yaxis: { from: 0 }, // >0
// color: '#dddddd',
color: '#e0e0e0',
}, },
{ {
yaxis: { from: 0, to: 0 }, xaxis: { from: now }, // future
color: '#bb0000', yaxis: { to: 0 }, // <0
lineWidth:1 // color: '#ddbbbb',
color: '#e8c8c8',
},
{
// xaxis: { from: now }, // future
yaxis: { from: 0, to: 0 }, // =0
color: '#bb0000',
lineWidth:1
}, },
]; ];
// console.log(markings); // console.log(markings);
return markings; return markings;
} },
hoverable: true,
autoHighlight: true,
clickable: true,
},
/* https://github.com/krzysu/flot.tooltip */
tooltip: true,
tooltipOpts: {
xDateFormat: "%Y/%m/%d",
content:
function(label, x, y, flotitem) {
var data = flotitem.series.data[flotitem.dataIndex];
return data[3]+" balance on %x after "+data[2]+" posted by transaction:<pre>"+data[4]+"</pre>";
},
onHover: function(flotitem, $tooltipel) {
$tooltipel.css('border-color',flotitem.series.color);
},
}, },
} }
).data("plot"); ).data("plot");
} }
function registerChartClick(ev, pos, item) {
if (item) {
var date = $.plot.dateGenerator(item.datapoint[0], {});
var dateid = $.plot.formatDate(date, '%Y-%m-%d');
$target = $('#'+dateid);
if ($target.length)
$('html, body').animate({
scrollTop: $target.offset().top
}, 1000);
}
}
//---------------------------------------------------------------------- //----------------------------------------------------------------------
// ADD FORM // ADD FORM