Merge pull request #557 from MrConcept/notifications

XP Changes, Better Notifications and Fixes!
This commit is contained in:
Tyler Renelle 2013-03-02 16:33:09 -08:00
commit e4b8742320
10 changed files with 231 additions and 154 deletions

54
src/app/algos.coffee Normal file
View file

@ -0,0 +1,54 @@
MODIFIER = .02
module.exports.tnl = (level) ->
return (Math.pow(level,2)*10)+(level*10)+80
###
Calculates Exp modificaiton based on level and weapon strength
{value} task.value for exp gain
{weaponStrength) weapon strength
{level} current user level
###
module.exports.expModifier = (value, weaponStrength, level) ->
levelModifier = (level-1) * MODIFIER
weaponModifier = weaponStrength / 100
strength = 1 + weaponModifier + levelModifier
return value * strength
###
Calculates HP modification based on level and armor defence
{value} task.value for hp loss
{armorDefense} defense from armor
{helmDefense} defense from helm
{level} current user level
###
module.exports.hpModifier = (value, armorDefense, helmDefense, shieldDefense, level) ->
levelModifier = (level-1) * MODIFIER
armorModifier = (armorDefense + helmDefense + shieldDefense) / 100
defense = 1 - levelModifier + armorModifier
return value * defense
###
Future use
###
module.exports.gpModifier = (value, modifier) ->
return value * modifier
###
Calculates the next task.value based on direction
Uses a capped inverse log y=.95^x, y>= -5
{currentValue} the current value of the task
{direction} up or down
###
module.exports.taskDeltaFormula = (currentValue, direction) ->
if direction is 'up'
delta = Math.max(Math.pow(0.95,currentValue),0.25)
else
delta = -Math.min(Math.pow(0.95,currentValue),5)
#sign = if (direction is 'up') then 1 else -1
#delta = Math.pow(0.95,currentValue) * sign
#if delta < -5 then delta = -5
#console.log("CurrentValue: " + currentValue + " delta: " + delta)
#delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign)
return delta

View file

@ -1,12 +1,14 @@
_ = require 'underscore'
moment = require 'moment'
#algos = require './algos'
module.exports.restoreRefs = restoreRefs = (model) ->
# tnl function
model.fn '_tnl', '_user.stats.lvl', (lvl) ->
# see https://github.com/lefnire/habitrpg/issues/4
# also update in scoring.coffee. TODO create a function accessible in both locations
(lvl*100)/5
#TODO find a method of calling algos.tnl()
10*Math.pow(lvl,2)+(lvl*10)+80
#refLists
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
@ -176,7 +178,7 @@ setupGrowlNotifications = (model) ->
return if user.get('stats.lvl') == 0
$.bootstrapGrowl html,
ele: '#notification-area',
type: type # (null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl')
type: type # (null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl','death')
top_offset: 20
align: 'right' # ('left', 'right', or 'center')
width: 250 # (integer, or 'auto')
@ -189,21 +191,34 @@ setupGrowlNotifications = (model) ->
num = captures - args
rounded = Math.abs(num.toFixed(1))
if num < 0
statsNotification "<i class='icon-heart'></i> -#{rounded} HP", 'hp' # lost hp from purchase
statsNotification "<i class='icon-heart'></i> - #{rounded} HP", 'hp' # lost hp from purchase
else if num > 0
statsNotification "<i class='icon-heart'></i> + #{rounded} HP", 'hp' # gained hp from potion/level?
user.on 'set', 'stats.exp', (captures, args, isLocal, silent) ->
num = captures - args
rounded = Math.abs(num.toFixed(1))
if num < 0 and not silent
statsNotification "<i class='icon-star'></i> - #{rounded} XP", 'xp'
else if num > 0
statsNotification "<i class='icon-star'></i> + #{rounded} XP", 'xp'
user.on 'set', 'stats.gp', (captures, args) ->
num = captures - args
rounded = Math.abs(num.toFixed(1))
# made purchase
if num < 0
# FIXME use 'warning' when unchecking an accidently completed daily/todo, and notify of exp too
statsNotification "<i class='icon-star'></i> -#{rounded} GP", 'gp'
# gained gp (and thereby exp)
else if num > 0
num = Math.abs(num)
statsNotification "<i class='icon-star'></i> +#{rounded} XP", 'xp'
statsNotification "<i class='icon-gp'></i> +#{rounded} GP", 'gp'
absolute = Math.abs(num)
gold = Math.floor(absolute)
silver = Math.floor((absolute-gold)*100)
sign = if num < 0 then '-' else '+'
if gold and silver > 0
statsNotification "#{sign} #{gold} <i class='icon-gold'></i> #{silver} <i class='icon-silver'></i>", 'gp'
else if gold > 0
statsNotification "#{sign} #{gold} <i class='icon-gold'></i>", 'gp'
else if silver > 0
statsNotification "#{sign} #{silver} <i class='icon-silver'></i>", 'gp'
user.on 'set', 'stats.lvl', (captures, args) ->
if captures > args
statsNotification('<i class="icon-chevron-up"></i> Level Up!', 'lvl')
if captures is 1 and args is 0
statsNotification '<i class="icon-death"></i> You died!', 'death'
else
statsNotification '<i class="icon-chevron-up"></i> Level Up!', 'lvl'

View file

@ -221,6 +221,7 @@ module.exports.BatchUpdate = BatchUpdate = (model) ->
commit: ->
model._dontPersist = false
# some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js
# pass true if we have levelled to supress xp notification
user.set "update__", updates
transactionInProgress = false
updates = {}

View file

@ -11,3 +11,12 @@ module.exports.app = (appExports, model) ->
appExports.cheat = ->
user.incr 'stats.exp', 20
user.incr 'stats.gp', 1000
appExports.reset = ->
user.set 'stats.exp', 0
user.set 'stats.lvl', 0
user.set 'stats.gp', 0
user.set 'items.weapon', 0
user.set 'items.armor', 0
user.set 'items.head', 0
user.set 'items.shield', 0

View file

@ -14,6 +14,9 @@ module.exports.viewHelpers = (view) ->
view.fn "round", (num) ->
Math.round num
view.fn "floor", (num) ->
Math.floor num
view.fn "lt", (a, b) ->
a < b

View file

@ -2,37 +2,37 @@ _ = require 'underscore'
items = module.exports.items =
weapon: [
{index: 0, text: "Training Sword", classes: "weapon_0", notes:'Training weapon.', modifier: 0.00, value:0}
{index: 1, text: "Sword", classes:'weapon_1', notes:'Increases experience gain by 3%.', modifier: 0.03, value:20}
{index: 2, text: "Axe", classes:'weapon_2', notes:'Increases experience gain by 6%.', modifier: 0.06, value:30}
{index: 3, text: "Morningstar", classes:'weapon_3', notes:'Increases experience gain by 9%.', modifier: 0.09, value:45}
{index: 4, text: "Blue Sword", classes:'weapon_4', notes:'Increases experience gain by 12%.', modifier: 0.12, value:65}
{index: 5, text: "Red Sword", classes:'weapon_5', notes:'Increases experience gain by 15%.', modifier: 0.15, value:90}
{index: 6, text: "Golden Sword", classes:'weapon_6', notes:'Increases experience gain by 18%.', modifier: 0.18, value:120}
{index: 0, text: "Training Sword", classes: "weapon_0", notes:'Training weapon.', strength: 0, value:0}
{index: 1, text: "Sword", classes:'weapon_1', notes:'Increases experience gain by 3%.', strength: 3, value:20}
{index: 2, text: "Axe", classes:'weapon_2', notes:'Increases experience gain by 6%.', strength: 6, value:30}
{index: 3, text: "Morningstar", classes:'weapon_3', notes:'Increases experience gain by 9%.', strength: 9, value:45}
{index: 4, text: "Blue Sword", classes:'weapon_4', notes:'Increases experience gain by 12%.', strength: 12, value:65}
{index: 5, text: "Red Sword", classes:'weapon_5', notes:'Increases experience gain by 15%.', strength: 15, value:90}
{index: 6, text: "Golden Sword", classes:'weapon_6', notes:'Increases experience gain by 18%.', strength: 18, value:120}
]
armor: [
{index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', modifier: 0.00, value:0}
{index: 1, text: "Leather Armor", classes: 'armor_1', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:30}
{index: 2, text: "Chain Mail", classes: 'armor_2', notes:'Decreases HP loss by 6%.', modifier: 0.06, value:45}
{index: 3, text: "Plate Mail", classes: 'armor_3', notes:'Decreases HP loss by 7%.', modifier: 0.07, value:65}
{index: 4, text: "Red Armor", classes: 'armor_4', notes:'Decreases HP loss by 8%.', modifier: 0.08, value:90}
{index: 5, text: "Golden Armor", classes: 'armor_5', notes:'Decreases HP loss by 10%.', modifier: 0.1, value:120}
{index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', defense: 0, value:0}
{index: 1, text: "Leather Armor", classes: 'armor_1', notes:'Decreases HP loss by 4%.', defense: 4, value:30}
{index: 2, text: "Chain Mail", classes: 'armor_2', notes:'Decreases HP loss by 6%.', defense: 6, value:45}
{index: 3, text: "Plate Mail", classes: 'armor_3', notes:'Decreases HP loss by 7%.', defense: 7, value:65}
{index: 4, text: "Red Armor", classes: 'armor_4', notes:'Decreases HP loss by 8%.', defense: 8, value:90}
{index: 5, text: "Golden Armor", classes: 'armor_5', notes:'Decreases HP loss by 10%.', defense: 10, value:120}
]
head: [
{index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', modifier: 0.00, value:0}
{index: 1, text: "Leather Helm", classes: 'head_1', notes:'Decreases HP loss by 2%.', modifier: 0.02, value:15}
{index: 2, text: "Chain Coif", classes: 'head_2', notes:'Decreases HP loss by 3%.', modifier: 0.03, value:25}
{index: 3, text: "Plate Helm", classes: 'head_3', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:45}
{index: 4, text: "Red Helm", classes: 'head_4', notes:'Decreases HP loss by 5%.', modifier: 0.05, value:60}
{index: 5, text: "Golden Helm", classes: 'head_5', notes:'Decreases HP loss by 6%.', modifier: 0.06, value:80}
{index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', defense: 0, value:0}
{index: 1, text: "Leather Helm", classes: 'head_1', notes:'Decreases HP loss by 2%.', defense: 2, value:15}
{index: 2, text: "Chain Coif", classes: 'head_2', notes:'Decreases HP loss by 3%.', defense: 3, value:25}
{index: 3, text: "Plate Helm", classes: 'head_3', notes:'Decreases HP loss by 4%.', defense: 4, value:45}
{index: 4, text: "Red Helm", classes: 'head_4', notes:'Decreases HP loss by 5%.', defense: 5, value:60}
{index: 5, text: "Golden Helm", classes: 'head_5', notes:'Decreases HP loss by 6%.', defense: 6, value:80}
]
shield: [
{index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', modifier: 0.00, value:0}
{index: 1, text: "Wooden Shield", classes: 'shield_1', notes:'Decreases HP loss by 3%', modifier: 0.03, value:20}
{index: 2, text: "Buckler", classes: 'shield_2', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:35}
{index: 3, text: "Enforced Shield", classes: 'shield_3', notes:'Decreases HP loss by 5%.', modifier: 0.05, value:55}
{index: 4, text: "Red Shield", classes: 'shield_4', notes:'Decreases HP loss by 7%.', modifier: 0.07, value:70}
{index: 5, text: "Golden Shield", classes: 'shield_5', notes:'Decreases HP loss by 8%.', modifier: 0.08, value:90}
{index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', defense: 0, value:0}
{index: 1, text: "Wooden Shield", classes: 'shield_1', notes:'Decreases HP loss by 3%', defense: 3, value:20}
{index: 2, text: "Buckler", classes: 'shield_2', notes:'Decreases HP loss by 4%.', defense: 4, value:35}
{index: 3, text: "Enforced Shield", classes: 'shield_3', notes:'Decreases HP loss by 5%.', defense: 5, value:55}
{index: 4, text: "Red Shield", classes: 'shield_4', notes:'Decreases HP loss by 7%.', defense: 7, value:70}
{index: 5, text: "Golden Shield", classes: 'shield_5', notes:'Decreases HP loss by 8%.', defense: 8, value:90}
]
potion: {type: 'potion', text: "Potion", notes: "Recover 15 HP", value: 25, classes: 'potion'}
reroll: {type: 'reroll', text: "Re-Roll", classes: 'reroll', notes: "Resets your tasks. When you're struggling and everything's red, use for a clean slate.", value:0 }
@ -169,7 +169,7 @@ module.exports.app = (appExports, model) ->
return unless captures == true
html = """
<div class='item-store-popover'>
<img src='/vendor/BrowserQuest/client/img/1chest.png' />
<img src='/vendor/BrowserQuest/client/img/1/chest.png' />
Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information.
<a href='#' onClick="$('ul.items').popover('hide');return false;">[Close]</a>
</div>

View file

@ -5,109 +5,17 @@ helpers = require './helpers'
browser = require './browser'
character = require './character'
items = require './items'
algos = require './algos'
module.exports.Scoring = (model) ->
MODIFIER = .02 # each new level, armor, weapon add 2% modifier (this mechanism will change)
MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change)
user = model.at '_user'
###
Calculates Exp & GP modification based on weapon & lvl
{value} task.value for gain
{modifiers} may manually pass in stats as {weapon, exp}. This is used for testing
###
expModifier = (value, modifiers = {}) ->
weapon = modifiers.weapon || user.get('items.weapon')
lvl = modifiers.lvl || user.get('stats.lvl')
dmg = items.items.weapon[weapon].modifier # each new weapon increases exp gain
dmg += (lvl-1) * MODIFIER # same for lvls
modified = value + (value * dmg)
return modified
###
Calculates HP-loss modification based on armor & lvl
{value} task.value which is hurting us
{modifiers} may manually pass in modifier as {armor, lvl}. This is used for testing
###
hpModifier = (value, modifiers = {}) ->
armor = modifiers.armor || user.get('items.armor')
head = modifiers.head || user.get('items.head')
shield = modifiers.shield || user.get('items.shield')
lvl = modifiers.lvl || user.get('stats.lvl')
ac = items.items.armor[armor].modifier + items.items.head[head].modifier + items.items.shield[shield].modifier # each new armor decreases HP loss
ac += (lvl-1) * MODIFIER # same for lvls
modified = value - (value * ac)
return modified
###
Calculates the next task.value based on direction
For negative values, use a line: something like y=-.1x+1
For positibe values, taper off with inverse log: y=.9^x
Would love to use inverse log for the whole thing, but after 13 fails it hits infinity. Revisit this formula later
{currentValue} the current value of the task, determines it's next value
{direction} 'up' or 'down'
###
taskDeltaFormula = (currentValue, direction) ->
sign = if (direction == "up") then 1 else -1
delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign)
return delta
###
Updates user stats with new stats. Handles death, leveling up, etc
{stats} new stats
{update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
###
updateStats = (newStats, batch) ->
obj = batch.obj()
# if user is dead, dont do anything
return if obj.stats.lvl == 0
if newStats.hp?
# Game Over
if newStats.hp <= 0
obj.stats.lvl = 0 # signifies dead
obj.stats.hp = 0
return
else
obj.stats.hp = newStats.hp
if newStats.exp?
# level up & carry-over exp
tnl = model.get '_tnl'
if newStats.exp >= tnl
newStats.exp -= tnl
obj.stats.lvl++
obj.stats.hp = 50
obj.stats.exp = newStats.exp
# Set flags when they unlock features
if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1)
batch.set 'flags.customizationsNotification', true
obj.flags.customizationsNotification = true
if !obj.flags.itemsEnabled and obj.stats.lvl >= 2
# Set to object, then also send to browser right away to get model.on() subscription notification
batch.set 'flags.itemsEnabled', true
obj.flags.itemsEnabled = true
if !obj.flags.partyEnabled and obj.stats.lvl >= 3
batch.set 'flags.partyEnabled', true
obj.flags.partyEnabled = true
if !obj.flags.petsEnabled and obj.stats.lvl >= 4
batch.set 'flags.petsEnabled', true
obj.flags.petsEnabled = true
if newStats.gp?
#FIXME what was I doing here? I can't remember, gp isn't defined
gp = 0.0 if (!gp? or gp<0)
obj.stats.gp = newStats.gp
# {taskId} task you want to score
# {direction} 'up' or 'down'
# {times} # times to call score on this task (1 unless cron, usually)
# {update} if we're running updates en-mass (eg, cron on server) pass in userObj
score = (taskId, direction, times, batch, cron) ->
commit = false
unless batch?
commit = true
@ -136,23 +44,30 @@ module.exports.Scoring = (model) ->
# Each iteration calculate the delta (nextDelta), which is then accumulated in delta
# (aka, the total delta). This weirdness won't be necessary when calculating mathematically
# rather than iteratively
nextDelta = taskDeltaFormula(value, direction)
nextDelta = algos.taskDeltaFormula(value, direction)
value += nextDelta if adjustvalue
delta += nextDelta
addPoints = ->
modified = expModifier(delta)
exp += modified
gp += modified
level = user.get('stats.lvl')
weaponStrength = items.items.weapon[user.get('items.weapon')].strength
modified = algos.expModifier(delta,weaponStrength,level)
exp += modified*10
gp += delta
subtractPoints = ->
modified = hpModifier(delta)
level = user.get('stats.lvl')
armorDefense = items.items.armor[user.get('items.armor')].defense
helmDefense = items.items.head[user.get('items.head')].defense
shieldDefense = items.items.shield[user.get('items.shield')].defense
modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level)
hp += modified
switch type
when 'habit'
# Don't adjust values for habits that don't have both + and -
adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true
#adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true
adjustvalue = true;
calculateDelta(adjustvalue)
# Add habit value to habit-history (if different)
if (delta > 0) then addPoints() else subtractPoints()
@ -163,15 +78,20 @@ module.exports.Scoring = (model) ->
batch.set "#{taskPath}.history", taskObj.history
when 'daily'
calculateDelta()
#calculateDelta()
if cron? # cron
calculateDelta()
subtractPoints()
else
calculateDelta(false)
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'todo'
calculateDelta()
unless cron? # don't touch stats on cron
if cron? #cron
calculateDelta()
#don't touch stats on cron
else
calculateDelta(false)
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'reward'
@ -194,10 +114,64 @@ module.exports.Scoring = (model) ->
newStats = _.clone batch.obj().stats
_.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key]
batch.setStats(newStats)
# batch.setStats()
# batch.setStats()
batch.commit()
return delta
###
Updates user stats with new stats. Handles death, leveling up, etc
{stats} new stats
{update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
###
updateStats = (newStats, batch) ->
obj = batch.obj()
# if user is dead, dont do anything
return if obj.stats.lvl == 0
if newStats.hp?
# Game Over
if newStats.hp <= 0
obj.stats.lvl = 0 # signifies dead
obj.stats.hp = 0
return
else
obj.stats.hp = newStats.hp
if newStats.exp?
# level up & carry-over exp
tnl = model.get '_tnl'
silent = false
if newStats.exp >= tnl
silent = true
user.set('stats.exp', newStats.exp)
newStats.exp -= tnl
obj.stats.lvl++
obj.stats.hp = 50
obj.stats.exp = newStats.exp
user.pass(silent:true).set('stats.exp', obj.stats.exp) if silent
# Set flags when they unlock features
if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1)
batch.set 'flags.customizationsNotification', true
obj.flags.customizationsNotification = true
if !obj.flags.itemsEnabled and obj.stats.lvl >= 2
# Set to object, then also send to browser right away to get model.on() subscription notification
batch.set 'flags.itemsEnabled', true
obj.flags.itemsEnabled = true
if !obj.flags.partyEnabled and obj.stats.lvl >= 3
batch.set 'flags.partyEnabled', true
obj.flags.partyEnabled = true
if !obj.flags.petsEnabled and obj.stats.lvl >= 4
batch.set 'flags.petsEnabled', true
obj.flags.petsEnabled = true
if newStats.gp?
#FIXME what was I doing here? I can't remember, gp isn't defined
gp = 0.0 if (!gp? or gp<0)
obj.stats.gp = newStats.gp
###
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
@ -249,7 +223,7 @@ module.exports.Scoring = (model) ->
lvl = 0 #iterator
while lvl < (obj.stats.lvl-1)
lvl++
expTally += (lvl*100)/5
expTally += algos.tnl(lvl)
obj.history.exp.push { date: today, value: expTally }
# Set the new user specs, and animate HP loss
@ -262,12 +236,11 @@ module.exports.Scoring = (model) ->
return {
MODIFIER: MODIFIER
score: score
cron: cron
# testing stuff
expModifier: expModifier
hpModifier: hpModifier
taskDeltaFormula: taskDeltaFormula
expModifier: algos.expModifier
hpModifier: algos.hpModifier
taskDeltaFormula: algos.taskDeltaFormula
}

View file

@ -8,6 +8,8 @@ lvlColor = #d9edf7
lvlText = #3a87ad
xpColor = #DCC0FB
xpText = #635673
deathColor = #4E4E4E
deathText = #ABABAB
borderDarken = 20%
// alert styles
@ -32,11 +34,30 @@ borderDarken = 20%
border-color: darken(xpColor,borderDarken)
color: xpText
.alert-death
background-color: deathColor
border-color: deathColor
color: deathText
// alert icons
.icon-gp
background: url("img/sprites/shop_sprites.png") no-repeat
background-size: 400px
background-position: -386px 0
.icon-gold
background: url("img/coin_single_gold.png") no-repeat
background-position: center center
background-size: 18px
width: 14px
height: 14px
.icon-silver
background: url("img/coin_single_silver.png") no-repeat
background-position: center center
background-size: 18px
width: 14px
height: 14px
.icon-death
background: url("img/sprites/dead.png") no-repeat
background-position: center center
background-size: 14px
width: 14px
height: 14px

View file

@ -20,6 +20,7 @@
<div class='pull-right'>
<button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button>
<button class='btn' x-bind="click:cheat">Add GP & Exp</button>
<button class='btn' x-bind='click:reset'>Reset Level</button>
</div>
{/}
<div>

View file

@ -29,7 +29,7 @@
{#if _user.history.exp}
<a x-bind=click:toggleChart data-toggle-id="exp-chart" data-history-path="_user.history.exp" rel=tooltip title="Progress"><i class=icon-signal></i></a>&nbsp;
{/}
<i class=icon-star></i> {round(_user.stats.exp)} / {_tnl}
<i class=icon-star></i> {floor(_user.stats.exp)} / {_tnl}
</span>
</div>
<div id="exp-chart" style="display:none;"></div>